Files
odoo_source/addons/product/models/product_packaging.py
T
Triet Ngo d05372eb7b [IMP] purchase, sale, stock: add packaging in reports
Several reports don't include product packing info on them. This is
useful for customers to see that what they bought matches what they
expected and for pickers who need to know which product packaging they
should be selecting from stock. Reports this was added to are:
- Sales orders,
- purchase orders (including RFQs),
- picking operations,
- delivery slips.

Note that we purposely exclude backordered moves since they weren't
deemed to be necessary at the time this feature was created.

Also includes light refactoring to remove repeated code for conversion
of product qty to packaging qty and to make it easier to do this
conversion in the future (i.e. can pass product.packaging without
original record that it was assigned to)

Task id 2927379

closes odoo/odoo#96858

Signed-off-by: Steve Van Essche <svs@odoo.com>
2023-10-02 14:12:45 +00:00

81 lines
4.1 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from odoo.osv import expression
from odoo.tools import float_compare, float_round
class ProductPackaging(models.Model):
_name = "product.packaging"
_description = "Product Packaging"
_order = 'product_id, sequence, id'
_check_company_auto = True
name = fields.Char('Product Packaging', required=True)
sequence = fields.Integer('Sequence', default=1, help="The first in the sequence is the default one.")
product_id = fields.Many2one('product.product', string='Product', check_company=True, required=True, ondelete="cascade")
qty = fields.Float('Contained Quantity', default=1, digits='Product Unit of Measure', help="Quantity of products contained in the packaging.")
barcode = fields.Char('Barcode', copy=False, help="Barcode used for packaging identification. Scan this packaging barcode from a transfer in the Barcode app to move all the contained units")
product_uom_id = fields.Many2one('uom.uom', related='product_id.uom_id', readonly=True)
company_id = fields.Many2one('res.company', 'Company', index=True)
_sql_constraints = [
('positive_qty', 'CHECK(qty > 0)', 'Contained Quantity should be positive.'),
('barcode_uniq', 'unique(barcode)', 'A barcode can only be assigned to one packaging.'),
]
@api.constrains('barcode')
def _check_barcode_uniqueness(self):
""" With GS1 nomenclature, products and packagings use the same pattern. Therefore, we need
to ensure the uniqueness between products' barcodes and packagings' ones"""
domain = [('barcode', 'in', [b for b in self.mapped('barcode') if b])]
if self.env['product.product'].search(domain, order="id", limit=1):
raise ValidationError(_("A product already uses the barcode"))
def _check_qty(self, product_qty, uom_id, rounding_method="HALF-UP"):
"""Check if product_qty in given uom is a multiple of the packaging qty.
If not, rounding the product_qty to closest multiple of the packaging qty
according to the rounding_method "UP", "HALF-UP or "DOWN".
"""
self.ensure_one()
default_uom = self.product_id.uom_id
packaging_qty = default_uom._compute_quantity(self.qty, uom_id)
# We do not use the modulo operator to check if qty is a mltiple of q. Indeed the quantity
# per package might be a float, leading to incorrect results. For example:
# 8 % 1.6 = 1.5999999999999996
# 5.4 % 1.8 = 2.220446049250313e-16
if product_qty and packaging_qty:
rounded_qty = float_round(product_qty / packaging_qty, precision_rounding=1.0,
rounding_method=rounding_method) * packaging_qty
return rounded_qty if float_compare(rounded_qty, product_qty, precision_rounding=default_uom.rounding) else product_qty
return product_qty
def _find_suitable_product_packaging(self, product_qty, uom_id):
""" try find in `self` if a packaging's qty in given uom is a divisor of
the given product_qty. If so, return the one with greatest divisor.
"""
packagings = self.sorted(lambda p: p.qty, reverse=True)
for packaging in packagings:
new_qty = packaging._check_qty(product_qty, uom_id)
if new_qty == product_qty:
return packaging
return self.env['product.packaging']
def _compute_qty(self, qty, qty_uom=False):
"""Returns the qty of this packaging that qty converts to.
A float is returned because there are edge cases where some users use
"part" of a packaging
:param qty: float of product quantity (given in product UoM if no qty_uom provided)
:param qty_uom: Optional uom of quantity
:returns: float of packaging qty
"""
self.ensure_one()
if qty_uom:
qty = qty_uom._compute_quantity(qty, self.product_uom_id)
return float_round(qty / self.qty, precision_rounding=self.product_uom_id.rounding)