[MERGE][MOV] product: split product.py

--
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr

closes odoo/odoo#84970

Signed-off-by: Victor Feyens (vfe) <vfe@odoo.com>
This commit is contained in:
Odoo's Mergebot
2022-02-21 12:24:17 +01:00
committed by GitHub
5 changed files with 226 additions and 208 deletions
+5 -3
View File
@@ -3,16 +3,18 @@
# flake8: noqa: F401
from . import decimal_precision
# don't try to be a good boy and sort imports alphabetically.
# `product.template` should be initialised before `product.product`
from . import product_template
from . import product
from . import product_product
from . import decimal_precision
from . import product_attribute
from . import product_category
from . import product_packaging
from . import product_pricelist
from . import product_pricelist_item
from . import product_supplierinfo
from . import product_tag
from . import res_company
from . import res_config_settings
+62
View File
@@ -0,0 +1,62 @@
# -*- 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 UserError, ValidationError
class ProductCategory(models.Model):
_name = "product.category"
_description = "Product Category"
_parent_name = "parent_id"
_parent_store = True
_rec_name = 'complete_name'
_order = 'complete_name'
name = fields.Char('Name', index='trigram', required=True)
complete_name = fields.Char(
'Complete Name', compute='_compute_complete_name', recursive=True,
store=True)
parent_id = fields.Many2one('product.category', 'Parent Category', index=True, ondelete='cascade')
parent_path = fields.Char(index=True, unaccent=False)
child_id = fields.One2many('product.category', 'parent_id', 'Child Categories')
product_count = fields.Integer(
'# Products', compute='_compute_product_count',
help="The number of products under this category (Does not consider the children categories)")
@api.depends('name', 'parent_id.complete_name')
def _compute_complete_name(self):
for category in self:
if category.parent_id:
category.complete_name = '%s / %s' % (category.parent_id.complete_name, category.name)
else:
category.complete_name = category.name
def _compute_product_count(self):
read_group_res = self.env['product.template'].read_group([('categ_id', 'child_of', self.ids)], ['categ_id'], ['categ_id'])
group_data = dict((data['categ_id'][0], data['categ_id_count']) for data in read_group_res)
for categ in self:
product_count = 0
for sub_categ_id in categ.search([('id', 'child_of', categ.ids)]).ids:
product_count += group_data.get(sub_categ_id, 0)
categ.product_count = product_count
@api.constrains('parent_id')
def _check_category_recursion(self):
if not self._check_recursion():
raise ValidationError(_('You cannot create recursive categories.'))
@api.model
def name_create(self, name):
return self.create({'name': name}).name_get()[0]
def name_get(self):
if not self.env.context.get('hierarchical_naming', True):
return [(record.id, record.name) for record in self]
return super().name_get()
@api.ondelete(at_uninstall=False)
def _unlink_except_default_category(self):
main_category = self.env.ref('product.product_category_all')
if main_category in self:
raise UserError(_("You cannot delete this product category, it is the default generic category."))
@@ -0,0 +1,75 @@
# -*- 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)
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 packagins use the same pattern. Therefore, we need
to ensure the uniqueness between products' barcodes and packagings' ones"""
condition = expression.OR([[('barcode', '=', b)] for b in self.mapped('barcode') if b])
if self.env['product.product'].search(condition, 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
and float_compare(
product_qty / packaging_qty,
float_round(product_qty / packaging_qty, precision_rounding=1.0),
precision_rounding=default_uom.rounding
)
!= 0
):
return float_round(
product_qty / packaging_qty, precision_rounding=1.0, rounding_method=rounding_method
) * packaging_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']
@@ -4,68 +4,9 @@
import re
from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError, ValidationError
from odoo.exceptions import ValidationError
from odoo.osv import expression
from odoo.tools import float_compare, float_round
class ProductCategory(models.Model):
_name = "product.category"
_description = "Product Category"
_parent_name = "parent_id"
_parent_store = True
_rec_name = 'complete_name'
_order = 'complete_name'
name = fields.Char('Name', index='trigram', required=True)
complete_name = fields.Char(
'Complete Name', compute='_compute_complete_name', recursive=True,
store=True)
parent_id = fields.Many2one('product.category', 'Parent Category', index=True, ondelete='cascade')
parent_path = fields.Char(index=True, unaccent=False)
child_id = fields.One2many('product.category', 'parent_id', 'Child Categories')
product_count = fields.Integer(
'# Products', compute='_compute_product_count',
help="The number of products under this category (Does not consider the children categories)")
@api.depends('name', 'parent_id.complete_name')
def _compute_complete_name(self):
for category in self:
if category.parent_id:
category.complete_name = '%s / %s' % (category.parent_id.complete_name, category.name)
else:
category.complete_name = category.name
def _compute_product_count(self):
read_group_res = self.env['product.template'].read_group([('categ_id', 'child_of', self.ids)], ['categ_id'], ['categ_id'])
group_data = dict((data['categ_id'][0], data['categ_id_count']) for data in read_group_res)
for categ in self:
product_count = 0
for sub_categ_id in categ.search([('id', 'child_of', categ.ids)]).ids:
product_count += group_data.get(sub_categ_id, 0)
categ.product_count = product_count
@api.constrains('parent_id')
def _check_category_recursion(self):
if not self._check_recursion():
raise ValidationError(_('You cannot create recursive categories.'))
@api.model
def name_create(self, name):
return self.create({'name': name}).name_get()[0]
def name_get(self):
if not self.env.context.get('hierarchical_naming', True):
return [(record.id, record.name) for record in self]
return super().name_get()
@api.ondelete(at_uninstall=False)
def _unlink_except_default_category(self):
main_category = self.env.ref('product.product_category_all')
if main_category in self:
raise UserError(_("You cannot delete this product category, it is the default generic category."))
from odoo.tools import float_compare
class ProductProduct(models.Model):
@@ -745,147 +686,3 @@ class ProductProduct(models.Model):
uom = self.env['uom.uom'].browse(self.env.context.get('uom'))
date = self.env.context.get('date')
return pricelist._get_product_price(self, quantity, uom=uom, date=date)
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)
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 packagins use the same pattern. Therefore, we need
to ensure the uniqueness between products' barcodes and packagings' ones"""
condition = expression.OR([[('barcode', '=', b)] for b in self.mapped('barcode') if b])
if self.env['product.product'].search(condition, 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
and float_compare(
product_qty / packaging_qty,
float_round(product_qty / packaging_qty, precision_rounding=1.0),
precision_rounding=default_uom.rounding
)
!= 0
):
return float_round(
product_qty / packaging_qty, precision_rounding=1.0, rounding_method=rounding_method
) * packaging_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']
class SupplierInfo(models.Model):
_name = "product.supplierinfo"
_description = "Supplier Pricelist"
_order = 'sequence, min_qty DESC, price, id'
_rec_name = 'partner_id'
def _default_product_id(self):
product_id = self.env.get('default_product_id')
if not product_id:
model, active_id = [self.env.context.get(k) for k in ['model', 'active_id']]
if model == 'product.product' and active_id:
product_id = self.env[model].browse(active_id).exists()
return product_id
def _domain_product_id(self):
domain = "product_tmpl_id and [('product_tmpl_id', '=', product_tmpl_id)] or []"
if self.env.context.get('base_model_name') == 'product.template':
domain = "[('product_tmpl_id', '=', parent.id)]"
elif self.env.context.get('base_model_name') == 'product.product':
domain = "[('product_tmpl_id', '=', parent.product_tmpl_id)]"
return domain
partner_id = fields.Many2one(
'res.partner', 'Vendor',
ondelete='cascade', required=True,
help="Vendor of this product", check_company=True)
product_name = fields.Char(
'Vendor Product Name',
help="This vendor's product name will be used when printing a request for quotation. Keep empty to use the internal one.")
product_code = fields.Char(
'Vendor Product Code',
help="This vendor's product code will be used when printing a request for quotation. Keep empty to use the internal one.")
sequence = fields.Integer(
'Sequence', default=1, help="Assigns the priority to the list of product vendor.")
product_uom = fields.Many2one(
'uom.uom', 'Unit of Measure',
related='product_tmpl_id.uom_po_id',
help="This comes from the product form.")
min_qty = fields.Float(
'Quantity', default=0.0, required=True, digits="Product Unit Of Measure",
help="The quantity to purchase from this vendor to benefit from the price, expressed in the vendor Product Unit of Measure if not any, in the default unit of measure of the product otherwise.")
price = fields.Float(
'Price', default=0.0, digits='Product Price',
required=True, help="The price to purchase a product")
company_id = fields.Many2one(
'res.company', 'Company',
default=lambda self: self.env.company.id, index=1)
currency_id = fields.Many2one(
'res.currency', 'Currency',
default=lambda self: self.env.company.currency_id.id,
required=True)
date_start = fields.Date('Start Date', help="Start date for this vendor price")
date_end = fields.Date('End Date', help="End date for this vendor price")
product_id = fields.Many2one(
'product.product', 'Product Variant', check_company=True,
domain=_domain_product_id, default=_default_product_id,
help="If not set, the vendor price will apply to all variants of this product.")
product_tmpl_id = fields.Many2one(
'product.template', 'Product Template', check_company=True,
index=True, ondelete='cascade')
product_variant_count = fields.Integer('Variant Count', related='product_tmpl_id.product_variant_count')
delay = fields.Integer(
'Delivery Lead Time', default=1, required=True,
help="Lead time in days between the confirmation of the purchase order and the receipt of the products in your warehouse. Used by the scheduler for automatic computation of the purchase order planning.")
@api.onchange('product_tmpl_id')
def _onchange_product_tmpl_id(self):
if self.product_id and self.product_tmpl_id and self.product_id not in self.product_tmpl_id.product_variant_ids:
self.product_id = False
@api.model
def get_import_templates(self):
return [{
'label': _('Import Template for Vendor Pricelists'),
'template': '/product/static/xls/product_supplierinfo.xls'
}]
@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
class SupplierInfo(models.Model):
_name = "product.supplierinfo"
_description = "Supplier Pricelist"
_order = 'sequence, min_qty DESC, price, id'
_rec_name = 'partner_id'
def _default_product_id(self):
product_id = self.env.get('default_product_id')
if not product_id:
model, active_id = [self.env.context.get(k) for k in ['model', 'active_id']]
if model == 'product.product' and active_id:
product_id = self.env[model].browse(active_id).exists()
return product_id
def _domain_product_id(self):
domain = "product_tmpl_id and [('product_tmpl_id', '=', product_tmpl_id)] or []"
if self.env.context.get('base_model_name') == 'product.template':
domain = "[('product_tmpl_id', '=', parent.id)]"
elif self.env.context.get('base_model_name') == 'product.product':
domain = "[('product_tmpl_id', '=', parent.product_tmpl_id)]"
return domain
partner_id = fields.Many2one(
'res.partner', 'Vendor',
ondelete='cascade', required=True,
help="Vendor of this product", check_company=True)
product_name = fields.Char(
'Vendor Product Name',
help="This vendor's product name will be used when printing a request for quotation. Keep empty to use the internal one.")
product_code = fields.Char(
'Vendor Product Code',
help="This vendor's product code will be used when printing a request for quotation. Keep empty to use the internal one.")
sequence = fields.Integer(
'Sequence', default=1, help="Assigns the priority to the list of product vendor.")
product_uom = fields.Many2one(
'uom.uom', 'Unit of Measure',
related='product_tmpl_id.uom_po_id',
help="This comes from the product form.")
min_qty = fields.Float(
'Quantity', default=0.0, required=True, digits="Product Unit Of Measure",
help="The quantity to purchase from this vendor to benefit from the price, expressed in the vendor Product Unit of Measure if not any, in the default unit of measure of the product otherwise.")
price = fields.Float(
'Price', default=0.0, digits='Product Price',
required=True, help="The price to purchase a product")
company_id = fields.Many2one(
'res.company', 'Company',
default=lambda self: self.env.company.id, index=1)
currency_id = fields.Many2one(
'res.currency', 'Currency',
default=lambda self: self.env.company.currency_id.id,
required=True)
date_start = fields.Date('Start Date', help="Start date for this vendor price")
date_end = fields.Date('End Date', help="End date for this vendor price")
product_id = fields.Many2one(
'product.product', 'Product Variant', check_company=True,
domain=_domain_product_id, default=_default_product_id,
help="If not set, the vendor price will apply to all variants of this product.")
product_tmpl_id = fields.Many2one(
'product.template', 'Product Template', check_company=True,
index=True, ondelete='cascade')
product_variant_count = fields.Integer('Variant Count', related='product_tmpl_id.product_variant_count')
delay = fields.Integer(
'Delivery Lead Time', default=1, required=True,
help="Lead time in days between the confirmation of the purchase order and the receipt of the products in your warehouse. Used by the scheduler for automatic computation of the purchase order planning.")
@api.onchange('product_tmpl_id')
def _onchange_product_tmpl_id(self):
if self.product_id and self.product_tmpl_id and self.product_id not in self.product_tmpl_id.product_variant_ids:
self.product_id = False
@api.model
def get_import_templates(self):
return [{
'label': _('Import Template for Vendor Pricelists'),
'template': '/product/static/xls/product_supplierinfo.xls'
}]