# -*- 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)