This commit is contained in:
ljmwkx
2024-12-30 11:37:32 +08:00
parent 1f18963217
commit 0d9e44b638
229 changed files with 144211 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
from . import controllers
from . import models
from . import wizard
+51
View File
@@ -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/**/*',
],
}
}
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import stock_barcode
+356
View File
@@ -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')}
+24
View File
@@ -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>
+119
View File
@@ -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
+17
View File
@@ -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
+20
View File
@@ -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']
+53
View File
@@ -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)]))
+12
View File
@@ -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']
+18
View File
@@ -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']
+26
View File
@@ -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']
+49
View File
@@ -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
+100
View File
@@ -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']
+375
View File
@@ -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',
]
+143
View File
@@ -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)
+29
View File
@@ -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
+24
View File
@@ -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
+16
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 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

+20
View File
@@ -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.
+117
View File
@@ -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)
+194
View File
@@ -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