1
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import wizard
|
||||
@@ -0,0 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
{
|
||||
'name': "Barcode",
|
||||
'summary': "Use barcode scanners to process logistics operations",
|
||||
'description': """
|
||||
This module enables the barcode scanning feature for the warehouse management system.
|
||||
""",
|
||||
'category': 'Inventory/Inventory',
|
||||
'sequence': 255,
|
||||
'version': '1.0',
|
||||
'depends': ['stock', 'web_tour', 'web_mobile'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/stock_inventory_views.xml',
|
||||
'views/stock_picking_views.xml',
|
||||
'views/stock_move_line_views.xml',
|
||||
'views/stock_barcode_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/stock_scrap_views.xml',
|
||||
'views/stock_location_views.xml',
|
||||
'wizard/stock_barcode_cancel_operation.xml',
|
||||
'wizard/stock_backorder_confirmation_views.xml',
|
||||
'data/data.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/demo.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'license': 'OEEL-1',
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'stock_barcode/static/src/**/*.js',
|
||||
'stock_barcode/static/src/**/*.scss',
|
||||
'stock_barcode/static/src/**/*.xml',
|
||||
|
||||
# Don't include dark mode files in light mode
|
||||
('remove', 'stock_barcode/static/src/**/*.dark.scss'),
|
||||
],
|
||||
"web.assets_web_dark": [
|
||||
'stock_barcode/static/src/**/*.dark.scss',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'stock_barcode/static/tests/units/**/*',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'stock_barcode/static/tests/tours/**/*',
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import stock_barcode
|
||||
@@ -0,0 +1,356 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import http, _
|
||||
from odoo.http import request
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import pdf, split_every
|
||||
from odoo.tools.misc import file_open
|
||||
|
||||
|
||||
class StockBarcodeController(http.Controller):
|
||||
|
||||
@http.route('/stock_barcode/scan_from_main_menu', type='json', auth='user')
|
||||
def main_menu(self, barcode):
|
||||
""" Receive a barcode scanned from the main menu and return the appropriate
|
||||
action (open an existing / new picking) or warning.
|
||||
"""
|
||||
barcode_type = None
|
||||
nomenclature = request.env.company.nomenclature_id
|
||||
if nomenclature.is_gs1_nomenclature:
|
||||
parsed_results = nomenclature.parse_barcode(barcode)
|
||||
if parsed_results:
|
||||
# search with the last feasible rule
|
||||
for result in parsed_results[::-1]:
|
||||
if result['rule'].type in ['product', 'package', 'location', 'dest_location']:
|
||||
barcode_type = result['rule'].type
|
||||
break
|
||||
|
||||
if not barcode_type:
|
||||
ret_open_picking = self._try_open_picking(barcode)
|
||||
if ret_open_picking:
|
||||
return ret_open_picking
|
||||
|
||||
ret_open_picking_type = self._try_open_picking_type(barcode)
|
||||
if ret_open_picking_type:
|
||||
return ret_open_picking_type
|
||||
|
||||
if request.env.user.has_group('stock.group_stock_multi_locations') and \
|
||||
(not barcode_type or barcode_type in ['location', 'dest_location']):
|
||||
ret_new_internal_picking = self._try_new_internal_picking(barcode)
|
||||
if ret_new_internal_picking:
|
||||
return ret_new_internal_picking
|
||||
|
||||
if not barcode_type or barcode_type == 'product':
|
||||
ret_open_product_location = self._try_open_product_location(barcode)
|
||||
if ret_open_product_location:
|
||||
return ret_open_product_location
|
||||
|
||||
if not barcode_type or barcode_type == 'lot':
|
||||
ret_open_lot_location = self._try_open_lot_location(barcode)
|
||||
if ret_open_lot_location:
|
||||
return ret_open_lot_location
|
||||
|
||||
if request.env.user.has_group('stock.group_tracking_lot') and \
|
||||
(not barcode_type or barcode_type == 'package'):
|
||||
ret_open_package = self._try_open_package(barcode)
|
||||
if ret_open_package:
|
||||
return ret_open_package
|
||||
|
||||
if request.env.user.has_group('stock.group_stock_multi_locations'):
|
||||
return {'warning': _('No picking or location or product corresponding to barcode %(barcode)s', barcode=barcode)}
|
||||
else:
|
||||
return {'warning': _('No picking or product corresponding to barcode %(barcode)s', barcode=barcode)}
|
||||
|
||||
@http.route('/stock_barcode/save_barcode_data', type='json', auth='user')
|
||||
def save_barcode_data(self, model, res_id, write_field, write_vals):
|
||||
if not res_id:
|
||||
return request.env[model].barcode_write(write_vals)
|
||||
target_record = request.env[model].browse(res_id)
|
||||
target_record.write({write_field: write_vals})
|
||||
return target_record._get_stock_barcode_data()
|
||||
|
||||
@http.route('/stock_barcode/get_barcode_data', type='json', auth='user')
|
||||
def get_barcode_data(self, model, res_id):
|
||||
""" Returns a dict with values used by the barcode client:
|
||||
{
|
||||
"data": <data used by the stock barcode> {'records' : {'model': [{<record>}, ... ]}, 'other_infos':...}, _get_barcode_data_prefetch
|
||||
"groups": <security group>, self._get_groups_data
|
||||
}
|
||||
"""
|
||||
if not res_id:
|
||||
target_record = request.env[model].with_context(allowed_company_ids=self._get_allowed_company_ids())
|
||||
else:
|
||||
target_record = request.env[model].browse(res_id).with_context(allowed_company_ids=self._get_allowed_company_ids())
|
||||
data = target_record._get_stock_barcode_data()
|
||||
data['records'].update(self._get_barcode_nomenclature())
|
||||
data['precision'] = request.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
mute_sound = request.env['ir.config_parameter'].sudo().get_param('stock_barcode.mute_sound_notifications')
|
||||
config = {'play_sound': bool(not mute_sound or mute_sound == "False")}
|
||||
return {
|
||||
'data': data,
|
||||
'groups': self._get_groups_data(),
|
||||
'config': config,
|
||||
}
|
||||
|
||||
@http.route('/stock_barcode/get_specific_barcode_data', type='json', auth='user')
|
||||
def get_specific_barcode_data(self, barcode, model_name, domains_by_model=False):
|
||||
nomenclature = request.env.company.nomenclature_id
|
||||
# Adapts the search parameters for GS1 specifications.
|
||||
operator = '='
|
||||
limit = None if nomenclature.is_gs1_nomenclature else 1
|
||||
if nomenclature.is_gs1_nomenclature:
|
||||
try:
|
||||
# If barcode is digits only, cut off the padding to keep the original barcode only.
|
||||
barcode = str(int(barcode))
|
||||
operator = 'ilike'
|
||||
except ValueError:
|
||||
pass # Barcode isn't digits only.
|
||||
|
||||
domains_by_model = domains_by_model or {}
|
||||
barcode_field_by_model = self._get_barcode_field_by_model()
|
||||
result = defaultdict(list)
|
||||
model_names = model_name and [model_name] or list(barcode_field_by_model.keys())
|
||||
universal_domain = domains_by_model.get('all')
|
||||
for model in model_names:
|
||||
domain = [
|
||||
(barcode_field_by_model[model], operator, barcode),
|
||||
]
|
||||
domain_for_this_model = domains_by_model.get(model)
|
||||
if domain_for_this_model:
|
||||
domain = expression.AND([domain, domain_for_this_model])
|
||||
if universal_domain:
|
||||
domain = expression.AND([domain, universal_domain])
|
||||
record = request.env[model].with_context(display_default_code=False).search(domain, limit=limit)
|
||||
if record:
|
||||
result[model] += record.read(request.env[model]._get_fields_stock_barcode(), load=False)
|
||||
if hasattr(record, '_get_stock_barcode_specific_data'):
|
||||
additional_result = record._get_stock_barcode_specific_data()
|
||||
for key in additional_result:
|
||||
result[key] += additional_result[key]
|
||||
return result
|
||||
|
||||
@http.route('/stock_barcode/rid_of_message_demo_barcodes', type='json', auth='user')
|
||||
def rid_of_message_demo_barcodes(self, **kw):
|
||||
""" Edit the main_menu client action so that it doesn't display the 'print demo barcodes sheet' message """
|
||||
if not request.env.user.has_group('stock.group_stock_user'):
|
||||
return request.not_found()
|
||||
action = request.env.ref('stock_barcode.stock_barcode_action_main_menu')
|
||||
action and action.sudo().write({'params': {'message_demo_barcodes': False}})
|
||||
|
||||
@http.route('/stock_barcode/print_inventory_commands', type='http', auth='user')
|
||||
def print_inventory_commands(self, barcode_type=False):
|
||||
if not request.env.user.has_group('stock.group_stock_user'):
|
||||
return request.not_found()
|
||||
|
||||
# make sure we use the selected company if possible
|
||||
allowed_company_ids = self._get_allowed_company_ids()
|
||||
|
||||
# same domain conditions for picking types and locations
|
||||
domain = self._get_picking_type_domain(barcode_type, allowed_company_ids)
|
||||
|
||||
# get fixed command barcodes
|
||||
barcode_pdfs = self._get_barcode_pdfs(barcode_type, domain)
|
||||
|
||||
if not barcode_pdfs:
|
||||
raise UserError(_("Barcodes are not available."))
|
||||
merged_pdf = pdf.merge_pdf(barcode_pdfs)
|
||||
|
||||
pdfhttpheaders = [
|
||||
('Content-Type', 'application/pdf'),
|
||||
('Content-Length', len(merged_pdf))
|
||||
]
|
||||
|
||||
return request.make_response(merged_pdf, headers=pdfhttpheaders)
|
||||
|
||||
def _try_open_lot_location(self, barcode):
|
||||
""" If barcode represent a lot, open a list/kanban view to show all
|
||||
the locations of this lot.
|
||||
"""
|
||||
result = request.env['stock.lot'].search_read([
|
||||
('name', '=', barcode),
|
||||
], ['id', 'display_name'], limit=1)
|
||||
if result:
|
||||
tree_view_id = request.env.ref('stock.view_stock_quant_tree').id
|
||||
kanban_view_id = request.env.ref('stock_barcode.stock_quant_barcode_kanban_2').id
|
||||
return {
|
||||
'action': {
|
||||
'name': result[0]['display_name'],
|
||||
'res_model': 'stock.quant',
|
||||
'views': [(tree_view_id, 'list'), (kanban_view_id, 'kanban')],
|
||||
'type': 'ir.actions.act_window',
|
||||
'domain': [('lot_id', '=', result[0]['id'])],
|
||||
'context': {
|
||||
'search_default_internal_loc': True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def _try_open_product_location(self, barcode):
|
||||
""" If barcode represent a product, open a list/kanban view to show all
|
||||
the locations of this product.
|
||||
"""
|
||||
result = request.env['product.product'].search_read([
|
||||
('barcode', '=', barcode),
|
||||
], ['id', 'display_name'], limit=1)
|
||||
if result:
|
||||
tree_view_id = request.env.ref('stock.view_stock_quant_tree').id
|
||||
kanban_view_id = request.env.ref('stock_barcode.stock_quant_barcode_kanban_2').id
|
||||
return {
|
||||
'action': {
|
||||
'name': result[0]['display_name'],
|
||||
'res_model': 'stock.quant',
|
||||
'views': [(tree_view_id, 'list'), (kanban_view_id, 'kanban')],
|
||||
'type': 'ir.actions.act_window',
|
||||
'domain': [('product_id', '=', result[0]['id'])],
|
||||
'context': {
|
||||
'search_default_internal_loc': True,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
def _try_open_picking_type(self, barcode):
|
||||
""" If barcode represent a picking type, open a new
|
||||
picking with this type
|
||||
"""
|
||||
picking_type = request.env['stock.picking.type'].search([
|
||||
('barcode', '=', barcode),
|
||||
('company_id', 'in', [False, *self._get_allowed_company_ids()]),
|
||||
], limit=1)
|
||||
if picking_type:
|
||||
picking = request.env['stock.picking']._create_new_picking(picking_type)
|
||||
return picking._get_client_action()
|
||||
return False
|
||||
|
||||
def _try_open_picking(self, barcode):
|
||||
""" If barcode represents a picking, open it
|
||||
"""
|
||||
corresponding_picking = request.env['stock.picking'].search([
|
||||
('name', '=', barcode),
|
||||
], limit=1)
|
||||
if corresponding_picking:
|
||||
action = corresponding_picking.action_open_picking_client_action()
|
||||
return {'action': action}
|
||||
return False
|
||||
|
||||
def _try_open_package(self, barcode):
|
||||
""" If barcode represents a package, open it.
|
||||
"""
|
||||
package = request.env['stock.quant.package'].search([('name', '=', barcode)], limit=1)
|
||||
if package:
|
||||
view_id = request.env.ref('stock.view_quant_package_form').id
|
||||
return {
|
||||
'action': {
|
||||
'name': 'Open package',
|
||||
'res_model': 'stock.quant.package',
|
||||
'views': [(view_id, 'form')],
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_id': package.id,
|
||||
'context': {'active_id': package.id}
|
||||
}
|
||||
}
|
||||
return False
|
||||
|
||||
def _try_new_internal_picking(self, barcode):
|
||||
""" If barcode represents a location, open a new picking from this location
|
||||
"""
|
||||
corresponding_location = request.env['stock.location'].search([
|
||||
('barcode', '=', barcode),
|
||||
('usage', '=', 'internal')
|
||||
], limit=1)
|
||||
if corresponding_location:
|
||||
internal_picking_type = request.env['stock.picking.type'].search([('code', '=', 'internal')])
|
||||
warehouse = corresponding_location.warehouse_id
|
||||
if warehouse:
|
||||
internal_picking_type = internal_picking_type.filtered(lambda r: r.warehouse_id == warehouse)
|
||||
dest_loc = corresponding_location
|
||||
while dest_loc.location_id and dest_loc.location_id.usage == 'internal':
|
||||
dest_loc = dest_loc.location_id
|
||||
if internal_picking_type:
|
||||
# Create and confirm an internal picking
|
||||
picking = request.env['stock.picking'].create({
|
||||
'picking_type_id': internal_picking_type[0].id,
|
||||
'user_id': False,
|
||||
'location_id': corresponding_location.id,
|
||||
'location_dest_id': dest_loc.id,
|
||||
})
|
||||
picking.action_confirm()
|
||||
|
||||
return picking._get_client_action()
|
||||
else:
|
||||
return {'warning': _('No internal operation type. Please configure one in warehouse settings.')}
|
||||
return False
|
||||
|
||||
def _get_allowed_company_ids(self):
|
||||
""" Return the allowed_company_ids based on cookies.
|
||||
|
||||
Currently request.env.company returns the current user's company when called within a controller
|
||||
rather than the selected company in the company switcher and request.env.companies lists the
|
||||
current user's allowed companies rather than the selected companies.
|
||||
|
||||
:returns: List of active companies. The first company id in the returned list is the selected company.
|
||||
"""
|
||||
cids = request.httprequest.cookies.get('cids', str(request.env.user.company_id.id))
|
||||
return [int(cid) for cid in cids.split(',')]
|
||||
|
||||
def _get_picking_type_domain(self, barcode_type, allowed_company_ids):
|
||||
return [
|
||||
('active', '=', 'True'),
|
||||
('barcode', '!=', ''),
|
||||
('company_id', 'in', allowed_company_ids)
|
||||
]
|
||||
|
||||
def _get_barcode_pdfs(self, barcode_type, domain):
|
||||
barcode_pdfs = []
|
||||
if barcode_type == 'barcode_commands_and_operation_types':
|
||||
with file_open('stock_barcode/static/img/barcodes_actions.pdf', "rb") as commands_file:
|
||||
barcode_pdfs.append(commands_file.read())
|
||||
|
||||
if 'operation_types' in barcode_type:
|
||||
# get picking types barcodes
|
||||
picking_type_ids = request.env['stock.picking.type'].search(domain)
|
||||
for picking_type_batch in split_every(112, picking_type_ids.ids):
|
||||
picking_types_pdf, _content_type = request.env['ir.actions.report']._render_qweb_pdf('stock.action_report_picking_type_label', picking_type_batch)
|
||||
if picking_types_pdf:
|
||||
barcode_pdfs.append(picking_types_pdf)
|
||||
|
||||
# get locations barcodes
|
||||
if barcode_type == 'locations' and request.env.user.has_group('stock.group_stock_multi_locations'):
|
||||
locations_ids = request.env['stock.location'].search(domain)
|
||||
for location_ids_batch in split_every(112, locations_ids.ids):
|
||||
locations_pdf, _content_type = request.env['ir.actions.report']._render_qweb_pdf('stock.action_report_location_barcode', location_ids_batch)
|
||||
if locations_pdf:
|
||||
barcode_pdfs.append(locations_pdf)
|
||||
return barcode_pdfs
|
||||
|
||||
def _get_groups_data(self):
|
||||
return {
|
||||
'group_stock_multi_locations': request.env.user.has_group('stock.group_stock_multi_locations'),
|
||||
'group_tracking_owner': request.env.user.has_group('stock.group_tracking_owner'),
|
||||
'group_tracking_lot': request.env.user.has_group('stock.group_tracking_lot'),
|
||||
'group_production_lot': request.env.user.has_group('stock.group_production_lot'),
|
||||
'group_uom': request.env.user.has_group('uom.group_uom'),
|
||||
'group_stock_packaging': request.env.user.has_group('product.group_stock_packaging'),
|
||||
}
|
||||
|
||||
def _get_barcode_nomenclature(self):
|
||||
company = request.env['res.company'].browse(self._get_allowed_company_ids()[0])
|
||||
nomenclature = company.nomenclature_id
|
||||
return {
|
||||
"barcode.nomenclature": nomenclature.read(load=False),
|
||||
"barcode.rule": nomenclature.rule_ids.read(load=False)
|
||||
}
|
||||
|
||||
def _get_barcode_field_by_model(self):
|
||||
list_model = [
|
||||
'stock.location',
|
||||
'product.product',
|
||||
'product.packaging',
|
||||
'stock.picking',
|
||||
'stock.lot',
|
||||
'stock.quant.package',
|
||||
]
|
||||
return {model: request.env[model]._barcode_field for model in list_model if hasattr(request.env[model], '_barcode_field')}
|
||||
@@ -0,0 +1,24 @@
|
||||
<odoo>
|
||||
<record id="stock_barcode_picking_client_action" model="ir.actions.client">
|
||||
<field name="name">Barcode Picking Client Action</field>
|
||||
<field name="tag">stock_barcode_client_action</field>
|
||||
<field name="res_model">stock.picking</field>
|
||||
</record>
|
||||
|
||||
<record id="stock_barcode_inventory_client_action" model="ir.actions.client">
|
||||
<field name="name">Barcode Inventory Client Action</field>
|
||||
<field name="tag">stock_barcode_client_action</field>
|
||||
<field name="res_model">stock.quant</field>
|
||||
</record>
|
||||
|
||||
<data noupdate="1">
|
||||
<!-- Set `restrict_scan_source_location` on True for outgoing picking types. -->
|
||||
<function model="stock.picking.type" name="write">
|
||||
<value model="stock.picking.type" search="[('code', '=', 'outgoing')]"/>
|
||||
<value eval="{
|
||||
'restrict_scan_source_location': 'mandatory',
|
||||
'restrict_scan_dest_location': 'no',
|
||||
}"/>
|
||||
</function>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,119 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="product.product_product_22" model="product.product">
|
||||
<field name="barcode">601647855638</field>
|
||||
</record>
|
||||
<record id="stock.product_cable_management_box" model="product.product">
|
||||
<field name="barcode">601647855631</field>
|
||||
</record>
|
||||
<record id="product.product_product_12" model="product.product">
|
||||
<field name="barcode">601647855634</field>
|
||||
</record>
|
||||
<record id="product.consu_delivery_01" model="product.product">
|
||||
<field name="barcode">601647855635</field>
|
||||
</record>
|
||||
<record id="product.consu_delivery_02" model="product.product">
|
||||
<field name="barcode">601647855636</field>
|
||||
</record>
|
||||
<record id="product.consu_delivery_02" model="product.product">
|
||||
<field name="barcode">601647855637</field>
|
||||
</record>
|
||||
<record id="product.product_product_13" model="product.product">
|
||||
<field name="barcode">601647855640</field>
|
||||
</record>
|
||||
<record id="product.product_product_4" model="product.product">
|
||||
<field name="barcode">601647855641</field>
|
||||
</record>
|
||||
<record id="product.product_product_4b" model="product.product">
|
||||
<field name="barcode">601647855642</field>
|
||||
</record>
|
||||
<record id="product.product_product_4c" model="product.product">
|
||||
<field name="barcode">601647855643</field>
|
||||
</record>
|
||||
<record id="product.product_product_3" model="product.product">
|
||||
<field name="barcode">601647855644</field>
|
||||
</record>
|
||||
<record id="product.product_product_16" model="product.product">
|
||||
<field name="barcode">601647855645</field>
|
||||
</record>
|
||||
<record id="product.product_product_27" model="product.product">
|
||||
<field name="barcode">601647855648</field>
|
||||
</record>
|
||||
<record id="product.product_product_9" model="product.product">
|
||||
<field name="barcode">601647855649</field>
|
||||
</record>
|
||||
<record id="product.product_product_6" model="product.product">
|
||||
<field name="barcode">601647855650</field>
|
||||
</record>
|
||||
<record id="product.consu_delivery_03" model="product.product">
|
||||
<field name="barcode">601647855651</field>
|
||||
</record>
|
||||
<record id="product.product_product_10" model="product.product">
|
||||
<field name="barcode">601647855652</field>
|
||||
</record>
|
||||
<record id="product.product_product_25" model="product.product">
|
||||
<field name="barcode">601647855653</field>
|
||||
</record>
|
||||
|
||||
<record id="demo_package" model="stock.quant.package" />
|
||||
|
||||
<record id="stock.lot_product_cable_management" model="stock.lot">
|
||||
<field name="ref">0000000000017</field>
|
||||
</record>
|
||||
|
||||
<record id="stock_barcode.stock_barcode_action_main_menu" model="ir.actions.client">
|
||||
<field name="params" eval="{'message_demo_barcodes': True}" />
|
||||
</record>
|
||||
|
||||
<!-- GS1 Specific Barcodes -->
|
||||
<record id="product_cable_management_box_2" model="product.product">
|
||||
<field name="default_code">FURN_5800</field>
|
||||
<field name="name">Cable Management Box</field>
|
||||
<field name="detailed_type">product</field>
|
||||
<field name="weight">0.01</field>
|
||||
<field name="categ_id" ref="product.product_category_5"/>
|
||||
<field name="lst_price">120.0</field>
|
||||
<field name="standard_price">90.0</field>
|
||||
<field name="weight">1.1</field>
|
||||
<field name="tracking">lot</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit"/>
|
||||
<field name="image_1920" type="base64" file="stock/static/img/cable_management.png"/>
|
||||
<field name="barcode">06016478556677</field>
|
||||
</record>
|
||||
<record id="product_custom_cabinet_usa" model="product.product">
|
||||
<field name="name">Customized Cabinet (USA)</field>
|
||||
<field name="categ_id" ref="product.product_category_5"/>
|
||||
<field name="standard_price">175.50</field>
|
||||
<field name="list_price">200</field>
|
||||
<field name="detailed_type">product</field>
|
||||
<field name="weight">2</field>
|
||||
<field name="uom_id" ref="uom.product_uom_cubic_foot"/>
|
||||
<field name="uom_po_id" ref="uom.product_uom_cubic_foot"/>
|
||||
<field name="default_code">E-COM99</field>
|
||||
<field name="image_1920" type="base64" file="product/static/img/product_product_10-image.jpg"/>
|
||||
<field name="barcode">06016478559999</field>
|
||||
</record>
|
||||
<record id="product_custom_cabinet_metric" model="product.product">
|
||||
<field name="name">Customized Cabinet (Metric)</field>
|
||||
<field name="categ_id" ref="product.product_category_5"/>
|
||||
<field name="standard_price">190.50</field>
|
||||
<field name="list_price">210</field>
|
||||
<field name="detailed_type">product</field>
|
||||
<field name="weight">2</field>
|
||||
<field name="uom_id" ref="uom.product_uom_cubic_meter"/>
|
||||
<field name="uom_po_id" ref="uom.product_uom_cubic_meter"/>
|
||||
<field name="default_code">E-COM98</field>
|
||||
<field name="image_1920" type="base64" file="product/static/img/product_product_10-image.jpg"/>
|
||||
<field name="barcode">06016478559982</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_24" model="product.product">
|
||||
<field name="barcode">06016478556332</field>
|
||||
</record>
|
||||
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import stock_picking
|
||||
from . import stock_quant
|
||||
from . import stock_scrap
|
||||
from . import stock_location
|
||||
from . import stock_move
|
||||
from . import stock_move_line
|
||||
from . import stock_package_type
|
||||
from . import stock_lot
|
||||
from . import stock_quant_package
|
||||
from . import stock_warehouse
|
||||
from . import product_product
|
||||
from . import product_packaging
|
||||
from . import res_config_settings
|
||||
from . import res_partner
|
||||
from . import uom_uom
|
||||
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, api
|
||||
|
||||
|
||||
class ProductPackaging(models.Model):
|
||||
_inherit = 'product.packaging'
|
||||
_barcode_field = 'barcode'
|
||||
|
||||
def _get_stock_barcode_specific_data(self):
|
||||
products = self.product_id
|
||||
return {
|
||||
'product.product': products.read(self.env['product.product']._get_fields_stock_barcode(), load=False),
|
||||
'uom.uom': products.uom_id.read(self.env['uom.uom']._get_fields_stock_barcode(), load=False)
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_fields_stock_barcode(self):
|
||||
return ['barcode', 'product_id', 'qty', 'name']
|
||||
@@ -0,0 +1,53 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, api
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
_inherit = 'product.product'
|
||||
_barcode_field = 'barcode'
|
||||
|
||||
@api.model
|
||||
def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None):
|
||||
# sudo is added for external users to get the products
|
||||
domain = self.env.company.sudo().nomenclature_id._preprocess_gs1_search_args(domain, ['product'])
|
||||
return super()._search(domain, offset=offset, limit=limit, order=order, access_rights_uid=access_rights_uid)
|
||||
|
||||
@api.model
|
||||
def _get_fields_stock_barcode(self):
|
||||
return ['barcode', 'default_code', 'categ_id', 'code', 'detailed_type', 'tracking', 'display_name', 'uom_id']
|
||||
|
||||
def _get_stock_barcode_specific_data(self):
|
||||
return {
|
||||
'uom.uom': self.uom_id.read(self.env['uom.uom']._get_fields_stock_barcode(), load=False)
|
||||
}
|
||||
|
||||
def prefilled_owner_package_stock_barcode(self, lot_id=False, lot_name=False):
|
||||
domain = [
|
||||
lot_id and ('lot_id', '=', lot_id) or lot_name and ('lot_id.name', '=', lot_name),
|
||||
('product_id', '=', self.id),
|
||||
'|', ('package_id', '!=', False), ('owner_id', '!=', False),
|
||||
]
|
||||
|
||||
location_id = self.env.context.get('location_id', {}).get('id')
|
||||
if location_id:
|
||||
domain = expression.AND([domain, [('location_id', '=', location_id)]])
|
||||
else:
|
||||
domain = expression.AND([domain, [('location_id.usage', '=', 'internal')]])
|
||||
|
||||
quant = self.env['stock.quant'].search_read(
|
||||
domain,
|
||||
['package_id', 'owner_id'],
|
||||
limit=1, load=False, order='package_id',
|
||||
)
|
||||
if quant:
|
||||
quant = quant[0]
|
||||
res = {'quant': quant, 'records': {}}
|
||||
if quant and quant['package_id']:
|
||||
res['records']['stock.quant.package'] = self.env['stock.quant.package'].browse(quant['package_id']).read(self.env['stock.quant.package']._get_fields_stock_barcode(), load=False)
|
||||
if quant and quant['owner_id']:
|
||||
res['records']['res.partner'] = self.env['res.partner'].browse(quant['owner_id']).read(self.env['res.partner']._get_fields_stock_barcode(), load=False)
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
barcode_nomenclature_id = fields.Many2one('barcode.nomenclature', related='company_id.nomenclature_id', readonly=False)
|
||||
stock_barcode_demo_active = fields.Boolean("Demo Data Active", compute='_compute_stock_barcode_demo_active')
|
||||
show_barcode_nomenclature = fields.Boolean(compute='_compute_show_barcode_nomenclature')
|
||||
|
||||
@api.depends('company_id')
|
||||
def _compute_show_barcode_nomenclature(self):
|
||||
self.show_barcode_nomenclature = self.module_stock_barcode and self.env['barcode.nomenclature'].search_count([]) > 1
|
||||
|
||||
@api.depends('company_id')
|
||||
def _compute_stock_barcode_demo_active(self):
|
||||
for rec in self:
|
||||
rec.stock_barcode_demo_active = bool(self.env['ir.module.module'].search([('name', '=', 'stock_barcode'), ('demo', '=', True)]))
|
||||
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, api
|
||||
|
||||
|
||||
class Partner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
@api.model
|
||||
def _get_fields_stock_barcode(self):
|
||||
return ['display_name']
|
||||
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, api
|
||||
|
||||
|
||||
class Location(models.Model):
|
||||
_inherit = 'stock.location'
|
||||
_barcode_field = 'barcode'
|
||||
|
||||
@api.model
|
||||
def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None):
|
||||
domain = self.env.company.nomenclature_id._preprocess_gs1_search_args(domain, ['location', 'location_dest'])
|
||||
return super()._search(domain, offset=offset, limit=limit, order=order, access_rights_uid=access_rights_uid)
|
||||
|
||||
@api.model
|
||||
def _get_fields_stock_barcode(self):
|
||||
return ['barcode', 'display_name', 'name', 'parent_path', 'usage']
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, api
|
||||
|
||||
|
||||
class StockLot(models.Model):
|
||||
_inherit = 'stock.lot'
|
||||
_barcode_field = 'name'
|
||||
|
||||
@api.model
|
||||
def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None):
|
||||
# sudo is added for external users to get the lots
|
||||
domain = self.env.company.sudo().nomenclature_id._preprocess_gs1_search_args(domain, ['lot'], field='name')
|
||||
return super()._search(domain, offset=offset, limit=limit, order=order, access_rights_uid=access_rights_uid)
|
||||
|
||||
def _get_stock_barcode_specific_data(self):
|
||||
products = self.product_id
|
||||
return {
|
||||
'product.product': products.read(self.env['product.product']._get_fields_stock_barcode(), load=False),
|
||||
'uom.uom': products.uom_id.read(self.env['uom.uom']._get_fields_stock_barcode(), load=False)
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_fields_stock_barcode(self):
|
||||
return ['name', 'ref', 'product_id']
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
from odoo import models
|
||||
from odoo.tools.float_utils import float_compare
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = 'stock.move'
|
||||
|
||||
def split_uncompleted_moves(self):
|
||||
""" Creates a new move for every uncompleted move in order to get one picked move
|
||||
with the picked quantity, and one not picked move with the remaining quantity."""
|
||||
# Backport of `stock.move` `_create_backorder` method. For more details, see
|
||||
# https://github.com/odoo/odoo/commit/7472d5546580f28d58abfd20774b6e3eedc9f29a
|
||||
# Split moves where necessary and move quants
|
||||
new_moves_vals = []
|
||||
for move in self:
|
||||
if move.picked and move.quantity == 0:
|
||||
move.move_line_ids.unlink()
|
||||
move.quantity = move.product_uom_qty
|
||||
move.picked = False
|
||||
if not move.picked:
|
||||
continue
|
||||
# To know whether we need to split a move, rounds to the general
|
||||
# product's decimal precision and not the product's UOM.
|
||||
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
if float_compare(move.quantity, move.product_uom_qty, precision_digits=rounding) < 0:
|
||||
# Need to do some kind of conversion here
|
||||
qty_split = move.product_uom._compute_quantity(
|
||||
move.product_uom_qty - move.quantity,
|
||||
move.product_id.uom_id,
|
||||
rounding_method='HALF-UP')
|
||||
new_move_vals = move._split(qty_split)
|
||||
new_moves_vals += new_move_vals
|
||||
new_moves = self.env['stock.move'].create(new_moves_vals)
|
||||
# The new moves are not yet in their own picking. We do not want to check entire packs for those
|
||||
# ones as it could messed up the result_package_id of the moves being currently validated
|
||||
new_moves.with_context(bypass_entire_pack=True)._action_confirm(merge=False)
|
||||
if new_moves:
|
||||
# In some case, we already split the move lines in the front end.
|
||||
# Those move lines are linked to the original move. If their quantity
|
||||
# is 0 and they are already picked, there is no reason to keep them.
|
||||
moves_to_clean = self - new_moves
|
||||
move_lines_to_unlink = self.env['stock.move.line']
|
||||
for move in moves_to_clean:
|
||||
for move_line in move.move_line_ids:
|
||||
if move_line.quantity == 0 and move_line.picked:
|
||||
move_lines_to_unlink |= move_line
|
||||
move_lines_to_unlink.unlink()
|
||||
return new_moves
|
||||
@@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import frozendict
|
||||
|
||||
|
||||
class StockMoveLine(models.Model):
|
||||
_inherit = 'stock.move.line'
|
||||
|
||||
product_barcode = fields.Char(related='product_id.barcode')
|
||||
location_processed = fields.Boolean()
|
||||
dummy_id = fields.Char(compute='_compute_dummy_id', inverse='_inverse_dummy_id')
|
||||
parent_location_id = fields.Many2one('stock.location', compute='_compute_parent_location_id')
|
||||
parent_location_dest_id = fields.Many2one('stock.location', compute='_compute_parent_location_id')
|
||||
product_stock_quant_ids = fields.One2many('stock.quant', compute='_compute_product_stock_quant_ids')
|
||||
product_packaging_id = fields.Many2one(related='move_id.product_packaging_id')
|
||||
product_packaging_uom_qty = fields.Float('Packaging Quantity', compute='_compute_product_packaging_uom_qty', help="Quantity of the Packaging in the UoM of the Stock Move Line.")
|
||||
hide_lot_name = fields.Boolean(compute='_compute_hide_lot_name')
|
||||
hide_lot = fields.Boolean(compute='_compute_hide_lot_name')
|
||||
image_1920 = fields.Image(related="product_id.image_1920")
|
||||
product_reference_code = fields.Char(related="product_id.code", string="Product Reference Code")
|
||||
qty_done = fields.Float(compute='_compute_qty_done', inverse='_inverse_qty_done') # Dummy field
|
||||
|
||||
@api.depends('tracking', 'picking_type_use_existing_lots', 'picking_type_use_create_lots', 'lot_name')
|
||||
def _compute_hide_lot_name(self):
|
||||
for line in self:
|
||||
if line.tracking == 'none':
|
||||
line.hide_lot_name = True
|
||||
line.hide_lot = True
|
||||
continue
|
||||
line.hide_lot_name = not line.picking_type_use_create_lots or (line.picking_type_use_existing_lots and not line.lot_name)
|
||||
line.hide_lot = not line.picking_type_use_existing_lots or (line.picking_type_use_create_lots and line.lot_name)
|
||||
|
||||
@api.depends('picking_id')
|
||||
def _compute_parent_location_id(self):
|
||||
for line in self:
|
||||
line.parent_location_id = line.picking_id.location_id
|
||||
line.parent_location_dest_id = line.picking_id.location_dest_id
|
||||
|
||||
def _compute_product_stock_quant_ids(self):
|
||||
for line in self:
|
||||
line.product_stock_quant_ids = line.product_id.stock_quant_ids.filtered(lambda q: q.company_id in self.env.companies and q.location_id.usage == 'internal')
|
||||
|
||||
def _compute_dummy_id(self):
|
||||
self.dummy_id = ''
|
||||
|
||||
def _compute_qty_done(self):
|
||||
for line in self:
|
||||
line.qty_done = line.quantity if line.picked else 0
|
||||
|
||||
def _compute_product_packaging_uom_qty(self):
|
||||
for sml in self:
|
||||
sml.product_packaging_uom_qty = sml.product_packaging_id.product_uom_id._compute_quantity(sml.product_packaging_id.qty, sml.product_uom_id)
|
||||
|
||||
def _inverse_dummy_id(self):
|
||||
pass
|
||||
|
||||
def _inverse_qty_done(self):
|
||||
for line in self:
|
||||
line.quantity = line.qty_done
|
||||
line.picked = line.quantity > 0
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
# To avoid a write on `quantity` at the creation of the record (in the `qty_done`
|
||||
# inverse, when the line's move is not created yet), we set the `quantity` directly at
|
||||
# the creation and remove `qty_done` in the meantime.
|
||||
if 'qty_done' in vals:
|
||||
vals['quantity'] = vals['qty_done']
|
||||
vals['picked'] = vals['qty_done'] > 0
|
||||
del vals['qty_done']
|
||||
# Also delete the default value in the context.
|
||||
self.env.context = frozendict({k: v for k, v in self.env.context.items() if k != 'default_qty_done'})
|
||||
return super().create(vals_list)
|
||||
|
||||
|
||||
def _get_fields_stock_barcode(self):
|
||||
return [
|
||||
'product_id',
|
||||
'product_category_name',
|
||||
'location_id',
|
||||
'location_dest_id',
|
||||
'move_id',
|
||||
'qty_done',
|
||||
'quantity',
|
||||
'display_name',
|
||||
'product_uom_id',
|
||||
'product_barcode',
|
||||
'owner_id',
|
||||
'lot_id',
|
||||
'lot_name',
|
||||
'package_id',
|
||||
'result_package_id',
|
||||
'dummy_id',
|
||||
'picked',
|
||||
'product_packaging_id',
|
||||
'product_packaging_uom_qty',
|
||||
'move_id',
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class PackageType(models.Model):
|
||||
_inherit = 'stock.package.type'
|
||||
|
||||
@api.model
|
||||
def _get_fields_stock_barcode(self):
|
||||
return ['barcode', 'name']
|
||||
@@ -0,0 +1,375 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import fields, models, api, _
|
||||
from odoo.tools import html2plaintext, is_html_empty
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class StockPicking(models.Model):
|
||||
_inherit = 'stock.picking'
|
||||
_barcode_field = 'name'
|
||||
|
||||
def action_cancel_from_barcode(self):
|
||||
self.ensure_one()
|
||||
view = self.env.ref('stock_barcode.stock_barcode_cancel_operation_view')
|
||||
return {
|
||||
'name': _('Cancel this operation?'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'stock_barcode.cancel.operation',
|
||||
'views': [(view.id, 'form')],
|
||||
'view_id': view.id,
|
||||
'target': 'new',
|
||||
'context': dict(self.env.context, default_picking_id=self.id),
|
||||
}
|
||||
|
||||
@api.model
|
||||
def action_open_new_picking(self):
|
||||
""" Creates a new picking of the current picking type and open it.
|
||||
|
||||
:return: the action used to open the picking, or false
|
||||
:rtype: dict
|
||||
"""
|
||||
context = self.env.context
|
||||
if context.get('active_model') == 'stock.picking.type':
|
||||
picking_type = self.env['stock.picking.type'].browse(context.get('active_id'))
|
||||
if picking_type.exists():
|
||||
new_picking = self._create_new_picking(picking_type)
|
||||
return new_picking._get_client_action()['action']
|
||||
return False
|
||||
|
||||
def action_open_picking(self):
|
||||
""" method to open the form view of the current record
|
||||
from a button on the kanban view
|
||||
"""
|
||||
self.ensure_one()
|
||||
view_id = self.env.ref('stock.view_picking_form').id
|
||||
return {
|
||||
'name': _('Open picking form'),
|
||||
'res_model': 'stock.picking',
|
||||
'view_mode': 'form',
|
||||
'view_id': view_id,
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_id': self.id,
|
||||
}
|
||||
|
||||
def action_open_picking_client_action(self):
|
||||
""" method to open the form view of the current record
|
||||
from a button on the kanban view
|
||||
"""
|
||||
self.ensure_one()
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("stock_barcode.stock_barcode_picking_client_action")
|
||||
action = dict(action, target='fullscreen')
|
||||
action['context'] = {'active_id': self.id}
|
||||
return action
|
||||
|
||||
def action_create_return_picking(self):
|
||||
"""
|
||||
Create a return picking for the current picking and open it in the barcode app
|
||||
"""
|
||||
self.ensure_one()
|
||||
return_picking = self.with_context(active_model='stock.picking', active_id=self.id).env['stock.return.picking'].create({})
|
||||
if sum(return_picking.product_return_moves.mapped('quantity')) <= 0:
|
||||
raise UserError(_("All products have been returned already"))
|
||||
picking_id, _pt = return_picking._create_returns()
|
||||
new_picking = self.env['stock.picking'].browse(picking_id)
|
||||
return new_picking._get_client_action()['action']
|
||||
|
||||
def action_print_barcode(self):
|
||||
return self.action_open_label_type()
|
||||
|
||||
def action_print_delivery_slip(self):
|
||||
return self.env.ref('stock.action_report_delivery').report_action(self)
|
||||
|
||||
def action_print_packges(self):
|
||||
return self.env.ref('stock.action_report_picking_packages').report_action(self)
|
||||
|
||||
def _get_stock_barcode_data(self):
|
||||
# Avoid to get the products full name because code and name are separate in the barcode app.
|
||||
self = self.with_context(display_default_code=False)
|
||||
move_lines = self.move_line_ids
|
||||
lots = move_lines.lot_id
|
||||
owners = move_lines.owner_id
|
||||
# Fetch all implied products in `self` and adds last used products to avoid additional rpc.
|
||||
products = move_lines.product_id
|
||||
packagings = products.packaging_ids
|
||||
|
||||
uoms = products.uom_id | move_lines.product_uom_id
|
||||
# If UoM setting is active, fetch all UoM's data.
|
||||
if self.env.user.has_group('uom.group_uom'):
|
||||
uoms |= self.env['uom.uom'].search([])
|
||||
|
||||
# Fetch `stock.location`
|
||||
source_locations = self.env['stock.location'].search([('id', 'child_of', self.location_id.ids)])
|
||||
destination_locations = self.env['stock.location'].search([('id', 'child_of', self.location_dest_id.ids)])
|
||||
package_locations = self.env['stock.location'].search([('id', 'child_of', self.location_dest_id.ids), ('usage', '!=', 'customer')])
|
||||
locations = move_lines.location_id | move_lines.location_dest_id | source_locations | destination_locations
|
||||
|
||||
# Fetch `stock.quant.package` and `stock.package.type` if group_tracking_lot.
|
||||
packages = self.env['stock.quant.package']
|
||||
package_types = self.env['stock.package.type']
|
||||
if self.env.user.has_group('stock.group_tracking_lot'):
|
||||
packages |= move_lines.package_id | move_lines.result_package_id
|
||||
packages |= self.env['stock.quant.package'].with_context(pack_locs=package_locations.ids)._get_usable_packages()
|
||||
package_types = package_types.search([])
|
||||
|
||||
data = {
|
||||
"records": {
|
||||
"stock.picking": self.read(self._get_fields_stock_barcode(), load=False),
|
||||
"stock.picking.type": self.picking_type_id.read(self.picking_type_id._get_fields_stock_barcode(), load=False),
|
||||
"stock.move.line": move_lines.read(move_lines._get_fields_stock_barcode(), load=False),
|
||||
# `self` can be a record set (e.g.: a picking batch), set only the first partner in the context.
|
||||
"product.product": products.with_context(partner_id=self[:1].partner_id.id).read(products._get_fields_stock_barcode(), load=False),
|
||||
"product.packaging": packagings.read(packagings._get_fields_stock_barcode(), load=False),
|
||||
"res.partner": owners.read(owners._get_fields_stock_barcode(), load=False),
|
||||
"stock.location": locations.read(locations._get_fields_stock_barcode(), load=False),
|
||||
"stock.package.type": package_types.read(package_types._get_fields_stock_barcode(), False),
|
||||
"stock.quant.package": packages.read(packages._get_fields_stock_barcode(), load=False),
|
||||
"stock.lot": lots.read(lots._get_fields_stock_barcode(), load=False),
|
||||
"uom.uom": uoms.read(uoms._get_fields_stock_barcode(), load=False),
|
||||
},
|
||||
"nomenclature_id": [self.env.company.nomenclature_id.id],
|
||||
"source_location_ids": source_locations.ids,
|
||||
"destination_locations_ids": destination_locations.ids,
|
||||
}
|
||||
# Extracts pickings' note if it's empty HTML.
|
||||
for picking in data['records']['stock.picking']:
|
||||
picking['note'] = False if is_html_empty(picking['note']) else html2plaintext(picking['note'])
|
||||
|
||||
data['config'] = self.picking_type_id._get_barcode_config()
|
||||
if self.return_id:
|
||||
data['config']['create_backorder'] = 'never'
|
||||
data['line_view_id'] = self.env.ref('stock_barcode.stock_move_line_product_selector').id
|
||||
data['form_view_id'] = self.env.ref('stock_barcode.stock_picking_barcode').id
|
||||
data['package_view_id'] = self.env.ref('stock_barcode.stock_quant_barcode_kanban').id
|
||||
return data
|
||||
|
||||
@api.model
|
||||
def _create_new_picking(self, picking_type):
|
||||
""" Create a new picking for the given picking type.
|
||||
|
||||
:param picking_type:
|
||||
:type picking_type: :class:`~odoo.addons.stock.models.stock_picking.PickingType`
|
||||
:return: a new picking
|
||||
:rtype: :class:`~odoo.addons.stock.models.stock_picking.Picking`
|
||||
"""
|
||||
# Find source and destination Locations
|
||||
location_dest_id, location_id = picking_type.warehouse_id._get_partner_locations()
|
||||
if picking_type.default_location_src_id:
|
||||
location_id = picking_type.default_location_src_id
|
||||
if picking_type.default_location_dest_id:
|
||||
location_dest_id = picking_type.default_location_dest_id
|
||||
|
||||
# Create and confirm the picking
|
||||
return self.env['stock.picking'].create({
|
||||
'user_id': False,
|
||||
'picking_type_id': picking_type.id,
|
||||
'location_id': location_id.id,
|
||||
'location_dest_id': location_dest_id.id,
|
||||
})
|
||||
|
||||
def _get_client_action(self):
|
||||
self.ensure_one()
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("stock_barcode.stock_barcode_picking_client_action")
|
||||
action = dict(action, target='fullscreen')
|
||||
action['context'] = {'active_id': self.id}
|
||||
return {'action': action}
|
||||
|
||||
def _get_fields_stock_barcode(self):
|
||||
""" List of fields on the stock.picking object that are needed by the
|
||||
client action. The purpose of this function is to be overridden in order
|
||||
to inject new fields to the client action.
|
||||
"""
|
||||
return [
|
||||
'move_ids',
|
||||
'move_line_ids',
|
||||
'picking_type_id',
|
||||
'location_id',
|
||||
'location_dest_id',
|
||||
'name',
|
||||
'state',
|
||||
'picking_type_code',
|
||||
'company_id',
|
||||
'note',
|
||||
'picking_type_entire_packs',
|
||||
'use_create_lots',
|
||||
'use_existing_lots',
|
||||
'user_id',
|
||||
'return_id',
|
||||
]
|
||||
|
||||
@api.model
|
||||
def filter_on_barcode(self, barcode):
|
||||
""" Searches ready pickings for the scanned product/package/lot.
|
||||
"""
|
||||
barcode_type = None
|
||||
nomenclature = self.env.company.nomenclature_id
|
||||
if nomenclature.is_gs1_nomenclature:
|
||||
parsed_results = nomenclature.parse_barcode(barcode)
|
||||
if parsed_results:
|
||||
# filter with the last feasible rule
|
||||
for result in parsed_results[::-1]:
|
||||
if result['rule'].type in ('product', 'package', 'lot'):
|
||||
barcode_type = result['rule'].type
|
||||
break
|
||||
|
||||
active_id = self.env.context.get('active_id')
|
||||
picking_type = self.env['stock.picking.type'].browse(self.env.context.get('active_id'))
|
||||
base_domain = [
|
||||
('picking_type_id', '=', picking_type.id),
|
||||
('state', 'not in', ['cancel', 'done', 'draft'])
|
||||
]
|
||||
|
||||
picking_nums = 0
|
||||
additional_context = {'active_id': active_id}
|
||||
if barcode_type == 'product' or not barcode_type:
|
||||
product = self.env['product.product'].search([('barcode', '=', barcode)], limit=1)
|
||||
if product:
|
||||
picking_nums = self.search_count(base_domain + [('product_id', '=', product.id)])
|
||||
additional_context['search_default_product_id'] = product.id
|
||||
if self.env.user.has_group('stock.group_tracking_lot') and (barcode_type == 'package' or (not barcode_type and not picking_nums)):
|
||||
package = self.env['stock.quant.package'].search([('name', '=', barcode)], limit=1)
|
||||
if package:
|
||||
pack_domain = ['|', ('move_line_ids.package_id', '=', package.id), ('move_line_ids.result_package_id', '=', package.id)]
|
||||
picking_nums = self.search_count(base_domain + pack_domain)
|
||||
additional_context['search_default_move_line_ids'] = barcode
|
||||
if self.env.user.has_group('stock.group_production_lot') and (barcode_type == 'lot' or (not barcode_type and not picking_nums)):
|
||||
lot = self.env['stock.lot'].search([
|
||||
('name', '=', barcode),
|
||||
('company_id', '=', picking_type.company_id.id),
|
||||
], limit=1)
|
||||
if lot:
|
||||
lot_domain = [('move_line_ids.lot_id', '=', lot.id)]
|
||||
picking_nums = self.search_count(base_domain + lot_domain)
|
||||
additional_context['search_default_lot_id'] = lot.id
|
||||
if not barcode_type and not picking_nums: # Nothing found yet, try to find picking by name.
|
||||
picking_nums = self.search_count(base_domain + [('name', '=', barcode)])
|
||||
additional_context['search_default_name'] = barcode
|
||||
|
||||
if not picking_nums:
|
||||
if barcode_type:
|
||||
return {
|
||||
'warning': {
|
||||
'message': _("No %(picking_type)s ready for this %(barcode_type)s", picking_type=picking_type.name, barcode_type=barcode_type),
|
||||
}
|
||||
}
|
||||
return {
|
||||
'warning': {
|
||||
'title': _('No product, lot or package found for barcode %s', barcode),
|
||||
'message': _('Scan a product, a lot/serial number or a package to filter the transfers.'),
|
||||
}
|
||||
}
|
||||
|
||||
action = picking_type._get_action('stock_barcode.stock_picking_action_kanban')
|
||||
action['context'].update(additional_context)
|
||||
return {'action': action}
|
||||
|
||||
|
||||
class StockPickingType(models.Model):
|
||||
_inherit = 'stock.picking.type'
|
||||
|
||||
barcode_validation_after_dest_location = fields.Boolean("Force a destination on all products")
|
||||
barcode_validation_all_product_packed = fields.Boolean("Force all products to be packed")
|
||||
barcode_validation_full = fields.Boolean(
|
||||
"Allow full picking validation", default=True,
|
||||
help="Allow to validate a picking even if nothing was scanned yet (and so, do an immediate transfert)")
|
||||
restrict_scan_product = fields.Boolean(
|
||||
"Force Product scan?", help="Line's product must be scanned before the line can be edited")
|
||||
restrict_put_in_pack = fields.Selection(
|
||||
[
|
||||
('mandatory', "After each product"),
|
||||
('optional', "After group of Products"),
|
||||
('no', "No"),
|
||||
], "Force put in pack?",
|
||||
help="Does the picker have to put in a package the scanned products? If yes, at which rate?",
|
||||
default="optional", required=True)
|
||||
restrict_scan_tracking_number = fields.Selection(
|
||||
[
|
||||
('mandatory', "Mandatory Scan"),
|
||||
('optional', "Optional Scan"),
|
||||
], "Force Lot/Serial scan?", default='optional', required=True)
|
||||
restrict_scan_source_location = fields.Selection(
|
||||
[
|
||||
('no', "No Scan"),
|
||||
('mandatory', "Mandatory Scan"),
|
||||
], "Force Source Location scan?", default='no', required=True)
|
||||
restrict_scan_dest_location = fields.Selection(
|
||||
[
|
||||
('mandatory', "After each product"),
|
||||
('optional', "After group of Products"),
|
||||
('no', "No"),
|
||||
], "Force Destination Location scan?",
|
||||
help="Does the picker have to scan the destination? If yes, at which rate?",
|
||||
default='optional', required=True)
|
||||
show_barcode_validation = fields.Boolean(
|
||||
compute='_compute_show_barcode_validation',
|
||||
help='Technical field used to compute whether the "Final Validation" group should be displayed, solving combined groups/invisible complexity.')
|
||||
is_barcode_picking_type = fields.Boolean(
|
||||
compute='_compute_is_barcode_picking_type',
|
||||
help="Technical field indicating if should be used in barcode app and used to control visibility in the related UI.")
|
||||
|
||||
|
||||
@api.depends('restrict_scan_product', 'restrict_put_in_pack', 'restrict_scan_dest_location')
|
||||
def _compute_show_barcode_validation(self):
|
||||
for picking_type in self:
|
||||
# reflect all fields invisible conditions
|
||||
hide_full = picking_type.restrict_scan_product
|
||||
hide_all_product_packed = not self.user_has_groups('stock.group_tracking_lot') or\
|
||||
picking_type.restrict_put_in_pack != 'optional'
|
||||
hide_dest_location = not self.user_has_groups('stock.group_stock_multi_locations') or\
|
||||
(picking_type.code == 'outgoing' or picking_type.restrict_scan_dest_location != 'optional')
|
||||
# show if not all hidden
|
||||
picking_type.show_barcode_validation = not (hide_full and hide_all_product_packed and hide_dest_location)
|
||||
|
||||
@api.depends('code')
|
||||
def _compute_is_barcode_picking_type(self):
|
||||
for picking_type in self:
|
||||
if picking_type.code in ['incoming', 'outgoing', 'internal']:
|
||||
picking_type.is_barcode_picking_type = True
|
||||
else:
|
||||
picking_type.is_barcode_picking_type = False
|
||||
|
||||
@api.constrains('restrict_scan_source_location', 'restrict_scan_dest_location')
|
||||
def _check_restrinct_scan_locations(self):
|
||||
for picking_type in self:
|
||||
if picking_type.code == 'internal' and\
|
||||
picking_type.restrict_scan_dest_location == 'optional' and\
|
||||
picking_type.restrict_scan_source_location == 'mandatory':
|
||||
raise UserError(_("If the source location must be scanned, then the destination location must either be scanned after each product or not scanned at all."))
|
||||
|
||||
def get_action_picking_tree_ready_kanban(self):
|
||||
return self._get_action('stock_barcode.stock_picking_action_kanban')
|
||||
|
||||
def _get_barcode_config(self):
|
||||
self.ensure_one()
|
||||
# Defines if all lines need to be packed to be able to validate a transfer.
|
||||
locations_enable = self.env.user.has_group('stock.group_stock_multi_locations')
|
||||
lines_need_to_be_packed = self.env.user.has_group('stock.group_tracking_lot') and (
|
||||
self.restrict_put_in_pack == 'mandatory' or (
|
||||
self.restrict_put_in_pack == 'optional'
|
||||
and self.barcode_validation_all_product_packed
|
||||
)
|
||||
)
|
||||
config = {
|
||||
# Boolean fields.
|
||||
'barcode_validation_after_dest_location': self.barcode_validation_after_dest_location,
|
||||
'barcode_validation_all_product_packed': self.barcode_validation_all_product_packed,
|
||||
'barcode_validation_full': not self.restrict_scan_product and self.barcode_validation_full, # Forced to be False when scanning a product is mandatory.
|
||||
'create_backorder': self.create_backorder,
|
||||
'restrict_scan_product': self.restrict_scan_product,
|
||||
# Selection fields converted into boolean.
|
||||
'restrict_scan_tracking_number': self.restrict_scan_tracking_number == 'mandatory',
|
||||
'restrict_scan_source_location': locations_enable and self.restrict_scan_source_location == 'mandatory',
|
||||
# Selection fields.
|
||||
'restrict_put_in_pack': self.restrict_put_in_pack,
|
||||
'restrict_scan_dest_location': self.restrict_scan_dest_location if locations_enable else 'no',
|
||||
# Additional parameters.
|
||||
'lines_need_to_be_packed': lines_need_to_be_packed,
|
||||
}
|
||||
return config
|
||||
|
||||
def _get_fields_stock_barcode(self):
|
||||
return [
|
||||
'default_location_dest_id',
|
||||
'default_location_src_id',
|
||||
]
|
||||
@@ -0,0 +1,143 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class StockQuant(models.Model):
|
||||
_inherit = 'stock.quant'
|
||||
|
||||
dummy_id = fields.Char(compute='_compute_dummy_id', inverse='_inverse_dummy_id')
|
||||
image_1920 = fields.Image(related="product_id.image_1920")
|
||||
product_reference_code = fields.Char(related="product_id.code", string="Product Reference Code")
|
||||
|
||||
def _compute_dummy_id(self):
|
||||
self.dummy_id = ''
|
||||
|
||||
def _inverse_dummy_id(self):
|
||||
pass
|
||||
|
||||
@api.model
|
||||
def barcode_write(self, vals):
|
||||
""" Specially made to handle barcode app saving. Avoids overriding write method because pickings in barcode
|
||||
will also write to quants and handling context in this case is non-trivial. This method is expected to be
|
||||
called only when no record and vals is a list of lists of the form: [[1, quant_id, {write_values}],
|
||||
[0, 0, {write_values}], ...]} where [1, quant_id...] updates an existing quant or {[0, 0, ...]}
|
||||
when creating a new quant."""
|
||||
Quant = self.env['stock.quant'].with_context(inventory_mode=True)
|
||||
|
||||
# TODO batch
|
||||
|
||||
for val in vals:
|
||||
if val[0] in (0, 1) and not val[2].get('lot_id') and val[2].get('lot_name'):
|
||||
quant_db = val[0] == 1 and Quant.browse(val[1]) or False
|
||||
val[2]['lot_id'] = self.env['stock.lot'].create({
|
||||
'name': val[2].pop('lot_name'),
|
||||
'product_id': val[2].get('product_id', quant_db and quant_db.product_id.id or False),
|
||||
'company_id': self.env['stock.location'].browse(val[2].get('location_id') or quant_db.location_id.id).company_id.id
|
||||
}).id
|
||||
|
||||
quant_ids = []
|
||||
for val in vals:
|
||||
if val[0] == 1:
|
||||
quant_id = val[1]
|
||||
Quant.browse(quant_id).write(val[2])
|
||||
quant_ids.append(quant_id)
|
||||
elif val[0] == 0:
|
||||
quant = Quant.create(val[2])
|
||||
# in case an existing quant is written on instead (happens when scanning a product
|
||||
# with quants, but not assigned to user or doesn't have an inventory date to normally show up in view)
|
||||
if val[2].get('dummy_id'):
|
||||
quant.write({'dummy_id': val[2].get('dummy_id')})
|
||||
quant.write({'inventory_date': val[2].get('inventory_date')})
|
||||
user_id = val[2].get('user_id')
|
||||
# assign a user if one isn't assigned to avoid line disappearing when page left and returned to
|
||||
if not quant.user_id and user_id:
|
||||
quant.write({'user_id': user_id})
|
||||
quant_ids.append(quant.id)
|
||||
return self.browse(quant_ids)._get_stock_barcode_data()
|
||||
|
||||
def action_validate(self):
|
||||
quants = self.with_context(inventory_mode=True).filtered(lambda q: q.inventory_quantity_set)
|
||||
quants._compute_inventory_diff_quantity()
|
||||
res = quants.action_apply_inventory()
|
||||
if res:
|
||||
return res
|
||||
return True
|
||||
|
||||
def action_client_action(self):
|
||||
""" Open the mobile view specialized in handling barcodes on mobile devices.
|
||||
"""
|
||||
action = self.env['ir.actions.actions']._for_xml_id('stock_barcode.stock_barcode_inventory_client_action')
|
||||
return dict(action, target='fullscreen')
|
||||
|
||||
@api.model
|
||||
def get_existing_quant_and_related_data(self, domain):
|
||||
quants = self.search(domain)
|
||||
return quants.get_stock_barcode_data_records()
|
||||
|
||||
def _get_stock_barcode_data(self):
|
||||
locations = self.env['stock.location']
|
||||
company_id = self.env.company.id
|
||||
package_types = self.env['stock.package.type']
|
||||
if not self: # `self` is an empty recordset when we open the inventory adjustment.
|
||||
if self.env.user.has_group('stock.group_stock_multi_locations'):
|
||||
locations = self.env['stock.location'].search([('usage', 'in', ['internal', 'transit']), ('company_id', '=', company_id)], order='id')
|
||||
else:
|
||||
locations = self.env['stock.warehouse'].search([('company_id', '=', company_id)], limit=1).lot_stock_id
|
||||
self = self.env['stock.quant'].search([('user_id', '=?', self.env.user.id), ('location_id', 'in', locations.ids), ('inventory_date', '<=', fields.Date.today())])
|
||||
if self.env.user.has_group('stock.group_tracking_lot'):
|
||||
package_types = package_types.search([])
|
||||
|
||||
data = self.with_context(display_default_code=False, barcode_view=True).get_stock_barcode_data_records()
|
||||
if locations:
|
||||
data["records"]["stock.location"] = locations.read(locations._get_fields_stock_barcode(), load=False)
|
||||
if package_types:
|
||||
data["records"]["stock.package.type"] = package_types.read(package_types._get_fields_stock_barcode(), load=False)
|
||||
data['line_view_id'] = self.env.ref('stock_barcode.stock_quant_barcode').id
|
||||
return data
|
||||
|
||||
def get_stock_barcode_data_records(self):
|
||||
products = self.product_id
|
||||
companies = self.company_id or self.env.company
|
||||
lots = self.lot_id
|
||||
owners = self.owner_id
|
||||
packages = self.package_id
|
||||
uoms = products.uom_id
|
||||
# If UoM setting is active, fetch all UoM's data.
|
||||
if self.env.user.has_group('uom.group_uom'):
|
||||
uoms = self.env['uom.uom'].search([])
|
||||
|
||||
data = {
|
||||
"records": {
|
||||
"stock.quant": self.read(self._get_fields_stock_barcode(), load=False),
|
||||
"product.product": products.read(products._get_fields_stock_barcode(), load=False),
|
||||
"stock.quant.package": packages.read(packages._get_fields_stock_barcode(), load=False),
|
||||
"res.company": companies.read(['name']),
|
||||
"res.partner": owners.read(owners._get_fields_stock_barcode(), load=False),
|
||||
"stock.lot": lots.read(lots._get_fields_stock_barcode(), load=False),
|
||||
"uom.uom": uoms.read(uoms._get_fields_stock_barcode(), load=False),
|
||||
},
|
||||
"nomenclature_id": [self.env.company.nomenclature_id.id],
|
||||
"user_id": self.env.user.id,
|
||||
}
|
||||
return data
|
||||
|
||||
def _get_fields_stock_barcode(self):
|
||||
return [
|
||||
'product_id',
|
||||
'location_id',
|
||||
'inventory_date',
|
||||
'inventory_quantity',
|
||||
'inventory_quantity_set',
|
||||
'quantity',
|
||||
'product_uom_id',
|
||||
'lot_id',
|
||||
'package_id',
|
||||
'owner_id',
|
||||
'inventory_diff_quantity',
|
||||
'dummy_id',
|
||||
'user_id',
|
||||
]
|
||||
|
||||
def _get_inventory_fields_write(self):
|
||||
return ['dummy_id'] + super()._get_inventory_fields_write()
|
||||
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, api
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
class QuantPackage(models.Model):
|
||||
_inherit = 'stock.quant.package'
|
||||
_barcode_field = 'name'
|
||||
|
||||
@api.model
|
||||
def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None):
|
||||
domain = self.env.company.nomenclature_id._preprocess_gs1_search_args(domain, ['package'], 'name')
|
||||
return super()._search(domain, offset=offset, limit=limit, order=order, access_rights_uid=access_rights_uid)
|
||||
|
||||
@api.model
|
||||
def action_create_from_barcode(self, vals_list):
|
||||
""" Creates a new package then returns its data to be added in the client side cache.
|
||||
"""
|
||||
res = self.create(vals_list)
|
||||
return {
|
||||
'stock.quant.package': res.read(self._get_fields_stock_barcode(), False)
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_fields_stock_barcode(self):
|
||||
return ['name', 'location_id', 'package_type_id', 'quant_ids']
|
||||
|
||||
@api.model
|
||||
def _get_usable_packages(self):
|
||||
usable_packages_domain = [
|
||||
'|',
|
||||
('package_use', '=', 'reusable'),
|
||||
('location_id', '=', False),
|
||||
]
|
||||
loc_ids = self._context.get('pack_locs')
|
||||
if loc_ids:
|
||||
usable_packages_domain = expression.OR([usable_packages_domain, [('location_id', 'in', loc_ids)]])
|
||||
return self.env['stock.quant.package'].search(usable_packages_domain)
|
||||
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class StockScrap(models.Model):
|
||||
_name = 'stock.scrap'
|
||||
_inherit = ['stock.scrap', 'barcodes.barcode_events_mixin']
|
||||
|
||||
product_barcode = fields.Char(related='product_id.barcode', string='Barcode', readonly=False)
|
||||
|
||||
def on_barcode_scanned(self, barcode):
|
||||
self.ensure_one()
|
||||
product = self.env['product.product'].search([('barcode', '=', barcode)])
|
||||
if product and self.product_id == product:
|
||||
self.scrap_qty += 1
|
||||
elif product:
|
||||
self.scrap_qty = 1
|
||||
self.product_id = product
|
||||
self.lot_id = False
|
||||
else:
|
||||
lot = self.env['stock.lot'].search([('name', '=', barcode)])
|
||||
if lot and self.lot_id == lot:
|
||||
self.scrap_qty += 1
|
||||
elif lot:
|
||||
self.scrap_qty = 1
|
||||
self.lot_id = lot.id
|
||||
self.product_id = lot.product_id
|
||||
@@ -0,0 +1,24 @@
|
||||
from odoo import models
|
||||
|
||||
|
||||
class StockWarehouse(models.Model):
|
||||
_inherit = 'stock.warehouse'
|
||||
|
||||
def _get_picking_type_create_values(self, max_sequence):
|
||||
values = super()._get_picking_type_create_values(max_sequence)
|
||||
values[0]['pick_type_id']['restrict_scan_source_location'] = 'mandatory'
|
||||
values[0]['pick_type_id']['restrict_scan_dest_location'] = 'no'
|
||||
return values
|
||||
|
||||
def _get_picking_type_update_values(self):
|
||||
values = super()._get_picking_type_update_values()
|
||||
# When multi-steps delivery is enabled, the source scan setting for the pick is equal to the
|
||||
# delivery type's one, and the scan source for the delivery is disabled (by default).
|
||||
if values['pick_type_id'].get('active'):
|
||||
if self.out_type_id.restrict_scan_source_location == 'mandatory' and self.pick_type_id.restrict_scan_dest_location == 'optional':
|
||||
values['out_type_id']['restrict_scan_source_location'] = 'no'
|
||||
values['pick_type_id']['restrict_scan_source_location'] = self.out_type_id.restrict_scan_source_location
|
||||
values['pick_type_id']['restrict_scan_dest_location'] = 'no'
|
||||
elif not values['pick_type_id'].get('active') and self.pick_type_id.active:
|
||||
values['out_type_id']['restrict_scan_source_location'] = self.pick_type_id.restrict_scan_source_location
|
||||
return values
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, api
|
||||
|
||||
|
||||
class UoM(models.Model):
|
||||
_inherit = 'uom.uom'
|
||||
|
||||
@api.model
|
||||
def _get_fields_stock_barcode(self):
|
||||
return [
|
||||
'name',
|
||||
'category_id',
|
||||
'factor',
|
||||
'rounding',
|
||||
]
|
||||
@@ -0,0 +1,2 @@
|
||||
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
|
||||
"access_stock_barcode_cancel_operation","access.stock_barcode.cancel.operation","model_stock_barcode_cancel_operation","stock.group_stock_user",1,1,1,0
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 535 B |
@@ -0,0 +1 @@
|
||||
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path d="M22 8h8v34h-8V8ZM4 8h9v34H8a4 4 0 0 1-4-4V8Zm35 0h3a4 4 0 0 1 4 4v30h-7V8Zm-6 0h3v34h-3V8ZM16 8h3v34h-3V8Z" fill="#985184"/></svg>
|
||||
|
After Width: | Height: | Size: 223 B |
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="400" height="300" preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<metadata id="metadata27">image/svg+xml</metadata>
|
||||
<g class="layer">
|
||||
<title>Barcode Icon</title>
|
||||
<g id="g20">
|
||||
<g id="g12" fill="#8f8f8f">
|
||||
<rect height="186" id="rect2" width="44" x="61" y="57"/>
|
||||
<rect height="186" id="rect4" width="44" x="236.5" y="57"/>
|
||||
<rect height="186" id="rect6" width="44" x="295" y="57"/>
|
||||
<rect height="186" id="rect8" width="24" x="129.5" y="57"/>
|
||||
<rect height="186" id="rect10" width="14" x="193" y="57"/>
|
||||
</g>
|
||||
<g id="g18" fill="#8f8f8f">
|
||||
<polygon id="polygon14" points="59.5,269.5 25.5,269.5 25.5,30.5 59.5,30.5 59.5,19.5 14.5,19.5 14.5,280.5 59.5,280.5"/>
|
||||
<polygon id="polygon16" points="339.4999694824219,19.5 339.4999694824219,30.5 374.4999694824219,30.5 374.4999694824219,269.5 339.4999694824219,269.5 339.4999694824219,280.5 385.4999694824219,280.5 385.4999694824219,19.5"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,117 @@
|
||||
# Note to run this script (specifically for datamatrix generation) you will need the following installed:
|
||||
# - reportlab version 3.4 or higher (available on pip3)
|
||||
|
||||
|
||||
from io import BytesIO
|
||||
from PyPDF2 import PdfFileReader, PdfFileMerger
|
||||
|
||||
from reportlab.graphics import renderPDF
|
||||
from reportlab.graphics.barcode import createBarcodeDrawing
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.pagesizes import A4
|
||||
|
||||
|
||||
OTHER_DEMO_FILENAME = "barcodes_demo.pdf"
|
||||
FONT = "Helvetica"
|
||||
HEADER_FONT_SIZE = 16
|
||||
LABEL_FONT_SIZE = 14
|
||||
TITLE_FONT_SIZE = 11
|
||||
CODE_FONT_SIZE = 8
|
||||
FOOTER_FONT_SIZE = 8
|
||||
PAGE_SIZE = A4
|
||||
|
||||
|
||||
def create_page(barcodes, font_size_and_texts):
|
||||
kwargs = {
|
||||
'width': 75,
|
||||
'height': 75,
|
||||
}
|
||||
packet = BytesIO()
|
||||
can = canvas.Canvas(packet, pagesize=PAGE_SIZE)
|
||||
for barcode in barcodes:
|
||||
d = createBarcodeDrawing('ECC200DataMatrix', value=barcode[2], **kwargs)
|
||||
renderPDF.draw(d, can, barcode[0], barcode[1])
|
||||
|
||||
# add text
|
||||
for font_size, texts in font_size_and_texts:
|
||||
can.setFont(FONT, font_size)
|
||||
for text in texts:
|
||||
can.drawString(text[0], text[1], text[2])
|
||||
can.save()
|
||||
packet.seek(0)
|
||||
return PdfFileReader(packet)
|
||||
|
||||
|
||||
# same on all GS1 pages
|
||||
footer = [(45, 45, "Don't have any barcode scanner? Right click on your screen > Inspect > Console and type the following command:"),
|
||||
(45, 35, ' odoo.__WOWL_DEBUG__.root.env.services.barcode.bus.trigger("barcode_scanned", {barcode:"setyourbarcodehere"})'),
|
||||
(45, 25, 'and replace "setyourbarcodehere" by the barcode you would like to scan OR use our mobile app.'),
|
||||
(45, 15, 'For GS1 barcodes, remove all "("s and ")"s')]
|
||||
|
||||
# page 1
|
||||
# format is (x, y, "barcode")
|
||||
# barcodes to generate
|
||||
barcodes = [(75, 640, "WH-RECEIPTS"), (260, 640, "0106016478556677300000000510Lot0001"), (445, 640, "0106016478556677300000000510Lot0002"),
|
||||
(75, 505, "0106016478556677300000000510Lot0003"), (260, 505, "O-BTN.validate"),
|
||||
(75, 350, "WH-RECEIPTS"), (260, 350, "01060164785599993650000010"), (445, 350, "01060164785599823160000002"),
|
||||
(75, 220, "O-BTN.validate")]
|
||||
|
||||
header = [(45, 785, "GS1 Barcodes (set Barcode Nomenclature to Default GS1 Nomenclature)")]
|
||||
# text to describe barcode flow
|
||||
flow_labels = [(45, 752, "Receive products tracked by lot (activate Lots & Serial Numbers) "),
|
||||
(45, 460, "Receive products with different unit of measures (activate Units of Measure)")]
|
||||
|
||||
# text to describe (above) each barcode
|
||||
barcode_titles = [(55, 733, "YourCompany Receipts"), (215, 733, "Cable Management Box x5 Lot0001"), (400, 733, "Cable Management Box x5 Lot0002"),
|
||||
(45, 600, "Cable Management Box x5 Lot0003"), (275, 600, "Validate"),
|
||||
(55, 444, "YourCompany Receipts"), (230, 444, "Customized Cabinet (USA) 10 ft³"), (415, 444, "Customized Cabinet (Metric) 2 m³"),
|
||||
(93, 315, "Validate")]
|
||||
|
||||
# text of barcode code (below), but made to look pretty (not literal barcode)
|
||||
barcode_code = [(85, 631, "WH-RECEIPTS"), (220, 631, "(01)06016478556677(30)00000005(10)Lot0001"), (405, 631, "(01)06016478556677(30)00000005(10)Lot0002"),
|
||||
(45, 495, "(01)06016478556677(30)00000005(10)Lot0003"), (270, 495, "O-BTN.validate"),
|
||||
(85, 340, "WH-RECEIPTS"), (238, 340, "(01)06016478559999(3650)000010"), (425, 340, "(01)06016478559982(3160)000002"),
|
||||
(85, 212, "O-BTN.validate")]
|
||||
|
||||
|
||||
font_size_and_texts = [(HEADER_FONT_SIZE, header),
|
||||
(LABEL_FONT_SIZE, flow_labels),
|
||||
(TITLE_FONT_SIZE, barcode_titles),
|
||||
(CODE_FONT_SIZE, barcode_code),
|
||||
(FOOTER_FONT_SIZE, footer)]
|
||||
|
||||
page1 = create_page(barcodes, font_size_and_texts)
|
||||
|
||||
# page 2
|
||||
# format is (x, y, "barcode")
|
||||
# barcodes to generate
|
||||
barcodes = [(75, 640, "WH-RECEIPTS"), (260, 640, "0106016478556332305"), (445, 640, "0006016471234567890591PAL"),
|
||||
(75, 505, "O-BTN.validate")]
|
||||
|
||||
header = []
|
||||
# text to describe barcode flow
|
||||
flow_labels = [(45, 752, "Put in Pack (activate Packages)")]
|
||||
|
||||
# text to describe (above) each barcode
|
||||
barcode_titles = [(55, 733, "YourCompany Receipts"), (233, 733, "Individual Workplace x10"), (410, 733, "Put in Pack: Pallet with SSCC"),
|
||||
(93, 600, "Validate")]
|
||||
|
||||
# text of barcode code (below), but made to look pretty (not literal barcode)
|
||||
barcode_code = [(85, 631, "WH-RECEIPTS"), (245, 631, "(01)06016478556332(30)10"), (425, 631, "(00)060164712345678905(91)PAL"),
|
||||
(85, 495, "O-BTN.validate")]
|
||||
|
||||
font_size_and_texts = [(HEADER_FONT_SIZE, header),
|
||||
(LABEL_FONT_SIZE, flow_labels),
|
||||
(TITLE_FONT_SIZE, barcode_titles),
|
||||
(CODE_FONT_SIZE, barcode_code),
|
||||
(FOOTER_FONT_SIZE, footer)]
|
||||
|
||||
page2 = create_page(barcodes, font_size_and_texts)
|
||||
|
||||
|
||||
# merge with other demo barcodes
|
||||
merger = PdfFileMerger()
|
||||
merger.append(PdfFileReader(open(OTHER_DEMO_FILENAME, "rb")))
|
||||
merger.append(page1)
|
||||
merger.append(page2)
|
||||
merger.write(OTHER_DEMO_FILENAME)
|
||||
@@ -0,0 +1,194 @@
|
||||
#!/bin/sh
|
||||
|
||||
barcode -t 2x7+40+40 -m 50x30 -p "210x297mm" -e code128b -n > barcodes_actions_barcode.ps << BARCODES
|
||||
O-CMD.MAIN-MENU
|
||||
O-CMD.DISCARD
|
||||
O-BTN.validate
|
||||
O-CMD.cancel
|
||||
O-BTN.print-op
|
||||
O-BTN.print-slip
|
||||
O-BTN.pack
|
||||
O-BTN.scrap
|
||||
O-BTN.record-components
|
||||
O-CMD.PREV
|
||||
O-CMD.NEXT
|
||||
O-CMD.PAGER-FIRST
|
||||
O-CMD.PAGER-LAST
|
||||
O-BTN.return
|
||||
BARCODES
|
||||
|
||||
cat > barcodes_actions_header.ps << HEADER
|
||||
/showTitle { /Helvetica findfont 12 scalefont setfont moveto show } def
|
||||
(MAIN MENU) 89 768 showTitle
|
||||
(DISCARD) 348 768 showTitle
|
||||
(VALIDATE) 89 660 showTitle
|
||||
(CANCEL) 348 660 showTitle
|
||||
(PRINT PICKING OPERATION) 89 551 showTitle
|
||||
(PRINT DELIVERY SLIP) 348 551 showTitle
|
||||
(PUT IN PACK) 89 444 showTitle
|
||||
(SCRAP) 348 444 showTitle
|
||||
(RECORD COMPONENTS) 89 337 showTitle
|
||||
(PREVIOUS PAGE) 348 337 showTitle
|
||||
(NEXT PAGE) 89 230 showTitle
|
||||
(FIRST PAGE) 348 230 showTitle
|
||||
(LAST PAGE) 89 123 showTitle
|
||||
(RETURN) 348 123 showTitle
|
||||
|
||||
HEADER
|
||||
|
||||
cat barcodes_actions_header.ps barcodes_actions_barcode.ps | ps2pdf - - > barcodes_actions.pdf
|
||||
rm barcodes_actions_header.ps barcodes_actions_barcode.ps
|
||||
|
||||
# pg 1 of demo barcodes due to ps headers being restricted to 1 page. Some blanks may exist due to flows having a rows with less than 3 barcodes.
|
||||
barcode -t 3x7+20+35 -m 25x30 -p "210x297mm" -e code128b -n > barcodes_demo_barcode_pg_1.ps << BARCODES
|
||||
WH-RECEIPTS
|
||||
601647855638
|
||||
O-BTN.validate
|
||||
WH/OUT/00005
|
||||
601647855644
|
||||
O-BTN.validate
|
||||
WH-RECEIPTS
|
||||
601647855640
|
||||
601647855631
|
||||
LOT-000002
|
||||
LOT-000003
|
||||
O-BTN.validate
|
||||
WH-STOCK
|
||||
601647855649
|
||||
2601892
|
||||
O-BTN.validate
|
||||
|
||||
|
||||
WH-RECEIPTS
|
||||
601647855650
|
||||
O-BTN.pack
|
||||
BARCODES
|
||||
|
||||
# blank lines included for easier visual matching to barcode spacing
|
||||
cat > barcodes_demo_header_pg_1.ps << HEADER
|
||||
/showLabel { /Helvetica findfont 14 scalefont setfont moveto show } def
|
||||
/showTitle { /Helvetica findfont 11 scalefont setfont moveto show } def
|
||||
/showCode { /Helvetica findfont 8 scalefont setfont moveto show } def
|
||||
/showFooter { /Helvetica findfont 8 scalefont setfont moveto show } def
|
||||
(Receive products in stock) 45 797 showLabel
|
||||
(YourCompany Receipts) 45 777 showTitle
|
||||
(WH-RECEIPTS) 85 718 showCode
|
||||
(Desk Stand with Screen) 230 777 showTitle
|
||||
(601647855638) 271 718 showCode
|
||||
(Validate) 415 777 showTitle
|
||||
(O-BTN.validate) 456 718 showCode
|
||||
(Deliver products to your customers) 45 687 showLabel
|
||||
(WH/OUT/00005) 45 667 showTitle
|
||||
(WH/OUT/00005) 85 608 showCode
|
||||
(Desk Combination) 230 667 showTitle
|
||||
(601647855644) 271 608 showCode
|
||||
(Validate) 415 667 showTitle
|
||||
(O-BTN.validate) 456 608 showCode
|
||||
(Receive products tracked by lot number (activate Lots & Serial Numbers)) 45 577 showLabel
|
||||
(YourCompany Receipts) 45 557 showTitle
|
||||
(WH-RECEIPTS) 85 498 showCode
|
||||
(Corner Desk Black) 230 557 showTitle
|
||||
(601647855640) 271 498 showCode
|
||||
(Cable Management Box) 415 557 showTitle
|
||||
(601647855631) 456 498 showCode
|
||||
(LOT-000002) 45 447 showTitle
|
||||
(LOT-000002) 85 388 showCode
|
||||
(LOT-000003) 230 447 showTitle
|
||||
(LOT-000003) 271 388 showCode
|
||||
(Validate) 415 447 showTitle
|
||||
(O-BTN.validate) 456 388 showCode
|
||||
(Internal transfer (activate Storage Locations)) 45 357 showLabel
|
||||
(WH/Stock) 45 337 showTitle
|
||||
(WH-STOCK) 85 278 showCode
|
||||
(Pedal Bin) 230 337 showTitle
|
||||
(601647855649) 271 278 showCode
|
||||
(WH/Stock/Shelf1) 415 337 showTitle
|
||||
(2601892) 456 278 showCode
|
||||
(Validate) 45 227 showTitle
|
||||
(O-BTN.validate) 85 168 showCode
|
||||
|
||||
|
||||
(Put in Pack (activate Packages)) 45 137 showLabel
|
||||
(YourCompany Receipts) 45 117 showTitle
|
||||
(WH-RECEIPTS) 85 58 showCode
|
||||
(Large Cabinet) 230 117 showTitle
|
||||
(601647855650) 271 58 showCode
|
||||
(Put in Pack) 415 117 showTitle
|
||||
(O-BTN.pack) 456 58 showCode
|
||||
|
||||
(Don't have any barcode scanner? Right click on your screen > Inspect > Console and type the following command:) 45 35 showFooter
|
||||
( odoo.__WOWL_DEBUG__.root.env.services.barcode.bus.trigger("barcode_scanned", {barcode:"setyourbarcodehere"})) 45 25 showFooter
|
||||
(and replace "setyourbarcodehere" by the barcode you would like to scan OR use our mobile app.) 45 15 showFooter
|
||||
HEADER
|
||||
|
||||
# pg 2 of demo barcodes. Some blanks may exist due to flows having a rows with less than 3 barcodes.
|
||||
barcode -t 3x7+20+35 -m 25x30 -p "210x297mm" -e code128b -n > barcodes_demo_barcode_pg_2.ps << BARCODES
|
||||
O-BTN.validate
|
||||
|
||||
|
||||
BATCH/00002
|
||||
601647855637
|
||||
601647855651
|
||||
601647855635
|
||||
O-BTN.validate
|
||||
|
||||
BATCH/00001
|
||||
601647855652
|
||||
CLUSTER-PACK-1
|
||||
601647855653
|
||||
CLUSTER-PACK-1
|
||||
601647855651
|
||||
CLUSTER-PACK-2
|
||||
O-BTN.validate
|
||||
|
||||
BARCODES
|
||||
|
||||
cat > barcodes_demo_header_pg_2.ps << HEADER
|
||||
/showLabel { /Helvetica findfont 14 scalefont setfont moveto show } def
|
||||
/showTitle { /Helvetica findfont 11 scalefont setfont moveto show } def
|
||||
/showCode { /Helvetica findfont 8 scalefont setfont moveto show } def
|
||||
/showFooter { /Helvetica findfont 8 scalefont setfont moveto show } def
|
||||
(Validate) 45 777 showTitle
|
||||
(O-BTN.validate) 85 718 showCode
|
||||
|
||||
|
||||
(Batch picking (activate Batch Pickings)) 45 687 showLabel
|
||||
(BATCH/00002) 45 667 showTitle
|
||||
(BATCH/00002) 85 608 showCode
|
||||
(Large Meeting Table) 230 667 showTitle
|
||||
(601647855637) 271 608 showCode
|
||||
(Four Person Desk) 415 667 showTitle
|
||||
(601647855651) 456 608 showCode
|
||||
(Three-Seat Sofa) 45 557 showTitle
|
||||
(601647855635) 85 498 showCode
|
||||
(Validate) 230 557 showTitle
|
||||
(O-BTN.validate) 271 498 showCode
|
||||
|
||||
(Batch picking with cluster pickings (activate Batch Pickings and Packages)) 45 467 showLabel
|
||||
(BATCH/00001) 45 447 showTitle
|
||||
(BATCH/00001) 85 388 showCode
|
||||
(Cabinet with Doors) 230 447 showTitle
|
||||
(601647855652) 271 388 showCode
|
||||
(CLUSTER-PACK-1) 415 447 showTitle
|
||||
(CLUSTER-PACK-1) 456 388 showCode
|
||||
(Acoustic Bloc Screens) 45 337 showTitle
|
||||
(601647855653) 85 278 showCode
|
||||
(CLUSTER-PACK-1) 230 337 showTitle
|
||||
(CLUSTER-PACK-1) 271 278 showCode
|
||||
(Four Person Desk) 415 337 showTitle
|
||||
(601647855651) 456 278 showCode
|
||||
(CLUSTER-PACK-2) 45 227 showTitle
|
||||
(CLUSTER-PACK-2) 85 168 showCode
|
||||
(Validate) 230 227 showTitle
|
||||
(O-BTN.validate) 271 168 showCode
|
||||
|
||||
(Don't have any barcode scanner? Right click on your screen > Inspect > Console and type the following command:) 45 35 showFooter
|
||||
( odoo.__WOWL_DEBUG__.root.env.services.barcode.bus.trigger("barcode_scanned", {barcode:"setyourbarcodehere"})) 45 25 showFooter
|
||||
(and replace "setyourbarcodehere" by the barcode you would like to scan OR use our mobile app.) 45 15 showFooter
|
||||
HEADER
|
||||
|
||||
cat barcodes_demo_header_pg_1.ps barcodes_demo_barcode_pg_1.ps barcodes_demo_header_pg_2.ps barcodes_demo_barcode_pg_2.ps | ps2pdf - - > barcodes_demo.pdf
|
||||
rm barcodes_demo_header_pg_1.ps barcodes_demo_barcode_pg_1.ps
|
||||
rm barcodes_demo_header_pg_2.ps barcodes_demo_barcode_pg_2.ps
|
||||
|
||||
python3 make_barcodes.py
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,26 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
// import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class BackorderDialog extends Component {
|
||||
setup() {
|
||||
this.title = _t("Incomplete Transfer");
|
||||
}
|
||||
|
||||
async _onApply() {
|
||||
await this.props.onApply();
|
||||
this.props.close();
|
||||
}
|
||||
}
|
||||
|
||||
BackorderDialog.components = { Dialog };
|
||||
BackorderDialog.props = {
|
||||
displayUoM: Boolean,
|
||||
uncompletedLines: Array,
|
||||
onApply: Function,
|
||||
close: Function,
|
||||
};
|
||||
BackorderDialog.template = "stock_barcode.BackorderDialog";
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="stock_barcode.BackorderDialog">
|
||||
<Dialog title="title" size="'md'" fullscreen="false" contentClass="'o_barcode_backorder_dialog'">
|
||||
<div class="container">
|
||||
<div class="text-muted mb-4">
|
||||
If you validate now, the remaining products will be added to a backorder.
|
||||
</div>
|
||||
<div class="o_barcode_backorder_column_titles row mb-2 pb-3 border-bottom text-capitalize">
|
||||
<div class="col-4">Product</div>
|
||||
<div class="col-2 text-end pe-0">Done /</div>
|
||||
<div class="col-2 ps-1">To do</div>
|
||||
<div class="col-2 text-end">Backorder</div>
|
||||
</div>
|
||||
<t t-foreach="props.uncompletedLines" t-as="line" t-key="line.id">
|
||||
<t t-set="colorClass" t-value="line.qty_done ? 'text-warning' : 'text-danger'"/>
|
||||
<div class="o_barcode_backorder_product_row row mb-1">
|
||||
<div class="col-4" name="product" t-esc="line.product_id.display_name"/>
|
||||
<div class="col-2 text-end pe-0" t-att-class="colorClass">
|
||||
<span name="qty-done" t-esc="line.qty_done"/> /
|
||||
</div>
|
||||
<div class="col-2 ps-1" t-att-class="colorClass">
|
||||
<span name="reserved-qty" t-esc="line.reserved_uom_qty"/>
|
||||
<span t-if="props.displayUoM" name="uom" class="ms-1" t-esc="line.product_uom_id.name"/>
|
||||
</div>
|
||||
<div class="col-2 text-end">
|
||||
<span name="backorder-qty" t-esc="line.reserved_uom_qty - line.qty_done"/>
|
||||
<span t-if="props.displayUoM" name="uom" class="ms-1" t-esc="line.product_uom_id.name"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<t t-set-slot="footer">
|
||||
<button class="btn btn-primary" t-on-click.prevent="_onApply">Validate</button>
|
||||
<button class="btn btn-secondary" t-on-click="props.close">Stay on transfer</button>
|
||||
</t>
|
||||
</Dialog>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,21 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import LineComponent from "@stock_barcode/components/line";
|
||||
|
||||
export default class GroupedLineComponent extends LineComponent {
|
||||
|
||||
get isSelected() {
|
||||
return this.line.virtual_ids.indexOf(this.env.model.selectedLineVirtualId) !== -1;
|
||||
}
|
||||
|
||||
get opened() {
|
||||
return this.env.model.groupKey(this.line) === this.env.model.unfoldLineKey;
|
||||
}
|
||||
|
||||
toggleSublines(ev) {
|
||||
ev.stopPropagation();
|
||||
this.env.model.toggleSublines(this.line);
|
||||
}
|
||||
}
|
||||
GroupedLineComponent.components = { LineComponent };
|
||||
GroupedLineComponent.template = 'stock_barcode.GroupedLineComponent';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user