质量模块和库存扫码
This commit is contained in:
16
stock_barcode/models/__init__.py
Normal file
16
stock_barcode/models/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import stock_picking
|
||||
from . import stock_quant
|
||||
from . import stock_scrap
|
||||
from . import stock_location
|
||||
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
stock_barcode/models/product_packaging.py
Normal file
20
stock_barcode/models/product_packaging.py
Normal 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']
|
||||
44
stock_barcode/models/product_product.py
Normal file
44
stock_barcode/models/product_product.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, api
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
_inherit = 'product.product'
|
||||
_barcode_field = 'barcode'
|
||||
|
||||
@api.model
|
||||
def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
|
||||
# sudo is added for external users to get the products
|
||||
args = self.env.company.sudo().nomenclature_id._preprocess_gs1_search_args(args, ['product'])
|
||||
return super()._search(args, offset=offset, limit=limit, order=order, count=count, 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):
|
||||
quant = self.env['stock.quant'].search_read(
|
||||
[
|
||||
lot_id and ('lot_id', '=', lot_id) or lot_name and ('lot_id.name', '=', lot_name),
|
||||
('location_id.usage', '=', 'internal'),
|
||||
('product_id', '=', self.id),
|
||||
],
|
||||
['package_id', 'owner_id'],
|
||||
limit=1, load=False
|
||||
)
|
||||
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
|
||||
20
stock_barcode/models/res_config_settings.py
Normal file
20
stock_barcode/models/res_config_settings.py
Normal file
@@ -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
stock_barcode/models/res_partner.py
Normal file
12
stock_barcode/models/res_partner.py
Normal 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
stock_barcode/models/stock_location.py
Normal file
18
stock_barcode/models/stock_location.py
Normal 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, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
|
||||
args = self.env.company.nomenclature_id._preprocess_gs1_search_args(args, ['location', 'location_dest'])
|
||||
return super()._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid)
|
||||
|
||||
@api.model
|
||||
def _get_fields_stock_barcode(self):
|
||||
return ['barcode', 'display_name', 'name', 'parent_path', 'usage']
|
||||
20
stock_barcode/models/stock_lot.py
Normal file
20
stock_barcode/models/stock_lot.py
Normal 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 StockLot(models.Model):
|
||||
_inherit = 'stock.lot'
|
||||
_barcode_field = 'name'
|
||||
|
||||
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']
|
||||
57
stock_barcode/models/stock_move_line.py
Normal file
57
stock_barcode/models/stock_move_line.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
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')
|
||||
picking_location_id = fields.Many2one(related='picking_id.location_id')
|
||||
picking_location_dest_id = fields.Many2one(related='picking_id.location_dest_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.")
|
||||
is_completed = fields.Boolean(compute='_compute_is_completed', help="Check if the quantity done matches the demand")
|
||||
|
||||
@api.depends('product_id', 'product_id.stock_quant_ids')
|
||||
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_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)
|
||||
|
||||
@api.depends('qty_done')
|
||||
def _compute_is_completed(self):
|
||||
for line in self:
|
||||
line.is_completed = line.qty_done == line.reserved_uom_qty
|
||||
|
||||
def _inverse_dummy_id(self):
|
||||
pass
|
||||
|
||||
def _get_fields_stock_barcode(self):
|
||||
return [
|
||||
'product_id',
|
||||
'location_id',
|
||||
'location_dest_id',
|
||||
'qty_done',
|
||||
'display_name',
|
||||
'reserved_uom_qty',
|
||||
'product_uom_id',
|
||||
'product_barcode',
|
||||
'owner_id',
|
||||
'lot_id',
|
||||
'lot_name',
|
||||
'package_id',
|
||||
'result_package_id',
|
||||
'dummy_id',
|
||||
'product_packaging_id',
|
||||
'product_packaging_uom_qty',
|
||||
]
|
||||
12
stock_barcode/models/stock_package_type.py
Normal file
12
stock_barcode/models/stock_package_type.py
Normal file
@@ -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']
|
||||
336
stock_barcode/models/stock_picking.py
Normal file
336
stock_barcode/models/stock_picking.py
Normal file
@@ -0,0 +1,336 @@
|
||||
# -*- 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_print_barcode_pdf(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.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']._get_usable_packages()
|
||||
package_types = package_types.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)])
|
||||
locations = move_lines.location_id | move_lines.location_dest_id | source_locations | destination_locations
|
||||
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()
|
||||
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,
|
||||
'immediate_transfer': True,
|
||||
})
|
||||
|
||||
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_line_ids',
|
||||
'picking_type_id',
|
||||
'location_id',
|
||||
'location_dest_id',
|
||||
'name',
|
||||
'state',
|
||||
'picking_type_code',
|
||||
'company_id',
|
||||
'immediate_transfer',
|
||||
'note',
|
||||
'picking_type_entire_packs',
|
||||
'use_create_lots',
|
||||
'use_existing_lots',
|
||||
'user_id',
|
||||
]
|
||||
|
||||
@api.model
|
||||
def filter_on_barcode(self, barcode):
|
||||
""" Searches ready pickings for the scanned product/package.
|
||||
"""
|
||||
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'):
|
||||
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_read([('barcode', '=', barcode)], ['id'], limit=1)
|
||||
if product:
|
||||
product_id = product[0]['id']
|
||||
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_read([('name', '=', barcode)], ['id'], limit=1)
|
||||
if package:
|
||||
package_id = package[0]['id']
|
||||
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 not picking_nums:
|
||||
if barcode_type:
|
||||
return {
|
||||
'warning': {
|
||||
'title': _("No %(picking_type)s ready for this %(barcode_type)s", picking_type=picking_type.name, barcode_type=barcode_type),
|
||||
}
|
||||
}
|
||||
return {
|
||||
'warning': {
|
||||
'title': _('No product or package found for barcode %s', barcode),
|
||||
'message': _('Scan a product 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.')
|
||||
|
||||
@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.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 for each product, the destination location must be either scanned after each line too, either 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.
|
||||
'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',
|
||||
]
|
||||
136
stock_barcode/models/stock_quant.py
Normal file
136
stock_barcode/models/stock_quant.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# -*- 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')
|
||||
|
||||
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')
|
||||
|
||||
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()
|
||||
36
stock_barcode/models/stock_quant_package.py
Normal file
36
stock_barcode/models/stock_quant_package.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, api
|
||||
|
||||
|
||||
class QuantPackage(models.Model):
|
||||
_inherit = 'stock.quant.package'
|
||||
_barcode_field = 'name'
|
||||
|
||||
@api.model
|
||||
def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
|
||||
args = self.env.company.nomenclature_id._preprocess_gs1_search_args(args, ['package'], 'name')
|
||||
return super()._search(args, offset=offset, limit=limit, order=order, count=count, 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),
|
||||
]
|
||||
return self.env['stock.quant.package'].search(usable_packages_domain)
|
||||
29
stock_barcode/models/stock_scrap.py
Normal file
29
stock_barcode/models/stock_scrap.py
Normal 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
stock_barcode/models/stock_warehouse.py
Normal file
24
stock_barcode/models/stock_warehouse.py
Normal 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
stock_barcode/models/uom_uom.py
Normal file
16
stock_barcode/models/uom_uom.py
Normal 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',
|
||||
]
|
||||
Reference in New Issue
Block a user