[FIX] sale: Split product_id_change into several compute methods

This commit is contained in:
Yannick Tivisse
2021-12-02 12:12:02 +01:00
parent 8a6093c612
commit 916e92aa30
16 changed files with 97 additions and 115 deletions
@@ -64,10 +64,9 @@ class SaleOrderLine(models.Model):
if self.event_booth_pending_ids and (not self.event_id or self.event_id != self.event_booth_pending_ids.event_id):
self.event_booth_pending_ids = None
@api.onchange('event_booth_pending_ids')
def _onchange_event_booth_pending_ids(self):
# trigger the description computation
self.product_id_change()
@api.depends('event_booth_pending_ids')
def _compute_name(self):
super()._compute_name()
def _update_event_booths(self, set_paid=False):
for so_line in self.filtered('is_event_booth'):
+13 -5
View File
@@ -125,12 +125,18 @@ class SaleOrderLine(models.Model):
@api.depends('product_uom', 'product_uom_qty')
def _compute_price_unit(self):
super(SaleOrderLine, self.filtered(lambda l: not l.event_ticket_id))._compute_price_unit()
# We want to recompute the price if the product changes
# Not for other triggers like product_uom or product_uom_qty
# for event SOL
if self._origin.product_id != self.product_id:
super()._compute_price_unit()
else:
super(SaleOrderLine, self.filtered(lambda l: not l.event_ticket_id))._compute_price_unit()
@api.onchange('event_ticket_id')
def _onchange_event_ticket_id(self):
@api.depends('event_ticket_id')
def _compute_name(self):
# we call this to force update the default name
self.product_id_change()
super()._compute_name()
def unlink(self):
self._unlink_associated_registrations()
@@ -159,6 +165,8 @@ class SaleOrderLine(models.Model):
def _get_display_price(self, product):
if self.event_ticket_id and self.event_id:
return self.event_ticket_id.with_context(pricelist=self.order_id.pricelist_id.id, uom=self.product_uom.id).price_reduce
return self.event_ticket_id.with_context(
pricelist=self.order_id.pricelist_id.id,
uom=self.product_uom.id).price_reduce
else:
return super()._get_display_price(product)
@@ -242,12 +242,10 @@ class TestEventSale(TestEventSaleCommon):
'product_id': event_product.product_variant_id.id,
'product_uom_qty': 1,
'product_uom': event_product.uom_id.id,
'price_unit': event_product.list_price,
'order_id': so.id,
'event_id': event.id,
'event_ticket_id': event_ticket.id,
})
sol.product_id_change()
self.assertEqual(so.amount_total, 660.0, "Ticket is $1000 but the event product is on a pricelist 10 -> 6. So, $600 + a 10% tax.")
@users('user_salesman')
+4 -3
View File
@@ -150,7 +150,9 @@ class SaleOrder(models.Model):
return [('id', 'not in', order_ids)]
return ['&', ('order_line.invoice_lines.move_id.move_type', 'in', ('out_invoice', 'out_refund')), ('order_line.invoice_lines.move_id', operator, value)]
name = fields.Char(string='Order Reference', required=True, copy=False, readonly=True, states={'draft': [('readonly', False)]}, index=True, default=lambda self: _('New'))
name = fields.Char(
string='Order Reference', required=True, copy=False, readonly=True,
states={'draft': [('readonly', False)]}, index=True, default=lambda self: _('New'))
origin = fields.Char(string='Source Document', help="Reference of the document that generated this sales order request.")
client_order_ref = fields.Char(string='Customer Reference', copy=False)
reference = fields.Char(string='Payment Ref.', copy=False,
@@ -352,7 +354,7 @@ class SaleOrder(models.Model):
tax_totals = account_move._get_tax_totals(order.partner_id, tax_lines_data, order.amount_total, order.amount_untaxed, order.currency_id)
order.tax_totals_json = json.dumps(tax_totals)
# YTI TODO: Convert into compute method, however this introduce
# YTI TODO: Convert into compute method, however this introduces
# a behavior change that breaks the test test_reservation_method_w_sale
# Note: This should be the case
@api.onchange('expected_date')
@@ -488,7 +490,6 @@ class SaleOrder(models.Model):
if partner.sale_warn == 'block':
self.update({'partner_id': False, 'partner_invoice_id': False, 'partner_shipping_id': False, 'pricelist_id': False})
return {
'warning': {
'title': _("Warning for %s", partner.name),
+65 -56
View File
@@ -143,27 +143,11 @@ class SaleOrderLine(models.Model):
taxes = line.product_id.taxes_id.filtered(lambda t: t.company_id == line.env.company)
line.tax_id = fpos.map_tax(taxes)
@api.model
def _prepare_add_missing_fields(self, values):
""" Deduce missing required fields from the onchange """
res = {}
onchange_fields = ['name', 'price_unit', 'product_uom', 'tax_id']
if values.get('order_id') and values.get('product_id') and any(f not in values for f in onchange_fields):
line = self.new(values)
line.product_id_change()
for field in onchange_fields:
if field not in values:
res[field] = line._fields[field].convert_to_write(line[field], line)
return res
@api.model_create_multi
def create(self, vals_list):
for values in vals_list:
if values.get('display_type', self.default_get(['display_type'])['display_type']):
values.update(product_id=False, price_unit=0, product_uom_qty=0, product_uom=False, customer_lead=0)
values.update(self._prepare_add_missing_fields(values))
lines = super().create(vals_list)
for line in lines:
if line.product_id and line.order_id.state == 'sale':
@@ -226,7 +210,9 @@ class SaleOrderLine(models.Model):
return result
order_id = fields.Many2one('sale.order', string='Order Reference', required=True, ondelete='cascade', index=True, copy=False)
name = fields.Text(string='Description', required=True)
name = fields.Text(
string='Description', required=True,
compute='_compute_name', store=True, readonly=False, pre_compute=True)
sequence = fields.Integer(string='Sequence', default=10)
invoice_lines = fields.Many2many('account.move.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_line_id', string='Invoice Lines', copy=False)
@@ -238,7 +224,7 @@ class SaleOrderLine(models.Model):
], string='Invoice Status', compute='_compute_invoice_status', store=True, default='no')
price_unit = fields.Float(
'Unit Price', required=True, digits='Product Price',
compute='_compute_price_unit', store=True, readonly=False)
compute='_compute_price_unit', store=True, readonly=False, pre_compute=True)
price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', store=True)
price_tax = fields.Float(compute='_compute_amount', string='Total Tax', store=True)
@@ -264,14 +250,22 @@ class SaleOrderLine(models.Model):
product_uom_qty = fields.Float(
string='Quantity', digits='Product Unit of Measure', required=True, default=1.0,
compute='_compute_product_uom_qty', store=True, readonly=False, pre_compute=True)
product_uom = fields.Many2one('uom.uom', string='Unit of Measure', domain="[('category_id', '=', product_uom_category_id)]", ondelete="restrict")
product_uom = fields.Many2one(
'uom.uom', string='Unit of Measure',
compute='_compute_product_uom', store=True, readonly=False, pre_compute=True,
domain="[('category_id', '=', product_uom_category_id)]", ondelete="restrict")
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
product_uom_readonly = fields.Boolean(compute='_compute_product_uom_readonly')
product_custom_attribute_value_ids = fields.One2many('product.attribute.custom.value', 'sale_order_line_id', string="Custom Values", copy=True)
product_custom_attribute_value_ids = fields.One2many(
'product.attribute.custom.value', 'sale_order_line_id',
string="Custom Values", copy=True,
compute='_compute_custom_attribute_values', store=True, readonly=False)
# M2M holding the values of product.attribute with create_variant field set to 'no_variant'
# It allows keeping track of the extra_price associated to those attribute values and add them to the SO line description
product_no_variant_attribute_value_ids = fields.Many2many('product.template.attribute.value', string="Extra Values", ondelete='restrict')
product_no_variant_attribute_value_ids = fields.Many2many(
'product.template.attribute.value', string="Extra Values", ondelete='restrict',
compute='_compute_no_variant_attribute_values', store=True, readonly=False)
qty_delivered_method = fields.Selection([
('manual', 'Manual'),
@@ -325,11 +319,12 @@ class SaleOrderLine(models.Model):
product_packaging_id = fields.Many2one(
'product.packaging', string='Packaging',
compute='_compute_product_packaging_id', store=True, readonly=False,
compute='_compute_product_packaging_id', store=True, readonly=False, pre_compute=True,
domain="[('sales', '=', True), ('product_id','=',product_id)]",
check_company=True)
product_packaging_qty = fields.Float(
'Packaging Quantity', compute='_compute_product_packaging_qty', store=True, readonly=False)
'Packaging Quantity',
compute='_compute_product_packaging_qty', store=True, readonly=False, pre_compute=True)
@api.depends('state')
def _compute_product_uom_readonly(self):
@@ -461,9 +456,17 @@ class SaleOrderLine(models.Model):
packaging_uom_qty / line.product_packaging_id.qty,
precision_rounding=packaging_uom.rounding)
@api.depends('product_packaging_qty')
@api.depends('product_id')
def _compute_product_uom(self):
for line in self:
if not line.product_uom or (line.product_id.uom_id.id != line.product_uom.id):
line.product_uom = line.product_id.uom_id
@api.depends('product_packaging_qty', 'product_id')
def _compute_product_uom_qty(self):
for line in self:
if not line.product_uom or (line.product_id.uom_id.id != line.product_uom.id):
line.product_uom_qty = line.product_uom_qty or 1.0
if not line.product_packaging_id:
continue
packaging_uom = line.product_packaging_id.product_uom_id
@@ -620,41 +623,47 @@ class SaleOrderLine(models.Model):
# negative discounts (= surcharge) are included in the display price
return max(base_price, final_price)
@api.depends('product_id')
def _compute_name(self):
for line in self:
if not line.product_id:
continue
product = line.product_id.with_context(
lang=get_lang(line.env, line.order_id.partner_id.lang).code,
partner=line.order_id.partner_id,
quantity=line.product_uom_qty,
date=line.order_id.date_order,
pricelist=line.order_id.pricelist_id.id,
uom=line.product_uom.id)
line.name = line.get_sale_order_line_multiline_description_sale(product)
@api.depends('product_id')
def _compute_custom_attribute_values(self):
for line in self:
if not line.product_id:
continue
valid_values = line.product_id.product_tmpl_id.valid_product_template_attribute_line_ids.product_template_value_ids
# remove the is_custom values that don't belong to this template
for pacv in line.product_custom_attribute_value_ids:
if pacv.custom_product_template_attribute_value_id not in valid_values:
line.product_custom_attribute_value_ids -= pacv
@api.depends('product_id')
def _compute_no_variant_attribute_values(self):
for line in self:
if not line.product_id:
continue
valid_values = line.product_id.product_tmpl_id.valid_product_template_attribute_line_ids.product_template_value_ids
# remove the no_variant attributes that don't belong to this template
for ptav in line.product_no_variant_attribute_value_ids:
if ptav._origin not in valid_values:
line.product_no_variant_attribute_value_ids -= ptav
@api.onchange('product_id')
def product_id_change(self):
if not self.product_id:
return
valid_values = self.product_id.product_tmpl_id.valid_product_template_attribute_line_ids.product_template_value_ids
# remove the is_custom values that don't belong to this template
for pacv in self.product_custom_attribute_value_ids:
if pacv.custom_product_template_attribute_value_id not in valid_values:
self.product_custom_attribute_value_ids -= pacv
# remove the no_variant attributes that don't belong to this template
for ptav in self.product_no_variant_attribute_value_ids:
if ptav._origin not in valid_values:
self.product_no_variant_attribute_value_ids -= ptav
vals = {}
if not self.product_uom or (self.product_id.uom_id.id != self.product_uom.id):
vals['product_uom'] = self.product_id.uom_id
vals['product_uom_qty'] = self.product_uom_qty or 1.0
product = self.product_id.with_context(
lang=get_lang(self.env, self.order_id.partner_id.lang).code,
partner=self.order_id.partner_id,
quantity=vals.get('product_uom_qty') or self.product_uom_qty,
date=self.order_id.date_order,
pricelist=self.order_id.pricelist_id.id,
uom=self.product_uom.id
)
vals.update(name=self.get_sale_order_line_multiline_description_sale(product))
if self.order_id.pricelist_id and self.order_id.partner_id:
vals['price_unit'] = self.env['account.tax']._fix_tax_included_price_company(self._get_display_price(product), product.taxes_id, self.tax_id, self.company_id)
self.update(vals)
product = self.product_id
if product.sale_line_warn != 'no-message':
if product.sale_line_warn == 'block':
self.product_id = False
@@ -671,8 +680,8 @@ class SaleOrderLine(models.Model):
'order_id.partner_id')
def _compute_price_unit(self):
for line in self:
line.price_unit = 0.0
if not line.product_uom or not line.product_id:
line.price_unit = 0.0
continue
if line.order_id.pricelist_id and line.order_id.partner_id:
product = line.product_id.with_context(
+2 -6
View File
@@ -151,13 +151,12 @@ class TestOnchangeProductId(TransactionCase):
'pricelist_id': christmas_pricelist.id,
})
order_line = self.env['sale.order.line'].new({
order_line = self.env['sale.order.line'].create({
'order_id': so.id,
'product_id': computer_case.id,
})
# force compute uom and prices
order_line.product_id_change()
order_line._onchange_discount()
self.assertEqual(order_line.price_subtotal, 90, "Christmas discount pricelist rule not applied")
self.assertEqual(order_line.discount, 10, "Christmas discount not equalt to 10%")
@@ -207,13 +206,11 @@ class TestOnchangeProductId(TransactionCase):
'pricelist_id': second_pricelist.id,
})
order_line = self.env['sale.order.line'].new({
order_line = self.env['sale.order.line'].create({
'order_id': so.id,
'product_id': computer_case.id,
})
# force compute uom and prices
order_line.product_id_change()
order_line._onchange_discount()
self.assertEqual(order_line.price_subtotal, 81, "Second pricelist rule not applied")
self.assertEqual(order_line.discount, 19, "Second discount not applied")
@@ -270,7 +267,6 @@ class TestOnchangeProductId(TransactionCase):
})
# force compute uom and prices
order_line.product_id_change()
self.assertEqual(order_line.price_unit, 180, "First pricelist rule not applied")
order_line.product_uom = new_uom
self.assertEqual(order_line.price_unit, 1800, "First pricelist rule not applied")
-6
View File
@@ -46,7 +46,6 @@ class TestReInvoice(TestSaleCommon):
'price_unit': self.company_data['product_order_cost'].list_price,
'order_id': self.sale_order.id,
})
sale_order_line1.product_id_change()
sale_order_line2 = self.env['sale.order.line'].create({
'name': self.company_data['product_delivery_cost'].name,
'product_id': self.company_data['product_delivery_cost'].id,
@@ -56,7 +55,6 @@ class TestReInvoice(TestSaleCommon):
'price_unit': self.company_data['product_delivery_cost'].list_price,
'order_id': self.sale_order.id,
})
sale_order_line2.product_id_change()
self.sale_order.action_confirm()
@@ -164,7 +162,6 @@ class TestReInvoice(TestSaleCommon):
'price_unit': self.company_data['product_delivery_sales_price'].list_price,
'order_id': self.sale_order.id,
})
sale_order_line1.product_id_change()
sale_order_line2 = self.env['sale.order.line'].create({
'name': self.company_data['product_order_sales_price'].name,
'product_id': self.company_data['product_order_sales_price'].id,
@@ -174,7 +171,6 @@ class TestReInvoice(TestSaleCommon):
'price_unit': self.company_data['product_order_sales_price'].list_price,
'order_id': self.sale_order.id,
})
sale_order_line2.product_id_change()
self.sale_order.action_confirm()
# create invoice lines and validate it
@@ -268,7 +264,6 @@ class TestReInvoice(TestSaleCommon):
'discount': 100.00,
'order_id': self.sale_order.id,
})
so_line1.product_id_change()
so_line2 = self.env['sale.order.line'].create({
'name': self.company_data['product_delivery_sales_price'].name,
'product_id': self.company_data['product_delivery_sales_price'].id,
@@ -278,7 +273,6 @@ class TestReInvoice(TestSaleCommon):
'discount': 100.00,
'order_id': self.sale_order.id,
})
so_line2.product_id_change()
self.sale_order.action_confirm()
-2
View File
@@ -69,8 +69,6 @@ class TestSaleFlow(TestSaleCommonBase):
}),
],
})
for line in sale_order.order_line:
line.product_id_change()
sale_order.action_confirm()
-1
View File
@@ -283,7 +283,6 @@ class TestSaleOrder(TestSaleCommon):
# Trigger onchange to reset discount, unit price, subtotal, ...
for line in self.sale_order.order_line:
line.product_id_change()
line._onchange_discount()
for line in self.sale_order.order_line:
-2
View File
@@ -121,7 +121,6 @@ class TestSaleOrder(TestSaleCommon):
self.sale_order.write({'pricelist_id': self.pricelist_discount_incl.id})
# Trigger onchange to reset discount, unit price, subtotal, ...
for line in self.sale_order.order_line:
line.product_id_change()
line._onchange_discount()
# Check that pricelist of the SO has been applied on the sale order lines or not
for line in self.sale_order.order_line:
@@ -145,7 +144,6 @@ class TestSaleOrder(TestSaleCommon):
self.sale_order.write({'pricelist_id': self.pricelist_discount_excl.id})
# Trigger onchange to reset discount, unit price, subtotal, ...
for line in self.sale_order.order_line:
line.product_id_change()
line._onchange_discount()
# Check pricelist of the SO apply or not on order lines where pricelist contains formula that add 15% on the cost price
+10 -10
View File
@@ -177,16 +177,16 @@ class SaleOrderLine(models.Model):
sale_order_option_ids = fields.One2many('sale.order.option', 'line_id', 'Optional Products Lines')
# Take the description on the order template if the product is present in it
@api.onchange('product_id')
def product_id_change(self):
domain = super(SaleOrderLine, self).product_id_change()
if self.product_id and self.order_id.sale_order_template_id:
for line in self.order_id.sale_order_template_id.sale_order_template_line_ids:
if line.product_id == self.product_id:
self.name = line.with_context(lang=self.order_id.partner_id.lang).name + self._get_sale_order_line_multiline_description_variants()
break
return domain
@api.depends('product_id')
def _compute_name(self):
# Take the description on the order template if the product is present in it
super()._compute_name()
for line in self:
if line.product_id and line.order_id.sale_order_template_id:
for line in line.order_id.sale_order_template_id.sale_order_template_line_ids:
if line.product_id == line.product_id:
line.name = line.with_context(lang=line.order_id.partner_id.lang).name + line._get_sale_order_line_multiline_description_variants()
break
class SaleOrderOption(models.Model):
@@ -116,16 +116,13 @@ class SaleOrder(models.Model):
product_no_variant_attribute_value_ids=no_variant_attribute_values.ids)
))
if product_ids:
res = False
if new_lines:
# Add new SO lines
self.update(dict(order_line=new_lines))
# Recompute prices for new/modified lines
for line in self.order_line.filtered(lambda line: line.product_id.id in product_ids):
res = line.product_id_change() or res
line._onchange_discount()
return res
def _get_matrix(self, product_template):
"""Return the matrix of the given product, updated with current SOLines quantities.
@@ -61,7 +61,6 @@ class TestReInvoice(TestCommonSaleTimesheet):
'price_unit': self.company_data['product_order_cost'].list_price,
'order_id': self.sale_order.id,
})
sale_order_line1.product_id_change()
sale_order_line2 = self.env['sale.order.line'].create({
'name': self.company_data['product_delivery_cost'].name,
'product_id': self.company_data['product_delivery_cost'].id,
@@ -70,7 +69,6 @@ class TestReInvoice(TestCommonSaleTimesheet):
'price_unit': self.company_data['product_delivery_cost'].list_price,
'order_id': self.sale_order.id,
})
sale_order_line2.product_id_change()
self.sale_order.action_confirm()
@@ -162,7 +160,6 @@ class TestReInvoice(TestCommonSaleTimesheet):
'price_unit': self.company_data['product_delivery_sales_price'].list_price,
'order_id': self.sale_order.id,
})
sale_order_line1.product_id_change()
sale_order_line2 = self.env['sale.order.line'].create({
'name': self.company_data['product_order_sales_price'].name,
'product_id': self.company_data['product_order_sales_price'].id,
@@ -172,7 +169,6 @@ class TestReInvoice(TestCommonSaleTimesheet):
'price_unit': self.company_data['product_order_sales_price'].list_price,
'order_id': self.sale_order.id,
})
sale_order_line2.product_id_change()
self.sale_order.action_confirm()
# let's log some timesheets (on the project created by sale_order_line1)
@@ -141,7 +141,6 @@ class TestSaleService(TestCommonSaleTimesheet):
'price_unit': self.product_delivery_timesheet2.list_price,
'order_id': self.sale_order.id,
})
so_line_deliver_global_project.product_id_change()
self.sale_order.action_confirm()
task_serv2 = self.env['project.task'].search([('sale_line_id', '=', so_line_deliver_global_project.id)])
@@ -187,7 +186,6 @@ class TestSaleService(TestCommonSaleTimesheet):
'price_unit': self.product_delivery_timesheet3.list_price,
'order_id': self.sale_order.id,
})
so_line_deliver_new_task_project.product_id_change()
self.sale_order.action_confirm()
task_serv2 = self.env['project.task'].search([('sale_line_id', '=', so_line_deliver_new_task_project.id)])
@@ -510,8 +508,6 @@ class TestSaleService(TestCommonSaleTimesheet):
'price_unit': self.product_delivery_timesheet3.list_price,
'order_id': self.sale_order.id,
})
so_line_deliver_new_task_project.product_id_change()
so_line_deliver_new_task_project_2.product_id_change()
self.sale_order.action_confirm()
project = so_line_deliver_new_task_project.project_id
@@ -50,8 +50,6 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
'price_unit': self.product_order_timesheet2.list_price,
'order_id': sale_order.id,
})
so_line_ordered_project_only.product_id_change()
so_line_ordered_global_project.product_id_change()
sale_order.action_confirm()
task_serv2 = self.env['project.task'].search([('sale_line_id', '=', so_line_ordered_global_project.id)])
project_serv1 = self.env['project.project'].search([('sale_line_id', '=', so_line_ordered_project_only.id)])
@@ -188,8 +186,6 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
'price_unit': self.product_delivery_timesheet3.list_price,
'order_id': sale_order.id,
})
so_line_deliver_global_project.product_id_change()
so_line_deliver_task_project.product_id_change()
# confirm SO
sale_order.action_confirm()
@@ -415,8 +411,6 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
'price_unit': self.product_delivery_timesheet3.list_price,
'order_id': sale_order.id,
})
so_line_deliver_global_project.product_id_change()
so_line_deliver_task_project.product_id_change()
# confirm SO
sale_order.action_confirm()
@@ -185,7 +185,6 @@ class TestWebsiteSaleProductPricelist(TestSaleProductAttributeValueCommon):
self.assertEqual(round(sol.price_total), 110.0, "110$ with 10% included tax")
so.pricelist_id = pricelist
so.fiscal_position_id = fpos
sol.product_id_change()
with MockRequest(self.env, website=current_website, sale_order_id=so.id):
so._cart_update(product_id=test_product.product_variant_id.id, line_id=sol.id, set_qty=1)
self.assertEqual(round(sol.price_total), 50, "100$ with 50% discount + 0% tax (mapped from fp 10% -> 0%)")