[MERGE] sale: Convert onchanges into compute + improve perfs

Purpose
=======

This is the first step of a technical cleaning of the sales
application. The final goal is to reduce the technical debt
and improve the performances / readability of the related
applications.

Some complex onchange methods containing purely business code
are defined, and called manually wherever the modifications
should be applied. This is a bad practice as there could be
a different business logic when making the same flow in the
interface or with python code.

Since #80449, it is now possible to add the precompute attribute on
stored computed fields to compute them before the insertion into the
database.

The idea is to convert this business code into compute methods to
apply it every time this is needed.

This is quite useful in our case in the sales application, as some of
the computed fields are required, and request some hacky overrides:
- in the create method, to simulate the onchange on new records, and
  to update the values dictionary
- in the default_get method, to set a default value, which has to
  side effect to prevent the field to be recomputed afterwards, which
  could also lead to inconsistencies.

This made impossible to convert the onchange methods into compute
methods.

Now, that is possible to precompute the fields before the database
insertion, it is now possible to remove all this crappy overhead,
to benefit from the batch method definitions, and to avoid creating
fake records to simulate the values that we should have computed
before.

On the other hand, this is the occasion to write some methods
(create, ...) in batch, to remove some dead code, to simplify
the api, to reorganize the code using our coding guidelines.

Results
======

The benefit of calling the computes in batch doesn't only bring
functional consistencies, it also allow to improve the performances
without changing any existing features.

For example, let's have a look at the execution time and queries to
generate 1000 sales orders with 10 products lines each + 1 section
line, for 1000 different customers and shared between 2 salesmen.

We consider the "batch" time, aka the time to call the "create" method,
the flush time and the number of SQL queries.

In master:
    BATCH: 121.97 seconds
    FLUSH: 24.05 seconds
    # queries: 53103

Now:
    BATCH: 113.07 seconds
    FLUSH: 1.22 seconds
    # queries 22057

This means a reduction of:
- 7.2% for the batch time
- 95% of the flush time
- 21.7% for the total time
- 58.5% for the number of SQL queries

Note that the effect of the precompute, the onchange methods
conversions, and the optimizations on this PR are emphasized
when the sales orders are related to the same partners.

If we take a look at the flamegraphs when creating 100 sales orders
of 3 lines each, we directly spot that the methods are now called once,
which also justify the large decrease of execution time and SQL queries.

Master:
![Screenshot from 2021-12-01 10-23-27](https://user-images.githubusercontent.com/8435879/144207460-5af36354-5ab6-4f83-846b-be71ab38ccc0.png)

Now:
![precompute](https://user-images.githubusercontent.com/8435879/144198634-1c32d9c9-0cf1-4011-a32f-b1edd7034d37.png)

closes odoo/odoo#79093

Taskid: 2679099
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
Co-authored-by: Victor Feyens vfe@odoo.com
This commit is contained in:
Odoo's Mergebot
2021-12-02 14:43:07 +01:00
committed by GitHub
co-authored by Victor Feyens vfe@odoo.com
79 changed files with 1130 additions and 1239 deletions
+6 -6
View File
@@ -284,9 +284,9 @@ class AccountMove(models.Model):
# =========================================================
# ==== Business fields ====
fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', readonly=True,
states={'draft': [('readonly', False)]},
check_company=True,
fiscal_position_id = fields.Many2one(
'account.fiscal.position', string='Fiscal Position', readonly=True,
states={'draft': [('readonly', False)]}, check_company=True,
domain="[('company_id', '=', company_id)]", ondelete="restrict",
help="Fiscal positions are used to adapt taxes and accounts for particular customers or sales orders/invoices. "
"The default value comes from the customer.")
@@ -502,9 +502,9 @@ class AccountMove(models.Model):
self.partner_bank_id = bank_ids and bank_ids[0]
# Find the new fiscal position.
delivery_partner_id = self._get_invoice_delivery_partner_id()
self.fiscal_position_id = self.env['account.fiscal.position'].get_fiscal_position(
self.partner_id.id, delivery_id=delivery_partner_id)
delivery_partner = self.env['res.partner'].browse(self._get_invoice_delivery_partner_id())
self.fiscal_position_id = self.env['account.fiscal.position']._get_fiscal_position(
self.partner_id, delivery=delivery_partner)
self._recompute_dynamic_lines()
if warning:
return {'warning': warning}
@@ -424,7 +424,7 @@ class AccountReconcileModel(models.Model):
if line.tax_ids:
taxes = line.tax_ids
detected_fiscal_position = self.env['account.fiscal.position'].get_fiscal_position(partner_id)
detected_fiscal_position = self.env['account.fiscal.position']._get_fiscal_position(self.env['res.partner'].browse(partner_id))
if detected_fiscal_position:
taxes = detected_fiscal_position.map_tax(taxes)
writeoff_line['tax_ids'] = [Command.set(taxes.ids)]
+6 -11
View File
@@ -206,27 +206,22 @@ class AccountFiscalPosition(models.Model):
return fpos
@api.model
def get_fiscal_position(self, partner_id, delivery_id=None):
def _get_fiscal_position(self, partner, delivery=None):
"""
:return: fiscal position found (recordset)
:rtype: :class:`account.fiscal.position`
"""
if not partner_id:
if not partner:
return self.env['account.fiscal.position']
# This can be easily overridden to apply more complex fiscal rules
PartnerObj = self.env['res.partner']
partner = PartnerObj.browse(partner_id)
# if no delivery use invoicing
if delivery_id:
delivery = PartnerObj.browse(delivery_id)
else:
if not delivery:
delivery = partner
# partner manually set fiscal position always win
if delivery.property_account_position_id or partner.property_account_position_id:
return delivery.property_account_position_id or partner.property_account_position_id
manual_fiscal_position = delivery.property_account_position_id or partner.property_account_position_id
if manual_fiscal_position:
return manual_fiscal_position
# First search only matching VAT positions
vat_required = bool(partner.vat)
+9 -14
View File
@@ -80,11 +80,13 @@ class ResConfigSettings(models.TransientModel):
group_show_line_subtotals_tax_excluded = fields.Boolean(
"Show line subtotals without taxes (B2B)",
implied_group='account.group_show_line_subtotals_tax_excluded',
group='base.group_portal,base.group_user,base.group_public')
group='base.group_portal,base.group_user,base.group_public',
compute='_compute_group_show_line_subtotals', store=True, readonly=False)
group_show_line_subtotals_tax_included = fields.Boolean(
"Show line subtotals with taxes (B2C)",
implied_group='account.group_show_line_subtotals_tax_included',
group='base.group_portal,base.group_user,base.group_public')
group='base.group_portal,base.group_user,base.group_public',
compute='_compute_group_show_line_subtotals', store=True, readonly=False)
group_show_sale_receipts = fields.Boolean(string='Sale Receipt',
implied_group='account.group_sale_receipts')
group_show_purchase_receipts = fields.Boolean(string='Purchase Receipt',
@@ -154,18 +156,11 @@ class ResConfigSettings(models.TransientModel):
self.has_chart_of_accounts = bool(self.company_id.chart_template_id)
self.has_accounting_entries = self.env['account.chart.template'].existing_accounting(self.company_id)
@api.onchange('show_line_subtotals_tax_selection')
def _onchange_sale_tax(self):
if self.show_line_subtotals_tax_selection == "tax_excluded":
self.update({
'group_show_line_subtotals_tax_included': False,
'group_show_line_subtotals_tax_excluded': True,
})
else:
self.update({
'group_show_line_subtotals_tax_included': True,
'group_show_line_subtotals_tax_excluded': False,
})
@api.depends('show_line_subtotals_tax_selection')
def _compute_group_show_line_subtotals(self):
for wizard in self:
wizard.group_show_line_subtotals_tax_included = wizard.show_line_subtotals_tax_selection == "tax_included"
wizard.group_show_line_subtotals_tax_excluded = wizard.show_line_subtotals_tax_selection == "tax_excluded"
@api.onchange('group_analytic_accounting')
def onchange_analytic_accounting(self):
+1 -1
View File
@@ -64,7 +64,7 @@ class TestFiscalPosition(common.TransactionCase):
def test_10_fp_country(self):
def assert_fp(partner, expected_pos, message):
self.assertEqual(
self.fp.get_fiscal_position(partner.id).id,
self.fp._get_fiscal_position(partner).id,
expected_pos.id,
message)
-3
View File
@@ -19,21 +19,18 @@ class TestSettings(AccountTestInvoicingCommon):
def switch_tax_settings(self, config):
config.show_line_subtotals_tax_selection = "tax_excluded"
config._onchange_sale_tax()
config.flush()
config.execute()
self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_excluded'), True)
self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_included'), False)
config.show_line_subtotals_tax_selection = "tax_included"
config._onchange_sale_tax()
config.flush()
config.execute()
self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_excluded'), False)
self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_included'), True)
config.show_line_subtotals_tax_selection = "tax_excluded"
config._onchange_sale_tax()
config.flush()
config.execute()
self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_excluded'), True)
+1 -1
View File
@@ -155,7 +155,7 @@ class AccruedExpenseRevenue(models.TransientModel):
o.order_line.with_context(accrual_entry_date=self.date)._compute_qty_delivered()
o.order_line.with_context(accrual_entry_date=self.date)._compute_qty_invoiced()
o.order_line.with_context(accrual_entry_date=self.date)._compute_untaxed_amount_invoiced()
o.order_line.with_context(accrual_entry_date=self.date)._get_to_invoice_qty()
o.order_line.with_context(accrual_entry_date=self.date)._compute_qty_to_invoice()
lines = o.order_line.filtered(
lambda l: fields.Float.compare(
l.qty_to_invoice,
@@ -56,6 +56,5 @@ class ChooseDeliveryCarrier(models.TransientModel):
})
if partner_shipping != self.order_id.partner_shipping_id:
self.order_id.partner_shipping_id = partner_shipping
self.order_id.onchange_partner_shipping_id()
return super().button_confirm()
@@ -64,10 +64,13 @@ 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):
"""Override to add the compute dependency.
The custom name logic can be found below in get_sale_order_line_multiline_description_sale.
"""
super()._compute_name()
def _update_event_booths(self, set_paid=False):
for so_line in self.filtered('is_event_booth'):
+6
View File
@@ -43,6 +43,12 @@ class EventTemplateTicket(models.Model):
if not ticket.description:
ticket.description = False
# TODO clean this feature in master
# Feature broken by design, depending on the hacky `price` field on products
# context_dependent, core part of the pricelist mess
# This field usage should be restricted to the UX, and any use in effective
# price computation should be replaced by clear calls to the pricelist API
@api.depends_context('uom', 'qty', 'pricelist') # Cf product.price context dependencies
@api.depends('product_id', 'price')
def _compute_price_reduce(self):
for ticket in self:
+44 -21
View File
@@ -57,14 +57,15 @@ class SaleOrder(models.Model):
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
event_id = fields.Many2one(
'event.event', string='Event',
compute="_compute_event_id", store=True, readonly=False, precompute=True,
help="Choose an event and it will automatically create a registration for this event.")
event_ticket_id = fields.Many2one(
'event.event.ticket', string='Event Ticket',
compute="_compute_event_ticket_id", store=True, readonly=False, precompute=True,
help="Choose an event ticket and it will automatically create a registration for this event ticket.")
event_ok = fields.Boolean(compute='_compute_event_ok')
@@ -110,28 +111,44 @@ class SaleOrderLine(models.Model):
RegistrationSudo.create(registrations_vals)
return True
@api.onchange('product_id')
def _onchange_product_id(self):
# We reset the event when keeping it would lead to an inconstitent state.
# We need to do it this way because the only relation between the product and the event is through the corresponding tickets.
if self.event_id and (not self.product_id or self.product_id.id not in self.event_id.mapped('event_ticket_ids.product_id.id')):
self.event_id = None
@api.depends('product_id')
def _compute_event_id(self):
event_lines = self.filtered(lambda line: line.product_id and line.product_id.detailed_type == 'event')
(self - event_lines).event_id = False
for line in event_lines:
if line.product_id not in line.event_id.event_ticket_ids.product_id:
line.event_id = False
@api.onchange('event_id')
def _onchange_event_id(self):
# We reset the ticket when keeping it would lead to an inconstitent state.
if self.event_ticket_id and (not self.event_id or self.event_id != self.event_ticket_id.event_id):
self.event_ticket_id = None
@api.depends('event_id')
def _compute_event_ticket_id(self):
event_lines = self.filtered('event_id')
(self - event_lines).event_ticket_id = False
for line in event_lines:
if line.event_id != line.event_ticket_id.event_id:
line.event_ticket_id = False
@api.onchange('product_uom', 'product_uom_qty')
def product_uom_change(self):
if not self.event_ticket_id:
super(SaleOrderLine, self).product_uom_change()
@api.depends('event_ticket_id')
def _compute_price_unit(self):
"""Do not update the price on qty/uom change"""
event_lines = self.filtered('event_ticket_id')
super(SaleOrderLine, self-event_lines)._compute_price_unit()
for line in event_lines:
if not line.product_id or line._origin.product_id != line.product_id:
super(SaleOrderLine, line)._compute_price_unit()
@api.onchange('event_ticket_id')
def _onchange_event_ticket_id(self):
# we call this to force update the default name
self.product_id_change()
@api.depends('event_ticket_id')
def _compute_discount(self):
"""Do not compute the discount on event lines, it's always included in the price."""
event_lines = self.filtered('event_ticket_id')
super(SaleOrderLine, self-event_lines)._compute_discount()
@api.depends('event_ticket_id')
def _compute_name(self):
"""Override to add the compute dependency.
The custom name logic can be found below in get_sale_order_line_multiline_description_sale.
"""
super()._compute_name()
def unlink(self):
self._unlink_associated_registrations()
@@ -160,6 +177,12 @@ 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
# FIXME this is strange
# price_reduce is the price after discount
# shouldn't we leave the discount computation to sale
# and use the non reduced price here (aka price field)
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)
@@ -49,10 +49,7 @@ class TestEventSale(TestEventSaleCommon):
cls.env['sale.order.line'].create({
'product_id': product.id,
'price_unit': 190.50,
'product_uom': cls.env.ref('uom.product_uom_unit').id,
'product_uom_qty': 1.0,
'order_id': cls.sale_order.id,
'name': 'sales order line',
'event_id': cls.event_0.id,
'event_ticket_id': cls.ticket.id,
})
@@ -238,16 +235,11 @@ class TestEventSale(TestEventSaleCommon):
'pricelist_id': pricelist.id,
})
sol = self.env['sale.order.line'].create({
'name': event.name,
'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
@@ -39,11 +39,12 @@ class AccountAnalyticLine(models.Model):
return []
task_id = fields.Many2one(
'project.task', 'Task', compute='_compute_task_id', store=True, readonly=False, index=True,
'project.task', 'Task', index=True,
compute='_compute_task_id', store=True, readonly=False,
domain="[('company_id', '=', company_id), ('project_id.allow_timesheets', '=', True), ('project_id', '=?', project_id)]")
project_id = fields.Many2one(
'project.project', 'Project', compute='_compute_project_id', store=True, readonly=False,
domain=_domain_project_id)
'project.project', 'Project', domain=_domain_project_id,
compute='_compute_project_id', store=True, readonly=False)
user_id = fields.Many2one(compute='_compute_user_id', store=True, readonly=False)
employee_id = fields.Many2one('hr.employee', "Employee", domain=_domain_employee_id)
department_id = fields.Many2one('hr.department', "Department", compute='_compute_department_id', store=True, compute_sudo=True)
@@ -12,30 +12,26 @@ class AccountFiscalPosition(models.Model):
'should be auto-detected')
@api.model
def get_fiscal_position(self, partner_id, delivery_id=None):
def _get_fiscal_position(self, partner, delivery=None):
""" Take into account the partner afip responsibility in order to auto-detect the fiscal position """
company = self.env.company
if company.country_id.code == "AR":
PartnerObj = self.env['res.partner']
partner = PartnerObj.browse(partner_id)
if self.env.company.country_id.code != "AR":
return super()._get_fiscal_position(partner, delivery=delivery)
# if no delivery use invoicing
if delivery_id:
delivery = PartnerObj.browse(delivery_id)
else:
delivery = partner
# if no delivery use invoicing
if not delivery:
delivery = partner
# partner manually set fiscal position always win
if delivery.property_account_position_id or partner.property_account_position_id:
return delivery.property_account_position_id or partner.property_account_position_id
domain = [
('auto_apply', '=', True),
('l10n_ar_afip_responsibility_type_ids', '=', self.env['res.partner'].browse(
partner_id).l10n_ar_afip_responsibility_type_id.id),
('company_id', '=', company.id),
]
return self.sudo().search(domain, limit=1)
return super().get_fiscal_position(partner_id, delivery_id=delivery_id)
# partner manually set fiscal position always win
manual_fiscal_position = delivery.property_account_position_id or partner.property_account_position_id
if manual_fiscal_position:
return manual_fiscal_position
domain = [
('auto_apply', '=', True),
('l10n_ar_afip_responsibility_type_ids', '=', partner.l10n_ar_afip_responsibility_type_id.id),
('company_id', '=', self.env.company.id),
]
return self.sudo().search(domain, limit=1)
@api.onchange('l10n_ar_afip_responsibility_type_ids', 'country_group_id', 'country_id', 'zip_from', 'zip_to')
def _onchange_afip_responsibility(self):
+1 -1
View File
@@ -48,7 +48,7 @@ class RepairLine(models.Model):
tax = self.env['account.tax']
if partner:
partner_invoice = self.repair_id.partner_invoice_id or partner
fpos = self.env['account.fiscal.position'].get_fiscal_position(partner_invoice.id, delivery_id=self.repair_id.address_id.id)
fpos = self.env['account.fiscal.position']._get_fiscal_position(partner_invoice, delivery=self.repair_id.address_id)
taxes = self.product_id.taxes_id.filtered(lambda x: x.company_id == self.repair_id.company_id)
tax = fpos.map_tax(taxes)
return {
-1
View File
@@ -51,7 +51,6 @@ class PosOrder(models.Model):
'discount': line.discount,
'sequence': sale_lines and sale_lines[-1].sequence + 1 or 10,
})
sale_line._compute_tax_id()
line.sale_order_line_id = sale_line
so_lines = order.lines.mapped('sale_order_line_id')
+4 -4
View File
@@ -936,8 +936,8 @@ class Task(models.Model):
index=True,
copy=False,
readonly=True)
project_id = fields.Many2one('project.project', string='Project',
compute='_compute_project_id', recursive=True, store=True, readonly=False,
project_id = fields.Many2one('project.project', string='Project', recursive=True,
compute='_compute_project_id', store=True, readonly=False,
index=True, tracking=True, check_company=True, change_default=True)
# Defines in which project the task will be displayed / taken into account in statistics.
# Example: 1 task A with 1 subtask B in project P
@@ -967,8 +967,8 @@ class Task(models.Model):
search='_search_personal_stage_type_id',
help="The current user's personal task stage.")
partner_id = fields.Many2one('res.partner',
string='Customer',
compute='_compute_partner_id', recursive=True, store=True, readonly=False, tracking=True,
string='Customer', recursive=True, tracking=True,
compute='_compute_partner_id', store=True, readonly=False,
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
partner_is_company = fields.Boolean(related='partner_id.is_company', readonly=True)
commercial_partner_id = fields.Many2one(related='partner_id.commercial_partner_id')
+5 -5
View File
@@ -308,7 +308,7 @@ class PurchaseOrder(models.Model):
self.fiscal_position_id = False
self.currency_id = self.env.company.currency_id.id
else:
self.fiscal_position_id = self.env['account.fiscal.position'].get_fiscal_position(self.partner_id.id)
self.fiscal_position_id = self.env['account.fiscal.position']._get_fiscal_position(self.partner_id)
self.payment_term_id = self.partner_id.property_supplier_payment_term_id.id
self.currency_id = self.partner_id.property_purchase_currency_id.id or self.env.company.currency_id.id
return {}
@@ -572,15 +572,15 @@ class PurchaseOrder(models.Model):
if not journal:
raise UserError(_('Please define an accounting purchase journal for the company %s (%s).') % (self.company_id.name, self.company_id.id))
partner_invoice_id = self.partner_id.address_get(['invoice'])['invoice']
partner_invoice = self.env['res.partner'].browse(self.partner_id.address_get(['invoice'])['invoice'])
invoice_vals = {
'ref': self.partner_ref or '',
'move_type': move_type,
'narration': self.notes,
'currency_id': self.currency_id.id,
'invoice_user_id': self.user_id and self.user_id.id or self.env.user.id,
'partner_id': partner_invoice_id,
'fiscal_position_id': (self.fiscal_position_id or self.fiscal_position_id.get_fiscal_position(partner_invoice_id)).id,
'partner_id': partner_invoice.id,
'fiscal_position_id': (self.fiscal_position_id or self.fiscal_position_id._get_fiscal_position(partner_invoice)).id,
'payment_reference': self.partner_ref or '',
'partner_bank_id': self.partner_id.bank_ids[:1].id,
'invoice_origin': self.name,
@@ -953,7 +953,7 @@ class PurchaseOrderLine(models.Model):
def _compute_tax_id(self):
for line in self:
line = line.with_company(line.company_id)
fpos = line.order_id.fiscal_position_id or line.order_id.fiscal_position_id.get_fiscal_position(line.order_id.partner_id.id)
fpos = line.order_id.fiscal_position_id or line.order_id.fiscal_position_id._get_fiscal_position(line.order_id.partner_id)
# filter taxes by company
taxes = line.product_id.supplier_taxes_id.filtered(lambda r: r.company_id == line.env.company)
line.taxes_id = fpos.map_tax(taxes)
@@ -24,7 +24,7 @@ class PurchaseOrder(models.Model):
payment_term = partner.property_supplier_payment_term_id
FiscalPosition = self.env['account.fiscal.position']
fpos = FiscalPosition.with_company(self.company_id).get_fiscal_position(partner.id)
fpos = FiscalPosition.with_company(self.company_id)._get_fiscal_position(partner)
self.partner_id = partner.id
self.fiscal_position_id = fpos.id
+1 -1
View File
@@ -270,7 +270,7 @@ class StockRule(models.Model):
partner = values['supplier'].partner_id
purchase_date = procurement_date_planned - relativedelta(days=supplier_delay)
fpos = self.env['account.fiscal.position'].with_company(company_id).get_fiscal_position(partner.id)
fpos = self.env['account.fiscal.position'].with_company(company_id)._get_fiscal_position(partner)
gpo = self.group_propagation_option
group = (gpo == 'fixed' and self.group_id.id) or \
+3 -3
View File
@@ -342,7 +342,7 @@ class Repair(models.Model):
current_invoices_list = grouped_invoices_vals[(partner_invoice.id, currency.id, company.id)]
if not group or len(current_invoices_list) == 0:
fpos = self.env['account.fiscal.position'].get_fiscal_position(partner_invoice.id, delivery_id=repair.address_id.id)
fpos = self.env['account.fiscal.position']._get_fiscal_position(partner_invoice, delivery=repair.address_id)
invoice_vals = {
'move_type': 'out_invoice',
'partner_id': partner_invoice.id,
@@ -717,7 +717,7 @@ class RepairLine(models.Model):
self.product_uom = product.uom_id.id
if self.type != 'remove':
if partner:
fpos = self.env['account.fiscal.position'].get_fiscal_position(partner_invoice.id, delivery_id=self.repair_id.address_id.id)
fpos = self.env['account.fiscal.position']._get_fiscal_position(partner_invoice, delivery=self.repair_id.address_id)
taxes = self.product_id.taxes_id.filtered(lambda x: x.company_id == self.repair_id.company_id)
self.tax_id = fpos.map_tax(taxes)
warning = False
@@ -795,7 +795,7 @@ class RepairFee(models.Model):
pricelist = self.repair_id.pricelist_id
if partner and self.product_id:
fpos = self.env['account.fiscal.position'].get_fiscal_position(partner_invoice.id, delivery_id=self.repair_id.address_id.id)
fpos = self.env['account.fiscal.position']._get_fiscal_position(partner_invoice, delivery=self.repair_id.address_id)
taxes = self.product_id.taxes_id.filtered(lambda x: x.company_id == self.repair_id.company_id)
self.tax_id = fpos.map_tax(taxes)
if partner:
+23 -14
View File
@@ -4,6 +4,7 @@
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class AccountMove(models.Model):
_name = 'account.move'
_inherit = ['account.move', 'utm.mixin']
@@ -14,6 +15,7 @@ class AccountMove(models.Model):
team_id = fields.Many2one(
'crm.team', string='Sales Team', default=_get_invoice_default_sale_team,
compute='_compute_team_id', store=True, readonly=False,
ondelete="set null", tracking=True,
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
partner_shipping_id = fields.Many2one(
@@ -23,22 +25,25 @@ class AccountMove(models.Model):
states={'draft': [('readonly', False)]},
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
help="Delivery address for current invoice.")
# UTMs - enforcing the fact that we want to 'set null' when relation is unlinked
campaign_id = fields.Many2one(ondelete='set null')
medium_id = fields.Many2one(ondelete='set null')
source_id = fields.Many2one(ondelete='set null')
@api.onchange('partner_shipping_id', 'company_id')
def _onchange_partner_shipping_id(self):
"""
Trigger the change of fiscal position when the shipping address is modified.
"""
delivery_partner_id = self._get_invoice_delivery_partner_id()
fiscal_position = self.env['account.fiscal.position'].with_company(self.company_id).get_fiscal_position(
self.partner_id.id, delivery_id=delivery_partner_id)
fiscal_position_id = fields.Many2one(
compute='_compute_fiscal_position_id', store=True)
if fiscal_position:
self.fiscal_position_id = fiscal_position
@api.depends('partner_shipping_id', 'company_id')
def _compute_fiscal_position_id(self):
# Trigger the change of fiscal position when the shipping address is modified.
for move in self:
delivery_partner = self.env['res.partner'].browse(move._get_invoice_delivery_partner_id())
fiscal_position = self.env['account.fiscal.position'].with_company(
move.company_id)._get_fiscal_position(move.partner_id, delivery=delivery_partner)
if fiscal_position:
move.fiscal_position_id = fiscal_position
def unlink(self):
downpayment_lines = self.mapped('line_ids.sale_line_ids').filtered(lambda line: line.is_downpayment and line.invoice_lines <= self.mapped('line_ids'))
@@ -58,10 +63,14 @@ class AccountMove(models.Model):
return res
@api.onchange('invoice_user_id')
def onchange_user_id(self):
if self.invoice_user_id and self.invoice_user_id.sale_team_id:
self.team_id = self.env['crm.team']._get_default_team_id(user_id=self.invoice_user_id.id, domain=[('company_id', '=', self.company_id.id)])
@api.depends('invoice_user_id')
def _compute_team_id(self):
for move in self:
if not move.invoice_user_id.sale_team_id:
continue
move.team_id = self.env['crm.team']._get_default_team_id(
user_id=move.invoice_user_id.id,
domain=[('company_id', '=', move.company_id.id)])
def _reverse_moves(self, default_values_list=None, cancel=False):
# OVERRIDE
+1 -3
View File
@@ -118,8 +118,6 @@ class AccountMoveLine(models.Model):
# create the sale lines in batch
new_sale_lines = self.env['sale.order.line'].create(sale_line_values_to_create)
for sol in new_sale_lines:
sol._onchange_discount()
# build result map by replacing index with newly created record of sale.order.line
result = {}
@@ -158,7 +156,7 @@ class AccountMoveLine(models.Model):
last_so_line = self.env['sale.order.line'].search([('order_id', '=', order.id)], order='sequence desc', limit=1)
last_sequence = last_so_line.sequence + 1 if last_so_line else 100
fpos = order.fiscal_position_id or order.fiscal_position_id.get_fiscal_position(order.partner_id.id)
fpos = order.fiscal_position_id or order.fiscal_position_id._get_fiscal_position(order.partner_id)
taxes = fpos.map_tax(self.product_id.taxes_id)
return {
+8 -8
View File
@@ -23,9 +23,11 @@ class ProductTemplate(models.Model):
sale_line_warn = fields.Selection(WARNING_MESSAGE, 'Sales Order Line', help=WARNING_HELP, required=True, default="no-message")
sale_line_warn_msg = fields.Text('Message for Sales Order Line')
expense_policy = fields.Selection(
[('no', 'No'), ('cost', 'At cost'), ('sales_price', 'Sales price')],
string='Re-Invoice Expenses',
default='no',
[('no', 'No'),
('cost', 'At cost'),
('sales_price', 'Sales price')
], string='Re-Invoice Expenses', default='no',
compute='_compute_expense_policy', store=True, readonly=False,
help="Expenses and vendor bills can be re-invoiced to a customer."
"With this option, a validated expense can be re-invoice to a customer at its cost or sales price.")
visible_expense_policy = fields.Boolean("Re-Invoice Policy visible", compute='_compute_visible_expense_policy')
@@ -49,17 +51,15 @@ class ProductTemplate(models.Model):
product_template.visible_expense_policy = visibility
@api.onchange('sale_ok')
def _change_sale_ok(self):
if not self.sale_ok:
self.expense_policy = 'no'
@api.depends('sale_ok')
def _compute_expense_policy(self):
self.filtered(lambda t: not t.sale_ok).expense_policy = 'no'
@api.depends('product_variant_ids.sales_count')
def _compute_sales_count(self):
for product in self:
product.sales_count = float_round(sum([p.sales_count for p in product.with_context(active_test=False).product_variant_ids]), precision_rounding=product.uom_id.rounding)
@api.constrains('company_id')
def _check_sale_product_company(self):
"""Ensure the product is not being restricted to a single company while
+143 -137
View File
@@ -1,14 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from functools import partial
from datetime import timedelta
from itertools import groupby
import json
from odoo import api, fields, models, SUPERUSER_ID, _
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools.misc import formatLang
from odoo.osv import expression
from odoo.tools import float_is_zero, html_keep_url, is_html_empty
@@ -24,7 +22,7 @@ class SaleOrder(models.Model):
if self.env['ir.config_parameter'].sudo().get_param('sale.use_quotation_validity_days'):
days = self.env.company.quotation_validity_days
if days > 0:
return fields.Date.to_string(datetime.now() + timedelta(days))
return fields.Datetime.now() + timedelta(days)
return False
def _get_default_require_signature(self):
@@ -105,29 +103,9 @@ class SaleOrder(models.Model):
return super(SaleOrder, self).get_empty_list_help(help)
@api.model
def _default_note_url(self):
def _get_note_url(self):
return self.env.company.get_base_url()
@api.model
def _default_note(self):
use_invoice_terms = self.env['ir.config_parameter'].sudo().get_param('account.use_invoice_terms')
if use_invoice_terms and self.env.company.terms_type == "html":
baseurl = html_keep_url(self._default_note_url() + '/terms')
return _('Terms & Conditions: %s', baseurl)
return use_invoice_terms and self.env.company.invoice_terms or ''
@api.model
def _get_default_team(self):
return self.env['crm.team']._get_default_team_id()
@api.onchange('fiscal_position_id')
def _compute_tax_id(self):
"""
Trigger the recompute of the taxes if the fiscal position is changed on the SO.
"""
for order in self:
order.order_line._compute_tax_id()
def _search_invoice_ids(self, operator, value):
if operator == 'in' and value:
self.env.cr.execute("""
@@ -161,7 +139,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,
@@ -186,29 +166,33 @@ class SaleOrder(models.Model):
create_date = fields.Datetime(string='Creation Date', readonly=True, index=True, help="Date on which sales order is created.")
user_id = fields.Many2one(
'res.users', string='Salesperson', index=True, tracking=2, default=lambda self: self.env.user,
'res.users', string='Salesperson', index=True, tracking=2,
compute='_compute_user_id', store=True, readonly=False, precompute=True,
domain=lambda self: [('groups_id', 'in', self.env.ref('sales_team.group_sale_salesman').id)])
partner_id = fields.Many2one(
'res.partner', string='Customer', readonly=True,
states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
'res.partner', string='Customer', readonly=False,
states={'sale': [('readonly', True)], 'done': [('readonly', True)], 'cancel': [('readonly', True)]},
required=True, change_default=True, index=True, tracking=1,
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",)
partner_invoice_id = fields.Many2one(
'res.partner', string='Invoice Address',
readonly=True, required=True,
states={'draft': [('readonly', False)], 'sent': [('readonly', False)], 'sale': [('readonly', False)]},
'res.partner', string='Invoice Address', required=True,
compute='_compute_partner_invoice_id', store=True, readonly=False, precompute=True,
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",)
partner_shipping_id = fields.Many2one(
'res.partner', string='Delivery Address', readonly=True, required=True,
states={'draft': [('readonly', False)], 'sent': [('readonly', False)], 'sale': [('readonly', False)]},
'res.partner', string='Delivery Address', required=True,
compute='_compute_partner_shipping_id', store=True, readonly=False, precompute=True,
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",)
pricelist_id = fields.Many2one(
'product.pricelist', string='Pricelist', check_company=True, # Unrequired company
required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
'product.pricelist', string='Pricelist', required=False, check_company=True, # Unrequired company
compute='_compute_pricelist_id', store=True, precompute=True, readonly=False,
states={'sale': [('readonly', True)], 'done': [('readonly', True)], 'cancel': [('readonly', True)]},
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=1,
help="If you change the pricelist, only newly added lines will be affected.")
currency_id = fields.Many2one(related='pricelist_id.currency_id', depends=["pricelist_id"], store=True, ondelete="restrict")
currency_id = fields.Many2one(
related='pricelist_id.currency_id', depends=["pricelist_id"], store=True, precompute=True, ondelete="restrict")
analytic_account_id = fields.Many2one(
'account.analytic.account', 'Analytic Account',
readonly=True, copy=False, check_company=True, # Unrequired company
@@ -227,21 +211,28 @@ class SaleOrder(models.Model):
('no', 'Nothing to Invoice')
], string='Invoice Status', compute='_get_invoice_status', store=True)
note = fields.Html('Terms and conditions', default=_default_note)
note = fields.Html(
'Terms and conditions',
compute='_compute_note', store=True, readonly=False, precompute=True)
terms_type = fields.Selection(related='company_id.terms_type')
amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, compute='_amount_all', tracking=5)
tax_totals_json = fields.Char(compute='_compute_tax_totals_json')
amount_tax = fields.Monetary(string='Taxes', store=True, compute='_amount_all')
amount_total = fields.Monetary(string='Total', store=True, compute='_amount_all', tracking=4)
currency_rate = fields.Float("Currency Rate", compute='_compute_currency_rate', store=True, digits=(12, 6), help='The rate of the currency to the currency of rate 1 applicable at the date of the order')
currency_rate = fields.Float(
string="Currency Rate",
compute='_compute_currency_rate', store=True, digits=(12, 6), precompute=True,
help='The rate of the currency to the currency of rate 1 applicable at the date of the order')
payment_term_id = fields.Many2one(
'account.payment.term', string='Payment Terms', check_company=True, # Unrequired company
compute='_compute_payment_term_id', store=True, readonly=False, precompute=True,
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",)
fiscal_position_id = fields.Many2one(
'account.fiscal.position', string='Fiscal Position',
domain="[('company_id', '=', company_id)]", check_company=True,
compute='_compute_fiscal_position_id', store=True, readonly=False, precompute=True,
help="Fiscal positions are used to adapt taxes and accounts for particular customers or sales orders/invoices."
"The default value comes from the customer.")
tax_country_id = fields.Many2one(
@@ -252,18 +243,20 @@ class SaleOrder(models.Model):
team_id = fields.Many2one(
'crm.team', 'Sales Team',
ondelete="set null", tracking=True,
change_default=True, default=_get_default_team, check_company=True, # Unrequired company
compute='_compute_team_id', store=True, readonly=False, precompute=True,
change_default=True, check_company=True, # Unrequired company
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
signature = fields.Image('Signature', help='Signature received through the portal.', copy=False, attachment=True, max_width=1024, max_height=1024)
signed_by = fields.Char('Signed By', help='Name of the person that signed the SO.', copy=False)
signed_on = fields.Datetime('Signed On', help='Date of the signature.', copy=False)
commitment_date = fields.Datetime('Delivery Date', copy=False,
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
help="This is the delivery date promised to the customer. "
"If set, the delivery order will be scheduled based on "
"this date rather than product lead times.")
commitment_date = fields.Datetime(
'Delivery Date', copy=False,
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
help="This is the delivery date promised to the customer. "
"If set, the delivery order will be scheduled based on "
"this date rather than product lead times.")
expected_date = fields.Datetime("Expected Date", compute='_compute_expected_date', store=False, # Note: can not be stored since depends on today()
help="Delivery date you can promise to the customer, computed from the minimum lead time of the order lines.")
amount_undiscounted = fields.Float('Amount Before Discount', compute='_compute_amount_undiscounted', digits=0)
@@ -274,9 +267,11 @@ class SaleOrder(models.Model):
string='Transactions', copy=False, readonly=True)
authorized_transaction_ids = fields.Many2many('payment.transaction', compute='_compute_authorized_transaction_ids',
string='Authorized Transactions', copy=False)
show_update_pricelist = fields.Boolean(string='Has Pricelist Changed',
help="Technical Field, True if the pricelist was changed;\n"
" this will then display a recomputation button")
show_update_pricelist = fields.Boolean(
string='Has Pricelist Changed',
compute='_compute_show_update_pricelist', store=True, readonly=True, precompute=True,
help="Technical Field, True if the pricelist was changed;\n"
" this will then display a recomputation button")
tag_ids = fields.Many2many('crm.tag', 'sale_order_tag_rel', 'order_id', 'tag_id', string='Tags')
# UTMs - enforcing the fact that we want to 'set null' when relation is unlinked
campaign_id = fields.Many2one(ondelete='set null')
@@ -300,16 +295,26 @@ class SaleOrder(models.Model):
bad_products=', '.join(bad_products.mapped('display_name')),
))
@api.depends('pricelist_id', 'date_order', 'company_id')
@api.depends('currency_id', 'date_order', 'company_id')
def _compute_currency_rate(self):
cache = {}
for order in self:
order_date = order.date_order.date()
if not order.company_id:
order.currency_rate = order.currency_id.with_context(date=order.date_order).rate or 1.0
order.currency_rate = order.currency_id.with_context(date=order_date).rate or 1.0
continue
elif order.company_id.currency_id and order.currency_id: # the following crashes if any one is undefined
order.currency_rate = self.env['res.currency']._get_conversion_rate(order.company_id.currency_id, order.currency_id, order.company_id, order.date_order)
else:
elif not order.currency_id:
order.currency_rate = 1.0
else:
key = (order.company_id.id, order_date, order.currency_id.id)
if key not in cache:
cache[key] = self.env['res.currency']._get_conversion_rate(
from_currency=order.company_id.currency_id,
to_currency=order.currency_id,
company=order.company_id,
date=order_date,
)
order.currency_rate = cache[key]
def _compute_access_url(self):
super(SaleOrder, self)._compute_access_url()
@@ -333,7 +338,7 @@ class SaleOrder(models.Model):
dt = line._expected_date()
dates_list.append(dt)
if dates_list:
order.expected_date = fields.Datetime.to_string(min(dates_list))
order.expected_date = min(dates_list)
else:
order.expected_date = False
@@ -350,6 +355,9 @@ 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 introduces
# a behavior change that breaks the test test_reservation_method_w_sale
# Note: This should be the case
@api.onchange('expected_date')
def _onchange_expected_date(self):
self.commitment_date = self.expected_date
@@ -398,66 +406,77 @@ class SaleOrder(models.Model):
return self.env.ref('sale.mt_order_sent')
return super(SaleOrder, self)._track_subtype(init_values)
@api.onchange('partner_shipping_id', 'partner_id', 'company_id')
def onchange_partner_shipping_id(self):
@api.depends('partner_shipping_id', 'partner_id', 'company_id')
def _compute_fiscal_position_id(self):
"""
Trigger the change of fiscal position when the shipping address is modified.
"""
self.fiscal_position_id = self.env['account.fiscal.position'].with_company(self.company_id).get_fiscal_position(self.partner_id.id, self.partner_shipping_id.id)
return {}
cache = {}
for order in self:
if not order.partner_id:
order.fiscal_position_id = False
continue
key = (order.company_id.id, order.partner_id.id, order.partner_shipping_id.id)
if key not in cache:
cache[key] = self.env['account.fiscal.position'].with_company(
order.company_id
)._get_fiscal_position(order.partner_id, order.partner_shipping_id)
order.fiscal_position_id = cache[key]
@api.onchange('partner_id')
def onchange_partner_id(self):
"""
Update the following fields when the partner is changed:
- Pricelist
- Payment terms
- Invoice address
- Delivery address
- Sales Team
"""
if not self.partner_id:
self.update({
'partner_invoice_id': False,
'partner_shipping_id': False,
'fiscal_position_id': False,
})
@api.depends('partner_id')
def _compute_partner_invoice_id(self):
for order in self:
order.partner_invoice_id = order.partner_id.address_get(['invoice'])['invoice'] if order.partner_id else False
@api.depends('partner_id')
def _compute_partner_shipping_id(self):
for order in self:
order.partner_shipping_id = order.partner_id.address_get(['delivery'])['delivery'] if order.partner_id else False
@api.depends('partner_id')
def _compute_pricelist_id(self):
for order in self.filtered(lambda o: o.partner_id):
order = order.with_company(order.company_id)
order.pricelist_id = order.partner_id.property_product_pricelist
@api.depends('partner_id')
def _compute_note(self):
use_invoice_terms = self.env['ir.config_parameter'].sudo().get_param('account.use_invoice_terms')
if not use_invoice_terms:
return
self = self.with_company(self.company_id)
addr = self.partner_id.address_get(['delivery', 'invoice'])
partner_user = self.partner_id.user_id or self.partner_id.commercial_partner_id.user_id
values = {
'pricelist_id': self.partner_id.property_product_pricelist and self.partner_id.property_product_pricelist.id or False,
'payment_term_id': self.partner_id.property_payment_term_id and self.partner_id.property_payment_term_id.id or False,
'partner_invoice_id': addr['invoice'],
'partner_shipping_id': addr['delivery'],
}
user_id = partner_user.id
if not self.env.context.get('not_self_saleperson'):
user_id = user_id or self.env.context.get('default_user_id', self.env.uid)
if user_id and self.user_id.id != user_id:
values['user_id'] = user_id
if self.env['ir.config_parameter'].sudo().get_param('account.use_invoice_terms'):
if self.terms_type == 'html' and self.env.company.invoice_terms_html:
baseurl = html_keep_url(self.get_base_url() + '/terms')
values['note'] = _('Terms & Conditions: %s', baseurl)
for order in self:
order = order.with_company(order.company_id)
if order.terms_type == 'html' and self.env.company.invoice_terms_html:
baseurl = html_keep_url(order._get_note_url() + '/terms')
order.note = _('Terms & Conditions: %s', baseurl)
elif not is_html_empty(self.env.company.invoice_terms):
values['note'] = self.with_context(lang=self.partner_id.lang).env.company.invoice_terms
if not self.env.context.get('not_self_saleperson') or not self.team_id:
values['team_id'] = self.env['crm.team'].with_context(
default_team_id=self.partner_id.team_id.id
)._get_default_team_id(domain=['|', ('company_id', '=', self.company_id.id), ('company_id', '=', False)], user_id=user_id)
self.update(values)
order.note = order.with_context(lang=order.partner_id.lang).env.company.invoice_terms
@api.onchange('user_id')
def onchange_user_id(self):
if self.user_id:
self.team_id = self.env['crm.team'].with_context(
default_team_id=self.team_id.id
)._get_default_team_id(user_id=self.user_id.id, domain=None)
@api.depends('partner_id')
def _compute_payment_term_id(self):
for order in self:
order = order.with_company(order.company_id)
order.payment_term_id = order.partner_id.property_payment_term_id
@api.depends('partner_id')
def _compute_user_id(self):
for order in self:
order.user_id = order.partner_id.user_id or order.partner_id.commercial_partner_id.user_id or self.env.user
@api.depends('partner_id', 'user_id')
def _compute_team_id(self):
cached_teams = {}
for order in self:
default_team_id = (order.team_id or order.partner_id.team_id).id
user_id = order.user_id.id
company_id = order.company_id.id
key = (default_team_id, user_id, company_id)
if key not in cached_teams:
cached_teams[key] = self.env['crm.team'].with_context(
default_team_id=default_team_id
)._get_default_team_id(
user_id=user_id, domain=[('company_id', 'in', [company_id, False])])
order.team_id = cached_teams[key]
@api.onchange('partner_id')
def _onchange_partner_id_warning(self):
@@ -476,7 +495,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),
@@ -496,12 +514,10 @@ class SaleOrder(models.Model):
}
}
@api.onchange('pricelist_id', 'order_line')
def _onchange_pricelist_id(self):
if self.order_line and self.pricelist_id and self._origin.pricelist_id != self.pricelist_id:
self.show_update_pricelist = True
else:
self.show_update_pricelist = False
@api.depends('pricelist_id', 'order_line')
def _compute_show_update_pricelist(self):
for order in self:
order.show_update_pricelist = order.order_line and order.pricelist_id and order._origin.pricelist_id != self.pricelist_id
def update_prices(self):
self.ensure_one()
@@ -525,25 +541,19 @@ class SaleOrder(models.Model):
self.show_update_pricelist = False
self.message_post(body=_("Product prices have been recomputed according to pricelist <b>%s<b> ", self.pricelist_id.display_name))
@api.model
def create(self, vals):
if 'company_id' in vals:
self = self.with_company(vals['company_id'])
if vals.get('name', _('New')) == _('New'):
seq_date = None
if 'date_order' in vals:
seq_date = fields.Datetime.context_timestamp(self, fields.Datetime.to_datetime(vals['date_order']))
vals['name'] = self.env['ir.sequence'].next_by_code('sale.order', sequence_date=seq_date) or _('New')
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if 'company_id' in vals:
self = self.with_company(vals['company_id'])
if vals.get('name', _('New')) == _('New'):
seq_date = fields.Datetime.context_timestamp(
self, fields.Datetime.to_datetime(vals['date_order'])
) if 'date_order' in vals else None
vals['name'] = self.env['ir.sequence'].next_by_code(
'sale.order', sequence_date=seq_date) or _('New')
# Makes sure partner_invoice_id', 'partner_shipping_id' and 'pricelist_id' are defined
if any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id']):
partner = self.env['res.partner'].browse(vals.get('partner_id'))
addr = partner.address_get(['delivery', 'invoice'])
vals['partner_invoice_id'] = vals.setdefault('partner_invoice_id', addr['invoice'])
vals['partner_shipping_id'] = vals.setdefault('partner_shipping_id', addr['delivery'])
vals['pricelist_id'] = vals.setdefault('pricelist_id', partner.property_product_pricelist.id)
result = super(SaleOrder, self).create(vals)
return result
return super().create(vals_list)
def _compute_field_value(self, field):
if field.name == 'invoice_status' and not self.env.context.get('mail_activity_automation_skip'):
@@ -624,7 +634,7 @@ class SaleOrder(models.Model):
'team_id': self.team_id.id,
'partner_id': self.partner_invoice_id.id,
'partner_shipping_id': self.partner_shipping_id.id,
'fiscal_position_id': (self.fiscal_position_id or self.fiscal_position_id.get_fiscal_position(self.partner_invoice_id.id)).id,
'fiscal_position_id': (self.fiscal_position_id or self.fiscal_position_id._get_fiscal_position(self.partner_invoice_id)).id,
'partner_bank_id': self.company_id.partner_id.bank_ids[:1].id,
'journal_id': journal.id, # company comes from the journal
'invoice_origin': self.name,
@@ -1064,10 +1074,6 @@ class SaleOrder(models.Model):
self.ensure_one()
return self.transaction_ids._get_last()
@api.model
def _get_customer_lead(self, product_tmpl_id):
return False
def _get_report_base_filename(self):
self.ensure_one()
return '%s %s' % (self.type_name, self.name)
+242 -188
View File
@@ -23,7 +23,7 @@ class SaleOrderLine(models.Model):
- no: if the SO is not in status 'sale' or 'done', we consider that there is nothing to
invoice. This is also hte default value if the conditions of no other status is met.
- to invoice: we refer to the quantity to invoice of the line. Refer to method
`_get_to_invoice_qty()` for more information on how this quantity is calculated.
`_compute_qty_to_invoice()` for more information on how this quantity is calculated.
- upselling: this is possible only for a product invoiced on ordered quantities for which
we delivered more than expected. The could arise if, for example, a project took more
time than expected but we decided not to invoice the extra cost to the client. This
@@ -49,7 +49,10 @@ class SaleOrderLine(models.Model):
def _expected_date(self):
self.ensure_one()
order_date = fields.Datetime.from_string(self.order_id.date_order if self.order_id.date_order and self.order_id.state in ['sale', 'done'] else fields.Datetime.now())
if self.state in ['sale', 'done'] and self.order_id.date_order:
order_date = self.order_id.date_order
else:
order_date = fields.Datetime.now()
return order_date + timedelta(days=self.customer_lead or 0.0)
@api.depends('product_uom_qty', 'discount', 'price_unit', 'tax_id')
@@ -59,7 +62,7 @@ class SaleOrderLine(models.Model):
"""
for line in self:
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
taxes = line.tax_id.compute_all(price, line.order_id.currency_id, line.product_uom_qty, product=line.product_id, partner=line.order_id.partner_shipping_id)
taxes = line.tax_id.compute_all(price, line.currency_id, line.product_uom_qty, product=line.product_id, partner=line.order_id.partner_shipping_id)
line.update({
'price_tax': taxes['total_included'] - taxes['total_excluded'],
'price_total': taxes['total_included'],
@@ -68,7 +71,7 @@ class SaleOrderLine(models.Model):
if self.env.context.get('import_file', False) and not self.env.user.user_has_groups('account.group_account_manager'):
line.tax_id.invalidate_cache(['invoice_repartition_line_ids'], [line.tax_id.id])
@api.depends('product_id', 'order_id.state', 'qty_invoiced', 'qty_delivered')
@api.depends('product_id', 'state', 'qty_invoiced', 'qty_delivered')
def _compute_product_updatable(self):
for line in self:
if line.state in ['done', 'cancel'] or (line.state == 'sale' and (line.qty_invoiced > 0 or line.qty_delivered > 0)):
@@ -77,14 +80,14 @@ class SaleOrderLine(models.Model):
line.product_updatable = True
# no trigger product_id.invoice_policy to avoid retroactively changing SO
@api.depends('qty_invoiced', 'qty_delivered', 'product_uom_qty', 'order_id.state')
def _get_to_invoice_qty(self):
@api.depends('qty_invoiced', 'qty_delivered', 'product_uom_qty', 'state')
def _compute_qty_to_invoice(self):
"""
Compute the quantity to invoice. If the invoice policy is order, the quantity to invoice is
calculated from the ordered quantity. Otherwise, the quantity delivered is used.
"""
for line in self:
if line.order_id.state in ['sale', 'done']:
if line.state in ['sale', 'done']:
if line.product_id.invoice_policy == 'order':
line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced
else:
@@ -134,38 +137,23 @@ class SaleOrderLine(models.Model):
for line in self:
line.price_reduce_taxexcl = line.price_subtotal / line.product_uom_qty if line.product_uom_qty else 0.0
@api.depends('product_id')
def _compute_tax_id(self):
for line in self:
line = line.with_company(line.company_id)
fpos = line.order_id.fiscal_position_id or line.order_id.fiscal_position_id.get_fiscal_position(line.order_partner_id.id)
# If company_id is set, always filter taxes by the company
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
if not line.product_id or not taxes:
# Nothing to map
line.tax_id = False
continue
# If company_id is set, always filter taxes by the company
line.tax_id = line.order_id.fiscal_position_id.map_tax(taxes)
@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':
if line.product_id and line.state == 'sale':
msg = _("Extra line with %s", line.product_id.display_name)
line.order_id.message_post(body=msg)
# create an analytic account if at least an expense product
@@ -211,7 +199,7 @@ class SaleOrderLine(models.Model):
# Prevent writing on a locked SO.
protected_fields = self._get_protected_fields()
if 'done' in self.mapped('order_id.state') and any(f in values.keys() for f in protected_fields):
if 'done' in self.mapped('state') and any(f in values.keys() for f in protected_fields):
protected_fields_modified = list(set(protected_fields) & set(values.keys()))
fields = self.env['ir.model.fields'].search([
('name', 'in', protected_fields_modified), ('model', '=', self._name)
@@ -225,7 +213,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, precompute=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)
@@ -234,19 +224,30 @@ class SaleOrderLine(models.Model):
('invoiced', 'Fully Invoiced'),
('to invoice', 'To Invoice'),
('no', 'Nothing to Invoice')
], string='Invoice Status', compute='_compute_invoice_status', store=True, default='no')
price_unit = fields.Float('Unit Price', required=True, digits='Product Price', default=0.0)
], string='Invoice Status', compute='_compute_invoice_status', store=True)
price_unit = fields.Float(
'Unit Price', required=True, digits='Product Price',
compute='_compute_price_unit', store=True, readonly=False, precompute=True)
price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', store=True)
price_tax = fields.Float(compute='_compute_amount', string='Total Tax', store=True)
price_total = fields.Monetary(compute='_compute_amount', string='Total', store=True)
price_reduce = fields.Float(
string='Price Reduce', digits='Product Price',
compute='_compute_price_reduce', store=True, precompute=True)
price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', store=True, precompute=True)
price_tax = fields.Float(compute='_compute_amount', string='Total Tax', store=True, precompute=True)
price_total = fields.Monetary(compute='_compute_amount', string='Total', store=True, precompute=True)
price_reduce_taxinc = fields.Monetary(
compute='_compute_price_reduce_taxinc', string='Price Reduce Tax inc', store=True, precompute=True)
price_reduce_taxexcl = fields.Monetary(
compute='_compute_price_reduce_taxexcl', string='Price Reduce Tax excl', store=True, precompute=True)
price_reduce = fields.Float(compute='_compute_price_reduce', string='Price Reduce', digits='Product Price', store=True)
tax_id = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)])
price_reduce_taxinc = fields.Monetary(compute='_compute_price_reduce_taxinc', string='Price Reduce Tax inc', store=True)
price_reduce_taxexcl = fields.Monetary(compute='_compute_price_reduce_taxexcl', string='Price Reduce Tax excl', store=True)
tax_id = fields.Many2many(
'account.tax', string='Taxes',
compute='_compute_tax_id', store=True, readonly=False, precompute=True,
domain=['|', ('active', '=', False), ('active', '=', True)])
discount = fields.Float(string='Discount (%)', digits='Discount', default=0.0)
discount = fields.Float(
string='Discount (%)', digits='Discount',
compute='_compute_discount', store=True, readonly=False, precompute=True)
product_id = fields.Many2one(
'product.product', string='Product', domain="[('sale_ok', '=', True), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
@@ -254,42 +255,54 @@ class SaleOrderLine(models.Model):
product_template_id = fields.Many2one(
'product.template', string='Product Template',
related="product_id.product_tmpl_id", domain=[('sale_ok', '=', True)])
product_updatable = fields.Boolean(compute='_compute_product_updatable', string='Can Edit Product', default=True)
product_uom_qty = fields.Float(string='Quantity', digits='Product Unit of Measure', required=True, default=1.0)
product_uom = fields.Many2one('uom.uom', string='Unit of Measure', domain="[('category_id', '=', product_uom_category_id)]", ondelete="restrict")
product_updatable = fields.Boolean(compute='_compute_product_updatable', string='Can Edit Product')
product_uom_qty = fields.Float(
string='Quantity', digits='Product Unit of Measure', required=True,
compute='_compute_product_uom_qty', store=True, readonly=False, precompute=True)
product_uom = fields.Many2one(
'uom.uom', string='Unit of Measure',
compute='_compute_product_uom', store=True, readonly=False, precompute=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, precompute=True)
# 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, precompute=True)
qty_delivered_method = fields.Selection([
('manual', 'Manual'),
('analytic', 'Analytic From Expenses')
], string="Method to update delivered qty", compute='_compute_qty_delivered_method', store=True,
qty_delivered_method = fields.Selection(
[('manual', 'Manual'),
('analytic', 'Analytic From Expenses')], string="Method to update delivered qty",
compute='_compute_qty_delivered_method', store=True, precompute=True,
help="According to product configuration, the delivered quantity can be automatically computed by mechanism :\n"
" - Manual: the quantity is set manually on the line\n"
" - Analytic From expenses: the quantity is the quantity sum from posted expenses\n"
" - Timesheet: the quantity is the sum of hours recorded on tasks linked to this sale line\n"
" - Stock Moves: the quantity comes from confirmed pickings\n")
qty_delivered = fields.Float('Delivered Quantity', copy=False, compute='_compute_qty_delivered', inverse='_inverse_qty_delivered', store=True, digits='Product Unit of Measure', default=0.0)
qty_delivered_manual = fields.Float('Delivered Manually', copy=False, digits='Product Unit of Measure', default=0.0)
qty_delivered = fields.Float(
'Delivered Quantity', copy=False,
compute='_compute_qty_delivered', store=True, readonly=False,
digits='Product Unit of Measure')
qty_to_invoice = fields.Float(
compute='_get_to_invoice_qty', string='To Invoice Quantity', store=True,
compute='_compute_qty_to_invoice', string='To Invoice Quantity', store=True,
digits='Product Unit of Measure')
qty_invoiced = fields.Float(
compute='_compute_qty_invoiced', string='Invoiced Quantity', store=True,
digits='Product Unit of Measure')
untaxed_amount_invoiced = fields.Monetary("Untaxed Invoiced Amount", compute='_compute_untaxed_amount_invoiced', store=True)
untaxed_amount_to_invoice = fields.Monetary("Untaxed Amount To Invoice", compute='_compute_untaxed_amount_to_invoice', store=True)
untaxed_amount_invoiced = fields.Monetary(
"Untaxed Invoiced Amount",
compute='_compute_untaxed_amount_invoiced', store=True)
untaxed_amount_to_invoice = fields.Monetary(
"Untaxed Amount To Invoice",
compute='_compute_untaxed_amount_to_invoice', store=True)
salesman_id = fields.Many2one(related='order_id.user_id', store=True, string='Salesperson')
currency_id = fields.Many2one(related='order_id.currency_id', depends=['order_id.currency_id'], store=True, string='Currency')
company_id = fields.Many2one(related='order_id.company_id', string='Company', store=True, index=True)
order_partner_id = fields.Many2one(related='order_id.partner_id', store=True, string='Customer')
analytic_tag_ids = fields.Many2many(
'account.analytic.tag', string='Analytic Tags',
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
@@ -299,19 +312,40 @@ class SaleOrderLine(models.Model):
string="Is a down payment", help="Down payments are made when creating invoices from a sales order."
" They are not copied when duplicating a sales order.")
# Related stored fields from the order
currency_id = fields.Many2one(
related='order_id.currency_id', depends=['order_id.currency_id'],
store=True, string='Currency', precompute=True)
company_id = fields.Many2one(related='order_id.company_id', string='Company', store=True, index=True, precompute=True)
order_partner_id = fields.Many2one(related='order_id.partner_id', store=True, string='Customer', precompute=True)
salesman_id = fields.Many2one(related='order_id.user_id', store=True, string='Salesperson', precompute=True)
state = fields.Selection(
related='order_id.state', string='Order Status', copy=False, store=True)
related='order_id.state', string='Order Status', copy=False, store=True, precompute=True)
customer_lead = fields.Float(
'Lead Time', required=True, default=0.0,
'Lead Time', required=True,
compute="_compute_customer_lead", store=True, readonly=False, precompute=True,
help="Number of days between the order confirmation and the shipping of the products to the customer")
display_type = fields.Selection([
('line_section', "Section"),
('line_note', "Note")], default=False, help="Technical field for UX purpose.")
product_packaging_id = fields.Many2one('product.packaging', string='Packaging', default=False, domain="[('sales', '=', True), ('product_id','=',product_id)]", check_company=True)
product_packaging_qty = fields.Float('Packaging Quantity')
product_packaging_id = fields.Many2one(
'product.packaging', string='Packaging',
compute='_compute_product_packaging_id', store=True, readonly=False, precompute=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, precompute=True)
# This computed default is necessary
# because the ORM doesn't provide a way to remove a field default on inheritance
# if a default is specified in sale, it disables the compute in sale_stock
# when a SOL is created with a product but without customer_lead values.
def _compute_customer_lead(self):
self.customer_lead = 0.0
@api.depends('state')
def _compute_product_uom_readonly(self):
@@ -334,7 +368,11 @@ class SaleOrderLine(models.Model):
else: # service and consu
line.qty_delivered_method = 'manual'
@api.depends('qty_delivered_method', 'qty_delivered_manual', 'analytic_line_ids.so_line', 'analytic_line_ids.unit_amount', 'analytic_line_ids.product_uom_id')
@api.depends(
'qty_delivered_method',
'analytic_line_ids.so_line',
'analytic_line_ids.unit_amount',
'analytic_line_ids.product_uom_id')
def _compute_qty_delivered(self):
""" This method compute the delivered quantity of the SO lines: it covers the case provide by sale module, aka
expense/vendor bills (sum of unit_amount of AAL), and manual case.
@@ -347,10 +385,6 @@ class SaleOrderLine(models.Model):
mapping = lines_by_analytic._get_delivered_quantity_by_analytic([('amount', '<=', 0.0)])
for so_line in lines_by_analytic:
so_line.qty_delivered = mapping.get(so_line.id or so_line._origin.id, 0.0)
# compute for manual lines
for line in self:
if line.qty_delivered_method == 'manual':
line.qty_delivered = line.qty_delivered_manual or 0.0
def _get_delivered_quantity_by_analytic(self, additional_domain):
""" Compute and write the delivered quantity of current SO lines, based on their related
@@ -391,26 +425,16 @@ class SaleOrderLine(models.Model):
return result
@api.onchange('qty_delivered')
def _inverse_qty_delivered(self):
""" When writing on qty_delivered, if the value should be modify manually (`qty_delivered_method` = 'manual' only),
then we put the value in `qty_delivered_manual`. Otherwise, `qty_delivered_manual` should be False since the
delivered qty is automatically compute by other mecanisms.
"""
@api.depends('product_id', 'product_uom_qty', 'product_uom')
def _compute_product_packaging_id(self):
for line in self:
if line.qty_delivered_method == 'manual':
line.qty_delivered_manual = line.qty_delivered
else:
line.qty_delivered_manual = 0.0
@api.onchange('product_id', 'product_uom_qty', 'product_uom')
def _onchange_suggest_packaging(self):
# remove packaging if not match the product
if self.product_packaging_id.product_id != self.product_id:
self.product_packaging_id = False
# suggest biggest suitable packaging
if self.product_id and self.product_uom_qty and self.product_uom:
self.product_packaging_id = self.product_id.packaging_ids.filtered('sales')._find_suitable_product_packaging(self.product_uom_qty, self.product_uom)
# remove packaging if not match the product
if line.product_packaging_id.product_id != line.product_id:
line.product_packaging_id = False
# Find biggest suitable packaging
if line.product_id and line.product_uom_qty and line.product_uom:
line.product_packaging_id = line.product_id.packaging_ids.filtered(
'sales')._find_suitable_product_packaging(line.product_uom_qty, line.product_uom)
@api.onchange('product_packaging_id')
def _onchange_product_packaging_id(self):
@@ -430,23 +454,41 @@ class SaleOrderLine(models.Model):
},
}
@api.onchange('product_packaging_id', 'product_uom', 'product_uom_qty')
def _onchange_update_product_packaging_qty(self):
if not self.product_packaging_id:
self.product_packaging_qty = False
else:
packaging_uom = self.product_packaging_id.product_uom_id
packaging_uom_qty = self.product_uom._compute_quantity(self.product_uom_qty, packaging_uom)
self.product_packaging_qty = float_round(packaging_uom_qty / self.product_packaging_id.qty, precision_rounding=packaging_uom.rounding)
@api.depends('product_packaging_id', 'product_uom', 'product_uom_qty')
def _compute_product_packaging_qty(self):
for line in self:
if not line.product_packaging_id:
line.product_packaging_qty = False
else:
packaging_uom = line.product_packaging_id.product_uom_id
packaging_uom_qty = line.product_uom._compute_quantity(line.product_uom_qty, packaging_uom)
line.product_packaging_qty = float_round(
packaging_uom_qty / line.product_packaging_id.qty,
precision_rounding=packaging_uom.rounding)
@api.onchange('product_packaging_qty')
def _onchange_product_packaging_qty(self):
if self.product_packaging_id:
packaging_uom = self.product_packaging_id.product_uom_id
qty_per_packaging = self.product_packaging_id.qty
product_uom_qty = packaging_uom._compute_quantity(self.product_packaging_qty * qty_per_packaging, self.product_uom)
if float_compare(product_uom_qty, self.product_uom_qty, precision_rounding=self.product_uom.rounding) != 0:
self.product_uom_qty = product_uom_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('display_type', 'product_id', 'product_packaging_qty')
def _compute_product_uom_qty(self):
for line in self:
if line.display_type:
line.product_uom_qty = 0.0
continue
# Default value = 1.0
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
qty_per_packaging = line.product_packaging_id.qty
product_uom_qty = packaging_uom._compute_quantity(
line.product_packaging_qty * qty_per_packaging, line.product_uom)
if float_compare(product_uom_qty, line.product_uom_qty, precision_rounding=line.product_uom.rounding) != 0:
line.product_uom_qty = product_uom_qty
@api.depends('invoice_lines', 'invoice_lines.price_total', 'invoice_lines.move_id.state', 'invoice_lines.move_id.move_type')
def _compute_untaxed_amount_invoiced(self):
@@ -495,7 +537,7 @@ class SaleOrderLine(models.Model):
# `price_reduce_taxexcl` cannot be used as it is computed from `price_subtotal` field. (see upper Note)
price_subtotal = line.tax_id.compute_all(
price_reduce,
currency=line.order_id.currency_id,
currency=line.currency_id,
quantity=uom_qty_to_consider,
product=line.product_id,
partner=line.order_id.partner_shipping_id)['total_excluded']
@@ -595,43 +637,43 @@ 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=line.order_partner_id.lang)
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:
line.product_custom_attribute_value_ids = False
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:
line.product_no_variant_attribute_value_ids = False
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):
def _onchange_product_id_warning(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))
self._compute_tax_id()
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
@@ -643,22 +685,25 @@ class SaleOrderLine(models.Model):
}
}
@api.onchange('product_uom', 'product_uom_qty')
def product_uom_change(self):
if not self.product_uom or not self.product_id:
self.price_unit = 0.0
return
if self.order_id.pricelist_id and self.order_id.partner_id:
product = self.product_id.with_context(
lang=self.order_id.partner_id.lang,
partner=self.order_id.partner_id,
quantity=self.product_uom_qty,
date=self.order_id.date_order,
pricelist=self.order_id.pricelist_id.id,
uom=self.product_uom.id,
fiscal_position=self.env.context.get('fiscal_position')
)
self.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)
@api.depends('product_id', 'product_uom', 'product_uom_qty')
def _compute_price_unit(self):
for line in self:
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_partner_id:
product = line.product_id.with_context(
partner=line.order_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,
fiscal_position=self.env.context.get('fiscal_position')
)
line.price_unit = self.env['account.tax']._fix_tax_included_price_company(
line._get_display_price(product), product.taxes_id, line.tax_id, line.company_id)
else:
line.price_unit = 0.0
def name_get(self):
result = []
@@ -745,42 +790,51 @@ class SaleOrderLine(models.Model):
'tax_id', 'analytic_tag_ids'
]
def _onchange_product_id_set_customer_lead(self):
pass
@api.onchange('product_id', 'price_unit', 'product_uom', 'product_uom_qty', 'tax_id')
def _onchange_discount(self):
if not (self.product_id and self.product_uom and
self.order_id.partner_id and self.order_id.pricelist_id and
self.order_id.pricelist_id.discount_policy == 'without_discount' and
self.env.user.has_group('product.group_discount_per_so_line')):
@api.depends('product_id', 'price_unit', 'product_uom', 'product_uom_qty', 'tax_id')
def _compute_discount(self):
if not self.env.user.has_group('product.group_discount_per_so_line'):
# Do not compute discounts if the feature is not enabled.
return
self.discount = 0.0
product = self.product_id.with_context(
lang=self.order_id.partner_id.lang,
partner=self.order_id.partner_id,
quantity=self.product_uom_qty,
date=self.order_id.date_order,
pricelist=self.order_id.pricelist_id.id,
uom=self.product_uom.id,
fiscal_position=self.env.context.get('fiscal_position')
)
for line in self:
if not (line.product_id and line.product_uom and
line.order_partner_id and line.order_id.pricelist_id and
line.order_id.pricelist_id.discount_policy == 'without_discount'):
continue
product_context = dict(self.env.context, partner_id=self.order_id.partner_id.id, date=self.order_id.date_order, uom=self.product_uom.id)
line.discount = 0.0
product = line.product_id.with_context(
partner=line.order_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,
fiscal_position=self.env.context.get('fiscal_position'))
price, rule_id = self.order_id.pricelist_id.with_context(product_context).get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id)
new_list_price, currency = self.with_context(product_context)._get_real_price_currency(product, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id)
product_context = dict(
self.env.context,
partner_id=line.order_partner_id.id,
date=line.order_id.date_order,
uom=line.product_uom.id)
if new_list_price != 0:
if self.order_id.pricelist_id.currency_id != currency:
# we need new_list_price in the same currency as price, which is in the SO's pricelist's currency
new_list_price = currency._convert(
new_list_price, self.order_id.pricelist_id.currency_id,
self.order_id.company_id or self.env.company, self.order_id.date_order or fields.Date.today())
discount = (new_list_price - price) / new_list_price * 100
if (discount > 0 and new_list_price > 0) or (discount < 0 and new_list_price < 0):
self.discount = discount
price, rule_id = line.order_id.pricelist_id.with_context(product_context).get_product_price_rule(
line.product_id,
line.product_uom_qty or 1.0,
line.order_partner_id)
new_list_price, currency = line.with_context(product_context)._get_real_price_currency(
product, rule_id, line.product_uom_qty,
line.product_uom, line.order_id.pricelist_id.id)
if new_list_price != 0:
if line.order_id.pricelist_id.currency_id != currency:
# we need new_list_price in the same currency as price, which is in the SO's pricelist's currency
new_list_price = currency._convert(
new_list_price, line.order_id.pricelist_id.currency_id,
line.order_id.company_id or self.env.company,
line.order_id.date_order or fields.Date.today())
discount = (new_list_price - price) / new_list_price * 100
if (discount > 0 and new_list_price > 0) or (discount < 0 and new_list_price < 0):
line.discount = discount
def _is_delivery(self):
self.ensure_one()
@@ -817,13 +871,13 @@ class SaleOrderLine(models.Model):
# display the no_variant attributes, except those that are also
# displayed by a custom (avoid duplicate description)
for ptav in (no_variant_ptavs - custom_ptavs):
name += "\n" + ptav.with_context(lang=self.order_id.partner_id.lang).display_name
name += "\n" + ptav.with_context(lang=self.order_partner_id.lang).display_name
# Sort the values according to _order settings, because it doesn't work for virtual records in onchange
custom_values = sorted(self.product_custom_attribute_value_ids, key=lambda r: (r.custom_product_template_attribute_value_id.id, r.id))
# display the is_custom values
for pacv in custom_values:
name += "\n" + pacv.with_context(lang=self.order_id.partner_id.lang).display_name
name += "\n" + pacv.with_context(lang=self.order_partner_id.lang).display_name
return name
+4 -3
View File
@@ -76,9 +76,10 @@ class SaleOrderLine(models.Model):
product_ids = self.env.registry.populated_models["product.product"]
# If we want more advanced products with multiple variants
# add a populate dependency on product template and the following lines
product_ids += self.env["product.product"].search([
('product_tmpl_id', 'in', self.env.registry.populated_models["product.template"])
]).ids
if 'product.template' in self.env.registry.populated_models:
product_ids += self.env["product.product"].search([
('product_tmpl_id', 'in', self.env.registry.populated_models["product.template"])
]).ids
self.env['product.product'].browse(product_ids).read(['uom_id']) # prefetch all uom_id
+8 -7
View File
@@ -1,16 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_sale_product_attribute_value_config
from . import test_sale_to_invoice
from . import test_sale_order
from . import test_sale_pricelist
from . import test_access_rights
from . import test_accrued_sale_orders
from . import test_onchange
from . import test_payment_flow
from . import test_performance
from . import test_reinvoice
from . import test_access_rights
from . import test_sale_flow
from . import test_sale_order
from . import test_sale_pricelist
from . import test_sale_product_attribute_value_config
from . import test_sale_refund
from . import test_sale_signature
from . import test_sale_flow
from . import test_accrued_sale_orders
from . import test_sale_tax_totals
from . import test_sale_to_invoice
+5 -276
View File
@@ -1,282 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import Form
from odoo.tests.common import TransactionCase
from odoo.tests import tagged, TransactionCase
class TestOnchangeProductId(TransactionCase):
"""Test that when an included tax is mapped by a fiscal position, the included tax must be
subtracted to the price of the product.
"""
def setUp(self):
super(TestOnchangeProductId, self).setUp()
self.fiscal_position_model = self.env['account.fiscal.position']
self.fiscal_position_tax_model = self.env['account.fiscal.position.tax']
self.tax_model = self.env['account.tax']
self.so_model = self.env['sale.order']
self.po_line_model = self.env['sale.order.line']
self.res_partner_model = self.env['res.partner']
self.product_tmpl_model = self.env['product.template']
self.product_model = self.env['product.product']
self.product_uom_model = self.env['uom.uom']
self.pricelist_model = self.env['product.pricelist']
def test_onchange_product_id(self):
uom_id = self.product_uom_model.search([('name', '=', 'Units')])[0]
pricelist = self.pricelist_model.search([('name', '=', 'Public Pricelist')])[0]
partner_id = self.res_partner_model.create(dict(name="George"))
tax_include_id = self.tax_model.create(dict(name="Include tax",
amount='21.00',
price_include=True,
type_tax_use='sale'))
tax_exclude_id = self.tax_model.create(dict(name="Exclude tax",
amount='0.00',
type_tax_use='sale'))
product_tmpl_id = self.product_tmpl_model.create(dict(name="Voiture",
list_price=121,
taxes_id=[(6, 0, [tax_include_id.id])]))
product_id = product_tmpl_id.product_variant_id
fp_id = self.fiscal_position_model.create(dict(name="fiscal position", sequence=1))
fp_tax_id = self.fiscal_position_tax_model.create(dict(position_id=fp_id.id,
tax_src_id=tax_include_id.id,
tax_dest_id=tax_exclude_id.id))
# Create the SO with one SO line and apply a pricelist and fiscal position on it
order_form = Form(self.env['sale.order'].with_context(tracking_disable=True))
order_form.partner_id = partner_id
order_form.pricelist_id = pricelist
order_form.fiscal_position_id = fp_id
with order_form.order_line.new() as line:
line.name = product_id.name
line.product_id = product_id
line.product_uom_qty = 1.0
line.product_uom = uom_id
sale_order = order_form.save()
# Check the unit price of SO line
self.assertEqual(100, sale_order.order_line[0].price_unit, "The included tax must be subtracted to the price")
def test_pricelist_application(self):
""" Test different prices are correctly applied based on dates """
support_product = self.env['product.product'].create({
'name': 'Virtual Home Staging',
'list_price': 100,
})
partner = self.res_partner_model.create(dict(name="George"))
christmas_pricelist = self.env['product.pricelist'].create({
'name': 'Christmas pricelist',
'item_ids': [(0, 0, {
'date_start': "2017-12-01",
'date_end': "2017-12-24",
'compute_price': 'percentage',
'base': 'list_price',
'percent_price': 20,
'applied_on': '3_global',
'name': 'Pre-Christmas discount'
}), (0, 0, {
'date_start': "2017-12-25",
'date_end': "2017-12-31",
'compute_price': 'percentage',
'base': 'list_price',
'percent_price': 50,
'applied_on': '3_global',
'name': 'Post-Christmas super-discount'
})]
})
# Create the SO with pricelist based on date
order_form = Form(self.env['sale.order'].with_context(tracking_disable=True))
order_form.partner_id = partner
order_form.date_order = '2017-12-20'
order_form.pricelist_id = christmas_pricelist
with order_form.order_line.new() as line:
line.product_id = support_product
so = order_form.save()
# Check the unit price and subtotal of SO line
self.assertEqual(so.order_line[0].price_unit, 80, "First date pricelist rule not applied")
self.assertEqual(so.order_line[0].price_subtotal, so.order_line[0].price_unit * so.order_line[0].product_uom_qty, 'Total of SO line should be a multiplication of unit price and ordered quantity')
# Change order date of the SO and check the unit price and subtotal of SO line
with Form(so) as order:
order.date_order = '2017-12-30'
with order.order_line.edit(0) as line:
line.product_id = support_product
self.assertEqual(so.order_line[0].price_unit, 50, "Second date pricelist rule not applied")
self.assertEqual(so.order_line[0].price_subtotal, so.order_line[0].price_unit * so.order_line[0].product_uom_qty, 'Total of SO line should be a multiplication of unit price and ordered quantity')
def test_pricelist_uom_discount(self):
""" Test prices and discounts are correctly applied based on date and uom"""
computer_case = self.env['product.product'].create({
'name': 'Drawer Black',
'list_price': 100,
})
partner = self.res_partner_model.create(dict(name="George"))
categ_unit_id = self.ref('uom.product_uom_categ_unit')
goup_discount_id = self.ref('product.group_discount_per_so_line')
self.env.user.write({'groups_id': [(4, goup_discount_id, 0)]})
new_uom = self.env['uom.uom'].create({
'name': '10 units',
'factor_inv': 10,
'uom_type': 'bigger',
'rounding': 1.0,
'category_id': categ_unit_id
})
christmas_pricelist = self.env['product.pricelist'].create({
'name': 'Christmas pricelist',
'discount_policy': 'without_discount',
'item_ids': [(0, 0, {
'date_start': "2017-12-01",
'date_end': "2017-12-30",
'compute_price': 'percentage',
'base': 'list_price',
'percent_price': 10,
'applied_on': '3_global',
'name': 'Christmas discount'
})]
})
so = self.env['sale.order'].create({
'partner_id': partner.id,
'date_order': '2017-12-20',
'pricelist_id': christmas_pricelist.id,
})
order_line = self.env['sale.order.line'].new({
'order_id': so.id,
'product_id': computer_case.id,
})
# force compute uom and prices
order_line.product_id_change()
order_line.product_uom_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%")
order_line.product_uom = new_uom
order_line.product_uom_change()
order_line._onchange_discount()
self.assertEqual(order_line.price_subtotal, 900, "Christmas discount pricelist rule not applied")
self.assertEqual(order_line.discount, 10, "Christmas discount not equalt to 10%")
def test_pricelist_based_on_other(self):
""" Test price and discount are correctly applied with a pricelist based on an other one"""
computer_case = self.env['product.product'].create({
'name': 'Drawer Black',
'list_price': 100,
})
partner = self.res_partner_model.create(dict(name="George"))
goup_discount_id = self.ref('product.group_discount_per_so_line')
self.env.user.write({'groups_id': [(4, goup_discount_id, 0)]})
first_pricelist = self.env['product.pricelist'].create({
'name': 'First pricelist',
'discount_policy': 'without_discount',
'item_ids': [(0, 0, {
'compute_price': 'percentage',
'base': 'list_price',
'percent_price': 10,
'applied_on': '3_global',
'name': 'First discount'
})]
})
second_pricelist = self.env['product.pricelist'].create({
'name': 'Second pricelist',
'discount_policy': 'without_discount',
'item_ids': [(0, 0, {
'compute_price': 'formula',
'base': 'pricelist',
'base_pricelist_id': first_pricelist.id,
'price_discount': 10,
'applied_on': '3_global',
'name': 'Second discount'
})]
})
so = self.env['sale.order'].create({
'partner_id': partner.id,
'date_order': '2018-07-11',
'pricelist_id': second_pricelist.id,
})
order_line = self.env['sale.order.line'].new({
'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")
def test_pricelist_with_other_currency(self):
""" Test prices are correctly applied with a pricelist with an other currency"""
computer_case = self.env['product.product'].create({
'name': 'Drawer Black',
'list_price': 100,
})
computer_case.list_price = 100
partner = self.res_partner_model.create(dict(name="George"))
categ_unit_id = self.ref('uom.product_uom_categ_unit')
other_currency = self.env['res.currency'].create({'name': 'other currency',
'symbol': 'other'})
self.env['res.currency.rate'].create({'name': '2018-07-11',
'rate': 2.0,
'currency_id': other_currency.id,
'company_id': self.env.company.id})
self.env['res.currency.rate'].search(
[('currency_id', '=', self.env.company.currency_id.id)]
).unlink()
new_uom = self.env['uom.uom'].create({
'name': '10 units',
'factor_inv': 10,
'uom_type': 'bigger',
'rounding': 1.0,
'category_id': categ_unit_id
})
# This pricelist doesn't show the discount
first_pricelist = self.env['product.pricelist'].create({
'name': 'First pricelist',
'currency_id': other_currency.id,
'discount_policy': 'with_discount',
'item_ids': [(0, 0, {
'compute_price': 'percentage',
'base': 'list_price',
'percent_price': 10,
'applied_on': '3_global',
'name': 'First discount'
})]
})
so = self.env['sale.order'].create({
'partner_id': partner.id,
'date_order': '2018-07-12',
'pricelist_id': first_pricelist.id,
})
order_line = self.env['sale.order.line'].new({
'order_id': so.id,
'product_id': computer_case.id,
})
# 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
order_line.product_uom_change()
self.assertEqual(order_line.price_unit, 1800, "First pricelist rule not applied")
@tagged('post_install', '-at_install')
class TestSaleOnchanges(TransactionCase):
def test_sale_warnings(self):
"""Test warnings & SO/SOL updates when partner/products with sale warnings are used."""
@@ -320,7 +49,7 @@ class TestOnchangeProductId(TransactionCase):
'order_id': sale_order.id,
'product_id': product_with_warning.id,
})
warning = sale_order_line.product_id_change()
warning = sale_order_line._onchange_product_id_warning()
self.assertDictEqual(warning, {
'warning': {
'title': "Warning for Test Product",
@@ -329,7 +58,7 @@ class TestOnchangeProductId(TransactionCase):
})
sale_order_line.product_id = product_with_block_warning
warning = sale_order_line.product_id_change()
warning = sale_order_line._onchange_product_id_warning()
self.assertDictEqual(warning, {
'warning': {
+15
View File
@@ -0,0 +1,15 @@
from odoo.tests import tagged
from odoo.addons.base.tests.common import PerformanceCommon
@tagged('post_install', '-at_install')
class TestSalePerformance(PerformanceCommon):
def test_batch_creation_sale_orders(self):
"""Enforce sale.order create overrides always support batch creation"""
self.assertModelCreateMulti('sale.order')
def test_batch_creation_sale_order_line(self):
"""Enforce sale.order.line create overrides always support batch creation"""
self.assertModelCreateMulti('sale.order.line')
-35
View File
@@ -38,28 +38,18 @@ class TestReInvoice(TestSaleCommon):
""" Test vendor bill at cost for product based on ordered and delivered quantities. """
# create SO line and confirm SO (with only one line)
sale_order_line1 = self.env['sale.order.line'].create({
'name': self.company_data['product_order_cost'].name,
'product_id': self.company_data['product_order_cost'].id,
'product_uom_qty': 2,
'qty_delivered': 1,
'product_uom': self.company_data['product_order_cost'].uom_id.id,
'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,
'product_uom_qty': 4,
'qty_delivered': 1,
'product_uom': self.company_data['product_delivery_cost'].uom_id.id,
'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.onchange_partner_id()
self.sale_order._compute_tax_id()
self.sale_order.action_confirm()
# create invoice lines and validate it
@@ -158,26 +148,17 @@ class TestReInvoice(TestSaleCommon):
"""
# create SO line and confirm SO (with only one line)
sale_order_line1 = 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,
'product_uom_qty': 2,
'qty_delivered': 1,
'product_uom': self.company_data['product_delivery_sales_price'].uom_id.id,
'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,
'product_uom_qty': 3,
'qty_delivered': 1,
'product_uom': self.company_data['product_order_sales_price'].uom_id.id,
'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._compute_tax_id()
self.sale_order.action_confirm()
# create invoice lines and validate it
@@ -237,15 +218,11 @@ class TestReInvoice(TestSaleCommon):
""" Test invoicing vendor bill with no policy. Check nothing happen. """
# confirm SO
sale_order_line = self.env['sale.order.line'].create({
'name': self.company_data['product_delivery_no'].name,
'product_id': self.company_data['product_delivery_no'].id,
'product_uom_qty': 2,
'qty_delivered': 1,
'product_uom': self.company_data['product_delivery_no'].uom_id.id,
'price_unit': self.company_data['product_delivery_no'].list_price,
'order_id': self.sale_order.id,
})
self.sale_order._compute_tax_id()
self.sale_order.action_confirm()
# create invoice lines and validate it
@@ -264,28 +241,16 @@ class TestReInvoice(TestSaleCommon):
def test_not_reinvoicing_invoiced_so_lines(self):
""" Test that invoiced SO lines are not re-invoiced. """
so_line1 = self.env['sale.order.line'].create({
'name': self.company_data['product_delivery_cost'].name,
'product_id': self.company_data['product_delivery_cost'].id,
'product_uom_qty': 1,
'product_uom': self.company_data['product_delivery_cost'].uom_id.id,
'price_unit': self.company_data['product_delivery_cost'].list_price,
'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,
'product_uom_qty': 1,
'product_uom': self.company_data['product_delivery_sales_price'].uom_id.id,
'price_unit': self.company_data['product_delivery_sales_price'].list_price,
'discount': 100.00,
'order_id': self.sale_order.id,
})
so_line2.product_id_change()
self.sale_order.onchange_partner_id()
self.sale_order._compute_tax_id()
self.sale_order.action_confirm()
for line in self.sale_order.order_line:
-4
View File
@@ -69,11 +69,7 @@ class TestSaleFlow(TestSaleCommonBase):
}),
],
})
for line in sale_order.order_line:
line.product_id_change()
sale_order.onchange_partner_id()
sale_order._compute_tax_id()
sale_order.action_confirm()
self.assertRecordValues(sale_order.order_line, [
+100 -30
View File
@@ -4,6 +4,7 @@ from datetime import timedelta
from freezegun import freeze_time
from odoo import fields
from odoo.fields import Command
from odoo.exceptions import UserError, AccessError
from odoo.tests import tagged, Form
from odoo.tools import float_compare
@@ -49,42 +50,84 @@ class TestSaleOrder(TestSaleCommon):
'pricelist_id': cls.company_data['default_pricelist'].id,
})
cls.sol_product_order = cls.env['sale.order.line'].create({
'name': cls.company_data['product_order_no'].name,
'product_id': cls.company_data['product_order_no'].id,
'product_uom_qty': 2,
'product_uom': cls.company_data['product_order_no'].uom_id.id,
'price_unit': cls.company_data['product_order_no'].list_price,
'order_id': cls.sale_order.id,
'tax_id': False,
})
cls.sol_serv_deliver = cls.env['sale.order.line'].create({
'name': cls.company_data['product_service_delivery'].name,
'product_id': cls.company_data['product_service_delivery'].id,
'product_uom_qty': 2,
'product_uom': cls.company_data['product_service_delivery'].uom_id.id,
'price_unit': cls.company_data['product_service_delivery'].list_price,
'order_id': cls.sale_order.id,
'tax_id': False,
})
cls.sol_serv_order = cls.env['sale.order.line'].create({
'name': cls.company_data['product_service_order'].name,
'product_id': cls.company_data['product_service_order'].id,
'product_uom_qty': 2,
'product_uom': cls.company_data['product_service_order'].uom_id.id,
'price_unit': cls.company_data['product_service_order'].list_price,
'order_id': cls.sale_order.id,
'tax_id': False,
})
cls.sol_product_deliver = cls.env['sale.order.line'].create({
'name': cls.company_data['product_delivery_no'].name,
'product_id': cls.company_data['product_delivery_no'].id,
'product_uom_qty': 2,
'product_uom': cls.company_data['product_delivery_no'].uom_id.id,
'price_unit': cls.company_data['product_delivery_no'].list_price,
'order_id': cls.sale_order.id,
'tax_id': False,
})
def test_computes_auto_fill(self):
free_product = self.env['product.product'].create({
'name': 'Free product',
'list_price': 0.0,
})
dummy_product = self.env['product.product'].create({
'name': 'Dummy product',
'list_price': 0.0,
})
# Test pre-computes of lines with order
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [
Command.create({
'display_type': 'line_section',
'name': 'Dummy section',
}),
Command.create({
'display_type': 'line_section',
'name': 'Dummy section',
}),
Command.create({
'product_id': free_product.id,
}),
Command.create({
'product_id': dummy_product.id,
})
]
})
# Test pre-computes of lines creation alone
# Ensures the creation works fine even if the computes
# are triggered after the defaults
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
})
self.env['sale.order.line'].create([
{
'display_type': 'line_section',
'name': 'Dummy section',
'order_id': order.id,
}, {
'display_type': 'line_section',
'name': 'Dummy section',
'order_id': order.id,
}, {
'product_id': free_product.id,
'order_id': order.id,
}, {
'product_id': dummy_product.id,
'order_id': order.id,
}
])
def test_sale_order(self):
""" Test the sales order flow (invoicing and quantity updates)
- Invoice repeatedly while varrying delivered quantities and check that invoice are always what we expect
@@ -281,11 +324,6 @@ class TestSaleOrder(TestSaleCommon):
self.sol_serv_order.write({'tax_id': [(4, tax_exclude.id)]})
self.sol_product_deliver.write({'tax_id': [(4, tax_exclude.id)]})
# 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:
if line.tax_id.price_include:
price = line.price_unit * line.product_uom_qty - line.price_tax
@@ -294,9 +332,10 @@ class TestSaleOrder(TestSaleCommon):
self.assertEqual(float_compare(line.price_subtotal, price, precision_digits=2), 0)
self.assertEqual(self.sale_order.amount_total,
self.sale_order.amount_untaxed + self.sale_order.amount_tax,
'Taxes should be applied')
self.assertAlmostEqual(
self.sale_order.amount_total,
self.sale_order.amount_untaxed + self.sale_order.amount_tax,
places=2)
def test_so_create_multicompany(self):
"""Check that only taxes of the right company are applied on the lines."""
@@ -314,8 +353,9 @@ class TestSaleOrder(TestSaleCommon):
'company_id': self.company_data['company'].id,
})
so_1.write({
'order_line': [(0, False, {'product_id': product_shared.product_variant_id.id, 'order_id': so_1.id})],
'order_line': [Command.create({'product_id': product_shared.product_variant_id.id, 'order_id': so_1.id})],
})
self.assertEqual(so_1.order_line.product_uom_qty, 1)
self.assertEqual(so_1.order_line.tax_id, self.company_data['default_tax_sale'],
'Only taxes from the right company are put by default')
@@ -461,9 +501,6 @@ class TestSaleOrder(TestSaleCommon):
})
]
})
for line in sales_order.order_line:
# Create values autofill does not compute discount.
line._onchange_discount()
so_line_1 = sales_order.order_line[0]
so_line_2 = sales_order.order_line[1]
@@ -492,8 +529,6 @@ class TestSaleOrder(TestSaleCommon):
})
]
})
for line in sales_order.order_line:
line._onchange_discount()
so_line_1 = sales_order.order_line[0]
so_line_2 = sales_order.order_line[1]
@@ -512,7 +547,6 @@ class TestSaleOrder(TestSaleCommon):
sale_order = self.env['sale.order'].create({
'partner_id': partner.id,
})
sale_order.onchange_partner_id()
self.assertEqual(sale_order.team_id.id, self.crm_team0.id, 'Should assign to team of sales person')
def test_assign_sales_team_from_partner_team(self):
@@ -525,7 +559,6 @@ class TestSaleOrder(TestSaleCommon):
sale_order = self.env['sale.order'].create({
'partner_id': partner.id,
})
sale_order.onchange_partner_id()
self.assertEqual(sale_order.team_id.id, self.crm_team1.id, 'Should assign to team of partner')
def test_assign_sales_team_when_changing_user(self):
@@ -536,7 +569,6 @@ class TestSaleOrder(TestSaleCommon):
'team_id': self.crm_team1.id
})
sale_order.user_id = self.user_in_team
sale_order.onchange_user_id()
self.assertEqual(sale_order.team_id.id, self.crm_team0.id, 'Should assign to team of sales person')
def test_keep_sales_team_when_changing_user_with_no_team(self):
@@ -546,7 +578,6 @@ class TestSaleOrder(TestSaleCommon):
'team_id': self.crm_team1.id
})
sale_order.user_id = self.user_not_in_team
sale_order.onchange_user_id()
self.assertEqual(sale_order.team_id.id, self.crm_team1.id, 'Should not reset the team to default')
def test_onchange_packaging_00(self):
@@ -736,3 +767,42 @@ class TestSaleOrder(TestSaleCommon):
# (541,26 / 1,15) * ,98 * 38 = 17527,410782609 ~= 17527.41
self.assertEqual(line.price_subtotal, 17527.41)
self.assertEqual(line.untaxed_amount_to_invoice, line.price_subtotal)
def test_so_tax_mapping(self):
uom = self.env['uom.uom'].search([('name', '=', 'Units')], limit=1)
pricelist = self.env['product.pricelist'].search([('name', '=', 'Public Pricelist')], limit=1)
partner = self.env['res.partner'].create(dict(name="George"))
tax_include = self.env['account.tax'].create(dict(name="Include tax",
amount='21.00',
price_include=True,
type_tax_use='sale'))
tax_exclude = self.env['account.tax'].create(dict(name="Exclude tax",
amount='0.00',
type_tax_use='sale'))
product_tmpl = self.env['product.template'].create(dict(name="Voiture",
list_price=121,
taxes_id=[(6, 0, [tax_include.id])]))
product = product_tmpl.product_variant_id
fpos = self.env['account.fiscal.position'].create(dict(name="fiscal position", sequence=1))
self.env['account.fiscal.position.tax'].create({
'position_id': fpos.id,
'tax_src_id': tax_include.id,
'tax_dest_id': tax_exclude.id,
})
sale_order = self.env['sale.order'].create({
'partner_id': partner.id,
'pricelist_id': pricelist.id,
'fiscal_position_id': fpos.id,
'order_line': [Command.create({
'product_id': product.id,
'product_uom': uom.id,
})]
})
# Check the unit price of SO line
self.assertEqual(100, sale_order.order_line[0].price_unit, "The included tax must be subtracted to the price")
+209 -20
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from .common import TestSaleCommon
from odoo.fields import Command
from odoo.tests import tagged
@@ -79,38 +80,26 @@ class TestSaleOrder(TestSaleCommon):
'pricelist_id': cls.company_data['default_pricelist'].id,
})
cls.sol_product_order = SaleOrderLine.create({
'name': cls.company_data['product_order_no'].name,
'product_id': cls.company_data['product_order_no'].id,
'product_uom_qty': 2,
'product_uom': cls.company_data['product_order_no'].uom_id.id,
'price_unit': cls.company_data['product_order_no'].list_price,
'order_id': cls.sale_order.id,
'tax_id': False,
})
cls.sol_serv_deliver = SaleOrderLine.create({
'name': cls.company_data['product_service_delivery'].name,
'product_id': cls.company_data['product_service_delivery'].id,
'product_uom_qty': 2,
'product_uom': cls.company_data['product_service_delivery'].uom_id.id,
'price_unit': cls.company_data['product_service_delivery'].list_price,
'order_id': cls.sale_order.id,
'tax_id': False,
})
cls.sol_serv_order = SaleOrderLine.create({
'name': cls.company_data['product_service_order'].name,
'product_id': cls.company_data['product_service_order'].id,
'product_uom_qty': 2,
'product_uom': cls.company_data['product_service_order'].uom_id.id,
'price_unit': cls.company_data['product_service_order'].list_price,
'order_id': cls.sale_order.id,
'tax_id': False,
})
cls.sol_prod_deliver = SaleOrderLine.create({
'name': cls.company_data['product_delivery_no'].name,
'product_id': cls.company_data['product_delivery_no'].id,
'product_uom_qty': 2,
'product_uom': cls.company_data['product_delivery_no'].uom_id.id,
'price_unit': cls.company_data['product_delivery_no'].list_price,
'order_id': cls.sale_order.id,
'tax_id': False,
})
@@ -119,10 +108,9 @@ class TestSaleOrder(TestSaleCommon):
""" Test SO with the pricelist and check unit price appeared on its lines """
# Change the pricelist
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()
# Trigger manually the computation for discount, unit price, subtotal, ...
self.sale_order.update_prices()
# Check that pricelist of the SO has been applied on the sale order lines or not
for line in self.sale_order.order_line:
if line.product_id == self.company_data['product_order_no']:
@@ -143,10 +131,8 @@ class TestSaleOrder(TestSaleCommon):
# Change the pricelist
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()
# Trigger manually the computation for discount, unit price, subtotal, ...
self.sale_order.update_prices()
# Check pricelist of the SO apply or not on order lines where pricelist contains formula that add 15% on the cost price
for line in self.sale_order.order_line:
@@ -161,3 +147,206 @@ class TestSaleOrder(TestSaleCommon):
else: # no discount for the rest
self.assertEqual(line.discount, 0.0, 'Pricelist of SO should not be applied on an order line')
self.assertEqual(line.price_unit, line.product_id.list_price, 'Unit price of order line should be a sale price as the pricelist not applied on the other category\'s product')
def test_pricelist_application(self):
""" Test different prices are correctly applied based on dates """
support_product = self.env['product.product'].create({
'name': 'Virtual Home Staging',
'list_price': 100,
})
partner = self.env['res.partner'].create(dict(name="George"))
christmas_pricelist = self.env['product.pricelist'].create({
'name': 'Christmas pricelist',
'item_ids': [(0, 0, {
'date_start': "2017-12-01",
'date_end': "2017-12-24",
'compute_price': 'percentage',
'base': 'list_price',
'percent_price': 20,
'applied_on': '3_global',
'name': 'Pre-Christmas discount'
}), (0, 0, {
'date_start': "2017-12-25",
'date_end': "2017-12-31",
'compute_price': 'percentage',
'base': 'list_price',
'percent_price': 50,
'applied_on': '3_global',
'name': 'Post-Christmas super-discount'
})]
})
# Create the SO with pricelist based on date
so = self.env['sale.order'].create({
'partner_id': partner.id,
'date_order': '2017-12-20',
'pricelist_id': christmas_pricelist.id,
'order_line': [Command.create({'product_id': support_product.id})]
})
# Check the unit price and subtotal of SO line
self.assertEqual(so.order_line[0].price_unit, 80, "First date pricelist rule not applied")
self.assertEqual(so.order_line[0].price_subtotal, so.order_line[0].price_unit * so.order_line[0].product_uom_qty, 'Total of SO line should be a multiplication of unit price and ordered quantity')
# Change order date of the SO and check the unit price and subtotal of SO line
so.date_order = '2017-12-30'
so.update_prices()
self.assertEqual(so.order_line[0].price_unit, 50, "Second date pricelist rule not applied")
self.assertEqual(so.order_line[0].price_subtotal, so.order_line[0].price_unit * so.order_line[0].product_uom_qty, 'Total of SO line should be a multiplication of unit price and ordered quantity')
def test_pricelist_uom_discount(self):
""" Test prices and discounts are correctly applied based on date and uom"""
computer_case = self.env['product.product'].create({
'name': 'Drawer Black',
'list_price': 100,
})
partner = self.env['res.partner'].create(dict(name="George"))
categ_unit_id = self.ref('uom.product_uom_categ_unit')
goup_discount_id = self.ref('product.group_discount_per_so_line')
self.env.user.write({'groups_id': [(4, goup_discount_id, 0)]})
new_uom = self.env['uom.uom'].create({
'name': '10 units',
'factor_inv': 10,
'uom_type': 'bigger',
'rounding': 1.0,
'category_id': categ_unit_id
})
christmas_pricelist = self.env['product.pricelist'].create({
'name': 'Christmas pricelist',
'discount_policy': 'without_discount',
'item_ids': [(0, 0, {
'date_start': "2017-12-01",
'date_end': "2017-12-30",
'compute_price': 'percentage',
'base': 'list_price',
'percent_price': 10,
'applied_on': '3_global',
'name': 'Christmas discount'
})]
})
so = self.env['sale.order'].create({
'partner_id': partner.id,
'date_order': '2017-12-20',
'pricelist_id': christmas_pricelist.id,
})
order_line = self.env['sale.order.line'].create({
'order_id': so.id,
'product_id': computer_case.id,
})
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%")
order_line.product_uom = new_uom
self.assertEqual(order_line.price_subtotal, 900, "Christmas discount pricelist rule not applied")
self.assertEqual(order_line.discount, 10, "Christmas discount not equalt to 10%")
def test_pricelist_based_on_other(self):
""" Test price and discount are correctly applied with a pricelist based on an other one"""
computer_case = self.env['product.product'].create({
'name': 'Drawer Black',
'list_price': 100,
})
partner = self.env['res.partner'].create(dict(name="George"))
goup_discount_id = self.ref('product.group_discount_per_so_line')
self.env.user.write({'groups_id': [(4, goup_discount_id, 0)]})
first_pricelist = self.env['product.pricelist'].create({
'name': 'First pricelist',
'discount_policy': 'without_discount',
'item_ids': [(0, 0, {
'compute_price': 'percentage',
'base': 'list_price',
'percent_price': 10,
'applied_on': '3_global',
'name': 'First discount'
})]
})
second_pricelist = self.env['product.pricelist'].create({
'name': 'Second pricelist',
'discount_policy': 'without_discount',
'item_ids': [(0, 0, {
'compute_price': 'formula',
'base': 'pricelist',
'base_pricelist_id': first_pricelist.id,
'price_discount': 10,
'applied_on': '3_global',
'name': 'Second discount'
})]
})
so = self.env['sale.order'].create({
'partner_id': partner.id,
'date_order': '2018-07-11',
'pricelist_id': second_pricelist.id,
})
order_line = self.env['sale.order.line'].create({
'order_id': so.id,
'product_id': computer_case.id,
})
self.assertEqual(order_line.price_subtotal, 81, "Second pricelist rule not applied")
self.assertEqual(order_line.discount, 19, "Second discount not applied")
def test_pricelist_with_other_currency(self):
""" Test prices are correctly applied with a pricelist with an other currency"""
computer_case = self.env['product.product'].create({
'name': 'Drawer Black',
'list_price': 100,
})
# computer_case.list_price = 100
partner = self.env['res.partner'].create(dict(name="George"))
categ_unit_id = self.ref('uom.product_uom_categ_unit')
other_currency = self.env['res.currency'].create({
'name': 'other currency',
'symbol': 'other'
})
self.env['res.currency.rate'].create({'name': '2018-07-11',
'rate': 2.0,
'currency_id': other_currency.id,
'company_id': self.env.company.id
})
self.env['res.currency.rate'].search(
[('currency_id', '=', self.env.company.currency_id.id)]
).unlink()
new_uom = self.env['uom.uom'].create({
'name': '10 units',
'factor_inv': 10,
'uom_type': 'bigger',
'rounding': 1.0,
'category_id': categ_unit_id
})
# This pricelist doesn't show the discount
first_pricelist = self.env['product.pricelist'].create({
'name': 'First pricelist',
'currency_id': other_currency.id,
'discount_policy': 'with_discount',
'item_ids': [(0, 0, {
'compute_price': 'percentage',
'base': 'list_price',
'percent_price': 10,
'applied_on': '3_global',
'name': 'First discount'
})]
})
so = self.env['sale.order'].create({
'partner_id': partner.id,
'date_order': '2018-07-12',
'pricelist_id': first_pricelist.id,
})
order_line = self.env['sale.order.line'].new({
'order_id': so.id,
'product_id': computer_case.id,
})
# force compute uom and prices
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")
-15
View File
@@ -21,38 +21,26 @@ class TestSaleToInvoice(TestSaleCommon):
})
SaleOrderLine = cls.env['sale.order.line'].with_context(tracking_disable=True)
cls.sol_prod_order = SaleOrderLine.create({
'name': cls.company_data['product_order_no'].name,
'product_id': cls.company_data['product_order_no'].id,
'product_uom_qty': 5,
'product_uom': cls.company_data['product_order_no'].uom_id.id,
'price_unit': cls.company_data['product_order_no'].list_price,
'order_id': cls.sale_order.id,
'tax_id': False,
})
cls.sol_serv_deliver = SaleOrderLine.create({
'name': cls.company_data['product_service_delivery'].name,
'product_id': cls.company_data['product_service_delivery'].id,
'product_uom_qty': 4,
'product_uom': cls.company_data['product_service_delivery'].uom_id.id,
'price_unit': cls.company_data['product_service_delivery'].list_price,
'order_id': cls.sale_order.id,
'tax_id': False,
})
cls.sol_serv_order = SaleOrderLine.create({
'name': cls.company_data['product_service_order'].name,
'product_id': cls.company_data['product_service_order'].id,
'product_uom_qty': 3,
'product_uom': cls.company_data['product_service_order'].uom_id.id,
'price_unit': cls.company_data['product_service_order'].list_price,
'order_id': cls.sale_order.id,
'tax_id': False,
})
cls.sol_prod_deliver = SaleOrderLine.create({
'name': cls.company_data['product_delivery_no'].name,
'product_id': cls.company_data['product_delivery_no'].id,
'product_uom_qty': 2,
'product_uom': cls.company_data['product_delivery_no'].uom_id.id,
'price_unit': cls.company_data['product_delivery_no'].list_price,
'order_id': cls.sale_order.id,
'tax_id': False,
})
@@ -329,11 +317,8 @@ class TestSaleToInvoice(TestSaleCommon):
'pricelist_id': self.company_data['default_pricelist'].id,
})
sol_product = self.env['sale.order.line'].create({
'name': self.company_data['product_order_no'].name,
'product_id': self.company_data['product_order_no'].id,
'product_uom_qty': 5,
'product_uom': self.company_data['product_order_no'].uom_id.id,
'price_unit': self.company_data['product_order_no'].list_price,
'order_id': sale_order_refund.id,
'tax_id': False,
})
-18
View File
@@ -22,38 +22,26 @@ class TestSaleToInvoice(TestSaleCommon):
})
SaleOrderLine = cls.env['sale.order.line'].with_context(tracking_disable=True)
cls.sol_prod_order = SaleOrderLine.create({
'name': cls.company_data['product_order_no'].name,
'product_id': cls.company_data['product_order_no'].id,
'product_uom_qty': 5,
'product_uom': cls.company_data['product_order_no'].uom_id.id,
'price_unit': cls.company_data['product_order_no'].list_price,
'order_id': cls.sale_order.id,
'tax_id': False,
})
cls.sol_serv_deliver = SaleOrderLine.create({
'name': cls.company_data['product_service_delivery'].name,
'product_id': cls.company_data['product_service_delivery'].id,
'product_uom_qty': 4,
'product_uom': cls.company_data['product_service_delivery'].uom_id.id,
'price_unit': cls.company_data['product_service_delivery'].list_price,
'order_id': cls.sale_order.id,
'tax_id': False,
})
cls.sol_serv_order = SaleOrderLine.create({
'name': cls.company_data['product_service_order'].name,
'product_id': cls.company_data['product_service_order'].id,
'product_uom_qty': 3,
'product_uom': cls.company_data['product_service_order'].uom_id.id,
'price_unit': cls.company_data['product_service_order'].list_price,
'order_id': cls.sale_order.id,
'tax_id': False,
})
cls.sol_prod_deliver = SaleOrderLine.create({
'name': cls.company_data['product_delivery_no'].name,
'product_id': cls.company_data['product_delivery_no'].id,
'product_uom_qty': 2,
'product_uom': cls.company_data['product_delivery_no'].uom_id.id,
'price_unit': cls.company_data['product_delivery_no'].list_price,
'order_id': cls.sale_order.id,
'tax_id': False,
})
@@ -296,11 +284,8 @@ class TestSaleToInvoice(TestSaleCommon):
'order_id': sale_order.id,
})
sol_prod_deliver = SaleOrderLine.create({
'name': self.company_data['product_order_no'].name,
'product_id': self.company_data['product_order_no'].id,
'product_uom_qty': 5,
'product_uom': self.company_data['product_order_no'].uom_id.id,
'price_unit': self.company_data['product_order_no'].list_price,
'order_id': sale_order.id,
'tax_id': False,
})
@@ -339,11 +324,8 @@ class TestSaleToInvoice(TestSaleCommon):
SaleOrderLine = self.env['sale.order.line'].with_context(tracking_disable=True)
sol_prod_deliver = SaleOrderLine.create({
'name': self.company_data['product_order_no'].name,
'product_id': self.company_data['product_order_no'].id,
'product_uom_qty': 5,
'product_uom': self.company_data['product_order_no'].uom_id.id,
'price_unit': self.company_data['product_order_no'].list_price,
'order_id': sale_order.id,
'tax_id': False,
})
-2
View File
@@ -378,7 +378,6 @@
/>
<field name="invoice_status" invisible="1"/>
<field name="qty_to_invoice" invisible="1"/>
<field name="qty_delivered_manual" invisible="1"/>
<field name="qty_delivered_method" invisible="1"/>
<field name="price_total" invisible="1"/>
<field name="price_tax" invisible="1"/>
@@ -528,7 +527,6 @@
}"
optional="show"
/>
<field name="qty_delivered_manual" invisible="1"/>
<field name="qty_delivered_method" invisible="1"/>
<field
name="qty_invoiced"
@@ -78,7 +78,7 @@ class SaleAdvancePaymentInv(models.TransientModel):
'invoice_user_id': order.user_id.id,
'narration': order.note,
'partner_id': order.partner_invoice_id.id,
'fiscal_position_id': (order.fiscal_position_id or order.fiscal_position_id.get_fiscal_position(order.partner_id.id)).id,
'fiscal_position_id': (order.fiscal_position_id or order.fiscal_position_id._get_fiscal_position(order.partner_id)).id,
'partner_shipping_id': order.partner_shipping_id.id,
'currency_id': order.pricelist_id.currency_id.id,
'payment_reference': order.reference,
+1 -1
View File
@@ -509,7 +509,7 @@ class SaleOrderLine(models.Model):
# In case of a free product, retrieving the tax on the line instead of the product won't affect the behavior.
for line in reward_lines:
line = line.with_company(line.company_id)
fpos = line.order_id.fiscal_position_id or line.order_id.fiscal_position_id.get_fiscal_position(line.order_partner_id.id)
fpos = line.order_id.fiscal_position_id or line.order_id.fiscal_position_id._get_fiscal_position(line.order_partner_id)
# If company_id is set, always filter taxes by the company
taxes = line.tax_id.filtered(lambda r: not line.company_id or r.company_id == line.company_id)
line.tax_id = fpos.map_tax(taxes)
@@ -26,7 +26,6 @@ class TestSaleExpense(TestExpenseCommon, TestSaleCommon):
})],
'pricelist_id': self.env.ref('product.list0').id,
})
so._compute_tax_id()
so.action_confirm()
so._create_analytic_account() # normally created at so confirmation when you use the right products
init_price = so.amount_total
@@ -43,8 +43,6 @@ class SaleOrder(models.Model):
self.env["sale.order.line"].create({
'product_id': pay_gift_card_id.id,
'price_unit': - amount,
'product_uom_qty': 1,
'product_uom': pay_gift_card_id.uom_id.id,
'gift_card_id': gift_card.id,
'order_id': self.id
})
+17 -19
View File
@@ -50,11 +50,12 @@ class SaleOrder(models.Model):
default['validity_date'] = fields.Date.context_today(self) + timedelta(self.sale_order_template_id.number_of_days)
return super(SaleOrder, self).copy(default=default)
@api.onchange('partner_id')
def onchange_partner_id(self):
super(SaleOrder, self).onchange_partner_id()
template = self.sale_order_template_id.with_context(lang=self.partner_id.lang)
self.note = template.note if not is_html_empty(template.note) else self.note
@api.depends('partner_id', 'sale_order_template_id')
def _compute_note(self):
super()._compute_note()
for order in self.filtered('sale_order_template_id'):
template = order.sale_order_template_id.with_context(lang=order.partner_id.lang)
order.note = template.note if not is_html_empty(template.note) else order.note
def _compute_line_data_for_template_change(self, line):
return {
@@ -124,13 +125,11 @@ class SaleOrder(models.Model):
'product_uom_qty': line.product_uom_qty,
'product_id': line.product_id.id,
'product_uom': line.product_uom_id.id,
'customer_lead': self._get_customer_lead(line.product_id.product_tmpl_id),
})
order_lines.append((0, 0, data))
self.order_line = order_lines
self.order_line._compute_tax_id()
# then, process the list of optional products from the template
option_lines = [(5, 0, 0)]
@@ -177,16 +176,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):
@@ -239,7 +238,7 @@ class SaleOrderOption(models.Model):
# To compute the discount a so line is created in cache
values = self._get_values_to_add_to_order()
new_sol = self.env['sale.order.line'].new(values)
new_sol._onchange_discount()
new_sol._compute_discount()
self.discount = new_sol.discount
if self.order_id.pricelist_id and self.order_id.partner_id:
self.price_unit = new_sol._get_display_price(product)
@@ -257,7 +256,6 @@ class SaleOrderOption(models.Model):
values = self._get_values_to_add_to_order()
order_line = self.env['sale.order.line'].create(values)
order_line._compute_tax_id()
self.write({'line_id': order_line.id})
if sale_order:
+3 -3
View File
@@ -8,12 +8,12 @@ class SaleOrderLine(models.Model):
margin = fields.Float(
"Margin", compute='_compute_margin',
digits='Product Price', store=True, groups="base.group_user")
digits='Product Price', store=True, groups="base.group_user", precompute=True)
margin_percent = fields.Float(
"Margin (%)", compute='_compute_margin', store=True, groups="base.group_user")
"Margin (%)", compute='_compute_margin', store=True, groups="base.group_user", precompute=True)
purchase_price = fields.Float(
string='Cost', compute="_compute_purchase_price",
digits='Product Price', store=True, readonly=False,
digits='Product Price', store=True, readonly=False, precompute=True,
groups="base.group_user")
@api.depends('product_id', 'company_id', 'currency_id', 'product_uom')
@@ -148,6 +148,8 @@ class TestUi(odoo.tests.HttpCase, TestProductConfiguratorCommon):
# Remove tax from Conference Chair and Chair floor protection
self.product_product_conf_chair.taxes_id = None
self.product_product_conf_chair_floor_protect.taxes_id = None
# YTI FIXME: This tour requires sale_management, as the Sale
# root menu item is clicked
self.start_tour("/web", 'sale_product_configurator_pricelist_tour', login="admin")
def test_06_product_configurator_optional_products(self):
@@ -45,7 +45,6 @@ class SaleOrder(models.Model):
if self.grid and self.grid_update:
grid = json.loads(self.grid)
product_template = self.env['product.template'].browse(grid['product_template_id'])
product_ids = set()
dirty_cells = grid['changes']
Attrib = self.env['product.template.attribute.value']
default_so_line_vals = {}
@@ -69,8 +68,6 @@ class SaleOrder(models.Model):
if not diff:
continue
product_ids.add(product.id)
# TODO keep qty check? cannot be 0 because we only get cell changes ...
if order_lines:
if qty == 0:
@@ -115,18 +112,9 @@ class SaleOrder(models.Model):
product_uom_qty=qty,
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()
line._onchange_product_id_set_customer_lead()
return res
if new_lines:
# Add new SO lines
self.update(dict(order_line=new_lines))
def _get_matrix(self, product_template):
"""Return the matrix of the given product, updated with current SOLines quantities.
+5 -3
View File
@@ -139,8 +139,10 @@ class ProjectTask(models.Model):
sale_order_id = fields.Many2one('sale.order', 'Sales Order', compute='_compute_sale_order_id', store=True, help="Sales order to which the task is linked.")
sale_line_id = fields.Many2one(
'sale.order.line', 'Sales Order Item', domain="[('company_id', '=', company_id), ('is_service', '=', True), ('order_partner_id', 'child_of', commercial_partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done'])]",
compute='_compute_sale_line', recursive=True, store=True, readonly=False, copy=False, tracking=True, index=True,
'sale.order.line', 'Sales Order Item',
copy=False, tracking=True, index=True, recursive=True,
compute='_compute_sale_line', store=True, readonly=False,
domain="[('company_id', '=', company_id), ('is_service', '=', True), ('order_partner_id', 'child_of', commercial_partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done'])]",
help="Sales Order Item to which the time spent on this task will be added, in order to be invoiced to your customer.")
project_sale_order_id = fields.Many2one('sale.order', string="Project's sale order", related='project_id.sale_order_id')
invoice_count = fields.Integer("Number of invoices", related='sale_order_id.invoice_count')
@@ -164,7 +166,7 @@ class ProjectTask(models.Model):
task.partner_id = sale_order_id.partner_id
task.sale_order_id = sale_order_id
@api.depends('commercial_partner_id', 'sale_line_id.order_partner_id.commercial_partner_id', 'parent_id.sale_line_id', 'project_id.sale_line_id')
@api.depends('commercial_partner_id', 'sale_line_id.order_partner_id', 'parent_id.sale_line_id', 'project_id.sale_line_id')
def _compute_sale_line(self):
for task in self:
if not task.sale_line_id:
@@ -91,38 +91,26 @@ class TestSaleProject(TransactionCase):
'partner_shipping_id': partner.id,
})
so_line_order_no_task = SaleOrderLine.create({
'name': self.product_order_service1.name,
'product_id': self.product_order_service1.id,
'product_uom_qty': 10,
'product_uom': self.product_order_service1.uom_id.id,
'price_unit': self.product_order_service1.list_price,
'order_id': sale_order.id,
})
so_line_order_task_in_global = SaleOrderLine.create({
'name': self.product_order_service2.name,
'product_id': self.product_order_service2.id,
'product_uom_qty': 10,
'product_uom': self.product_order_service2.uom_id.id,
'price_unit': self.product_order_service2.list_price,
'order_id': sale_order.id,
})
so_line_order_new_task_new_project = SaleOrderLine.create({
'name': self.product_order_service3.name,
'product_id': self.product_order_service3.id,
'product_uom_qty': 10,
'product_uom': self.product_order_service3.uom_id.id,
'price_unit': self.product_order_service3.list_price,
'order_id': sale_order.id,
})
so_line_order_only_project = SaleOrderLine.create({
'name': self.product_order_service4.name,
'product_id': self.product_order_service4.id,
'product_uom_qty': 10,
'product_uom': self.product_order_service4.uom_id.id,
'price_unit': self.product_order_service4.list_price,
'order_id': sale_order.id,
})
sale_order.action_confirm()
+1 -1
View File
@@ -193,7 +193,7 @@ class SaleOrderLine(models.Model):
"""
self.ensure_one()
partner_supplier = supplierinfo.partner_id
fpos = self.env['account.fiscal.position'].sudo().get_fiscal_position(partner_supplier.id)
fpos = self.env['account.fiscal.position'].sudo()._get_fiscal_position(partner_supplier)
date_order = self._purchase_get_date_order(supplierinfo)
return {
'partner_id': partner_supplier.id,
@@ -39,11 +39,8 @@ class TestAccessRights(TestCommonSalePurchaseNoChart):
})
sol_service_purchase = self.env['sale.order.line'].with_user(self.user_salesperson).create({
'name': self.service_purchase_1.name,
'product_id': self.service_purchase_1.id,
'product_uom_qty': 4,
'product_uom': self.service_purchase_1.uom_id.id,
'price_unit': self.service_purchase_1.list_price,
'order_id': sale_order.id,
'tax_id': False,
})
@@ -22,29 +22,20 @@ class TestSalePurchase(TestCommonSalePurchaseNoChart):
'pricelist_id': cls.company_data['default_pricelist'].id,
})
cls.sol1_service_deliver = cls.env['sale.order.line'].create({
'name': cls.company_data['product_service_delivery'].name,
'product_id': cls.company_data['product_service_delivery'].id,
'product_uom_qty': 1,
'product_uom': cls.company_data['product_service_delivery'].uom_id.id,
'price_unit': cls.company_data['product_service_delivery'].list_price,
'order_id': cls.sale_order_1.id,
'tax_id': False,
})
cls.sol1_product_order = cls.env['sale.order.line'].create({
'name': cls.company_data['product_order_no'].name,
'product_id': cls.company_data['product_order_no'].id,
'product_uom_qty': 2,
'product_uom': cls.company_data['product_order_no'].uom_id.id,
'price_unit': cls.company_data['product_order_no'].list_price,
'order_id': cls.sale_order_1.id,
'tax_id': False,
})
cls.sol1_service_purchase_1 = cls.env['sale.order.line'].create({
'name': cls.service_purchase_1.name,
'product_id': cls.service_purchase_1.id,
'product_uom_qty': 4,
'product_uom': cls.service_purchase_1.uom_id.id,
'price_unit': cls.service_purchase_1.list_price,
'order_id': cls.sale_order_1.id,
'tax_id': False,
})
@@ -56,29 +47,20 @@ class TestSalePurchase(TestCommonSalePurchaseNoChart):
'pricelist_id': cls.company_data['default_pricelist'].id,
})
cls.sol2_product_deliver = cls.env['sale.order.line'].create({
'name': cls.company_data['product_delivery_no'].name,
'product_id': cls.company_data['product_delivery_no'].id,
'product_uom_qty': 5,
'product_uom': cls.company_data['product_delivery_no'].uom_id.id,
'price_unit': cls.company_data['product_delivery_no'].list_price,
'order_id': cls.sale_order_2.id,
'tax_id': False,
})
cls.sol2_service_order = cls.env['sale.order.line'].create({
'name': cls.company_data['product_service_order'].name,
'product_id': cls.company_data['product_service_order'].id,
'product_uom_qty': 6,
'product_uom': cls.company_data['product_service_order'].uom_id.id,
'price_unit': cls.company_data['product_service_order'].list_price,
'order_id': cls.sale_order_2.id,
'tax_id': False,
})
cls.sol2_service_purchase_2 = cls.env['sale.order.line'].create({
'name': cls.service_purchase_2.name,
'product_id': cls.service_purchase_2.id,
'product_uom_qty': 7,
'product_uom': cls.service_purchase_2.uom_id.id,
'price_unit': cls.service_purchase_2.list_price,
'order_id': cls.sale_order_2.id,
'tax_id': False,
})
@@ -10,6 +10,7 @@ class SaleOrder(models.Model):
website_description = fields.Html('Website Description', sanitize_attributes=False, translate=html_translate, sanitize_form=False)
# YTI TODO: This should be converted
@api.onchange('partner_id')
def onchange_update_description_lang(self):
if not self.sale_order_template_id:
@@ -42,14 +43,14 @@ class SaleOrderLine(models.Model):
website_description = fields.Html('Website Description', sanitize=False, translate=html_translate, sanitize_form=False)
@api.model
def create(self, values):
values = self._inject_quotation_description(values)
return super(SaleOrderLine, self).create(values)
@api.model_create_multi
def create(self, vals_list):
vals_list = [self._inject_quotation_description(vals) for vals in vals_list]
return super().create(vals_list)
def write(self, values):
values = self._inject_quotation_description(values)
return super(SaleOrderLine, self).write(values)
return super().write(values)
def _inject_quotation_description(self, values):
values = dict(values or {})
@@ -31,10 +31,10 @@ class SaleOrderTemplateLine(models.Model):
self.website_description = self.product_id.quotation_description
return ret
@api.model
def create(self, values):
values = self._inject_quotation_description(values)
return super(SaleOrderTemplateLine, self).create(values)
@api.model_create_multi
def create(self, vals_list):
vals_list = [self._inject_quotation_description(vals) for vals in vals_list]
return super().create(vals_list)
def write(self, values):
values = self._inject_quotation_description(values)
+34 -40
View File
@@ -17,12 +17,6 @@ _logger = logging.getLogger(__name__)
class SaleOrder(models.Model):
_inherit = "sale.order"
@api.model
def _default_warehouse_id(self):
# !!! Any change to the default value may have to be repercuted
# on _init_column() below.
return self.env.user._get_default_warehouse_id()
incoterm = fields.Many2one(
'account.incoterms', 'Incoterm',
help="International Commercial Terms are a series of predefined commercial terms used in international transactions.")
@@ -30,13 +24,14 @@ class SaleOrder(models.Model):
('direct', 'As soon as possible'),
('one', 'When all products are ready')],
string='Shipping Policy', required=True, readonly=True, default='direct',
states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}
,help="If you deliver all products at once, the delivery order will be scheduled based on the greatest "
states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
help="If you deliver all products at once, the delivery order will be scheduled based on the greatest "
"product lead time. Otherwise, it will be based on the shortest.")
warehouse_id = fields.Many2one(
'stock.warehouse', string='Warehouse',
required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
default=_default_warehouse_id, check_company=True)
'stock.warehouse', string='Warehouse', required=True,
compute='_compute_warehouse_id', store=True, readonly=False, precompute=True,
states={'sale': [('readonly', True)], 'done': [('readonly', False)], 'cancel': [('readonly', False)]},
check_company=True)
picking_ids = fields.One2many('stock.picking', 'sale_id', string='Transfers')
delivery_count = fields.Integer(string='Delivery Orders', compute='_compute_picking_ids')
procurement_group_id = fields.Many2one('procurement.group', 'Procurement Group', copy=False)
@@ -88,13 +83,6 @@ class SaleOrder(models.Model):
expected_date = min(dates_list) if order.picking_policy == 'direct' else max(dates_list)
order.expected_date = fields.Datetime.to_string(expected_date)
@api.model
def create(self, vals):
if 'warehouse_id' not in vals and 'company_id' in vals:
user = self.env['res.users'].browse(vals.get('user_id', False))
vals['warehouse_id'] = user.with_company(vals.get('company_id'))._get_default_warehouse_id().id
return super().create(vals)
def write(self, values):
if values.get('order_line') and self.state == 'sale':
for order in self:
@@ -153,17 +141,20 @@ class SaleOrder(models.Model):
for order in self:
order.delivery_count = len(order.picking_ids)
@api.onchange('company_id')
def _onchange_company_id(self):
if self.company_id:
warehouse_id = self.env['ir.default'].get_model_defaults('sale.order').get('warehouse_id')
self.warehouse_id = warehouse_id or self.user_id.with_company(self.company_id.id)._get_default_warehouse_id().id
@api.onchange('user_id')
def onchange_user_id(self):
super().onchange_user_id()
if self.state in ['draft', 'sent']:
self.warehouse_id = self.user_id.with_company(self.company_id.id)._get_default_warehouse_id().id
@api.depends('user_id', 'company_id')
def _compute_warehouse_id(self):
for order in self:
default_warehouse_id = self.env['ir.default'].with_company(
order.company_id.id).get_model_defaults('sale.order').get('warehouse_id')
if order.company_id and order.company_id != order._origin.company_id:
warehouse = default_warehouse_id
else:
warehouse = self.env['stock.warehouse']
if order.state in ['draft', 'sent']:
order.warehouse_id = warehouse or order.user_id.with_company(order.company_id.id)._get_default_warehouse_id()
# In case we create a record in another state (eg: demo data, or business code)
if not order.warehouse_id:
order.warehouse_id = self.env.user._get_default_warehouse_id()
@api.onchange('partner_shipping_id')
def _onchange_partner_shipping_id(self):
@@ -234,11 +225,6 @@ class SaleOrder(models.Model):
invoice_vals['invoice_incoterm_id'] = self.incoterm.id
return invoice_vals
@api.model
def _get_customer_lead(self, product_tmpl_id):
super(SaleOrder, self)._get_customer_lead(product_tmpl_id)
return product_tmpl_id.sale_delay
def _log_decrease_ordered_quantity(self, documents, cancel=False):
def _render_note_exception_quantity_so(rendering_context):
@@ -281,6 +267,9 @@ class SaleOrderLine(models.Model):
qty_to_deliver = fields.Float(compute='_compute_qty_to_deliver', digits='Product Unit of Measure')
is_mto = fields.Boolean(compute='_compute_is_mto')
display_qty_widget = fields.Boolean(compute='_compute_qty_to_deliver')
customer_lead = fields.Float(
compute='_compute_customer_lead', store=True, readonly=False, precompute=True,
inverse='_inverse_customer_lead')
@api.depends('product_type', 'product_uom_qty', 'qty_delivered', 'state', 'move_ids', 'product_uom')
def _compute_qty_to_deliver(self):
@@ -434,9 +423,6 @@ class SaleOrderLine(models.Model):
res = super(SaleOrderLine, self).write(values)
if lines:
lines._action_launch_stock_rule(previous_product_uom_qty)
if 'customer_lead' in values and self.state == 'sale' and not self.order_id.commitment_date:
# Propagate deadline on related stock move
self.move_ids.date_deadline = self.order_id.date_order + timedelta(days=self.customer_lead or 0.0)
return res
@api.depends('order_id.state')
@@ -471,9 +457,17 @@ class SaleOrderLine(models.Model):
else:
line.product_updatable = False
@api.onchange('product_id')
def _onchange_product_id_set_customer_lead(self):
self.customer_lead = self.product_id.sale_delay
@api.depends('product_id')
def _compute_customer_lead(self):
super()._compute_customer_lead() # Reset customer_lead when the product is modified
for line in self:
line.customer_lead = line.product_id.sale_delay
def _inverse_customer_lead(self):
for line in self:
if line.state == 'sale' and not line.order_id.commitment_date:
# Propagate deadline on related stock move
line.move_ids.date_deadline = line.order_id.date_order + timedelta(days=line.customer_lead or 0.0)
def _prepare_procurement_values(self, group_id=False):
""" Prepare specific key for moves or other components that will be created from a stock rule
+1
View File
@@ -10,3 +10,4 @@ from . import test_sale_order_dates
from . import test_sale_stock_multicompany
from . import test_sale_stock_accrued_entries
from . import test_sale_stock_access_rights
from . import test_create_perf
@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import time
from odoo.tests import common, tagged
from odoo.tests.common import users, warmup
_logger = logging.getLogger(__name__)
@tagged('so_batch_perf')
class TestPERF(common.TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.ENTITIES = 50
cls.products = cls.env['product.product'].create([{
'name': 'Product %s' % i,
'list_price': 1 + 10 * i,
'type': 'service',
} for i in range(10)])
cls.partners = cls.env['res.partner'].create([{
'name': 'Partner %s' % i,
} for i in range(cls.ENTITIES)])
cls.salesmans = cls.env.ref('base.user_admin') | cls.env.ref('base.user_demo')
cls.env['base'].flush()
@users('admin')
@warmup
def test_sales_orders_batch_creation_perf(self):
MSG = "Model %s, %i records, %s, time %.2f"
vals_list = [{
"partner_id": self.partners[i].id,
"user_id": self.salesmans[i % 2].id,
"order_line": [
(0, 0, {"display_type": "line_note", "name": "NOTE"})
] + [
(0, 0, {'product_id': product.id}) for product in self.products
],
} for i in range(self.ENTITIES)]
with self.assertQueryCount(admin=1100):
t0 = time.time()
self.env["sale.order"].create(vals_list)
t1 = time.time()
_logger.info(MSG, 'sale.order', self.ENTITIES, "BATCH", t1 - t0)
self.env.cr.flush()
_logger.info(MSG, 'sale.order', self.ENTITIES, "FLUSH", time.time() - t1)
@@ -28,16 +28,17 @@ class TestSaleStockLeadTime(TestSaleCommon, ValuationReconciliationTestCommon):
# Create sale order of product_1
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'partner_invoice_id': self.partner_a.id,
'partner_shipping_id': self.partner_a.id,
'pricelist_id': self.company_data['default_pricelist'].id,
'picking_policy': 'direct',
'warehouse_id': self.company_data['default_warehouse'].id,
'order_line': [(0, 0, {'name': self.test_product_order.name,
'product_id': self.test_product_order.id,
'product_uom_qty': 10,
'product_uom': self.env.ref('uom.product_uom_unit').id,
'customer_lead': self.test_product_order.sale_delay})]})
'order_line': [(0, 0, {
'product_id': self.test_product_order.id,
'product_uom_qty': 10,
'product_uom': self.env.ref('uom.product_uom_unit').id,
})]
})
self.assertEqual(order.order_line.customer_lead, self.test_product_order.sale_delay)
# Confirm our standard sale order
order.action_confirm()
@@ -46,8 +47,8 @@ class TestSaleStockLeadTime(TestSaleCommon, ValuationReconciliationTestCommon):
self.assertTrue(order.picking_ids, "Picking should be created.")
# Check schedule date of picking
out_date = fields.Datetime.from_string(order.date_order) + timedelta(days=self.test_product_order.sale_delay) - timedelta(days=self.env.company.security_lead)
min_date = fields.Datetime.from_string(order.picking_ids[0].scheduled_date)
out_date = order.date_order + timedelta(days=self.test_product_order.sale_delay) - timedelta(days=self.env.company.security_lead)
min_date = order.picking_ids[0].scheduled_date
self.assertTrue(abs(min_date - out_date) <= timedelta(seconds=1), 'Schedule date of picking should be equal to: order date + Customer Lead Time - Sales Safety Days.')
def test_01_product_route_level_delays(self):
@@ -206,3 +207,34 @@ class TestSaleStockLeadTime(TestSaleCommon, ValuationReconciliationTestCommon):
self.assertEqual(pack.date_deadline, new_deadline)
new_deadline -= timedelta(days=pick.move_ids.rule_id.delay)
self.assertEqual(pick.date_deadline, new_deadline)
def test_03_product_company_level_delays(self):
"""Partial duplicate of test_02 to make sure there is no default value specified in sale
that disables the computation of the customer_lead.
"""
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'pricelist_id': self.company_data['default_pricelist'].id,
'picking_policy': 'direct',
'warehouse_id': self.company_data['default_warehouse'].id,
})
order_line = self.env['sale.order.line'].create({
'product_id': self.test_product_order.id,
'product_uom_qty': 10,
'product_uom': self.env.ref('uom.product_uom_unit').id,
'order_id': order.id,
})
self.assertEqual(order_line.customer_lead, self.test_product_order.sale_delay)
# Confirm our standard sale order
order.action_confirm()
# Check the picking crated or not
self.assertTrue(order.picking_ids, "Picking should be created.")
# Check schedule date of picking
out_date = order.date_order + timedelta(days=self.test_product_order.sale_delay) - timedelta(days=self.env.company.security_lead)
min_date = order.picking_ids[0].scheduled_date
self.assertTrue(abs(min_date - out_date) <= timedelta(seconds=1), 'Schedule date of picking should be equal to: order date + Customer Lead Time - Sales Safety Days.')
+3 -2
View File
@@ -53,7 +53,8 @@ class Project(models.Model):
compute="_compute_timesheet_product_id", store=True, readonly=False,
default=_default_timesheet_product_id)
warning_employee_rate = fields.Boolean(compute='_compute_warning_employee_rate')
partner_id = fields.Many2one(compute='_compute_partner_id', store=True, readonly=False)
partner_id = fields.Many2one(
compute='_compute_partner_id', store=True, readonly=False)
@api.depends('sale_line_id', 'sale_line_employee_ids', 'allow_billable')
def _compute_pricing_type(self):
@@ -482,7 +483,7 @@ class ProjectTask(models.Model):
super(ProjectTask, billable_tasks)._compute_sale_order_id()
(self - billable_tasks).sale_order_id = False
@api.depends('commercial_partner_id', 'sale_line_id.order_partner_id.commercial_partner_id', 'parent_id.sale_line_id', 'project_id.sale_line_id', 'allow_billable')
@api.depends('commercial_partner_id', 'sale_line_id.order_partner_id', 'parent_id.sale_line_id', 'project_id.sale_line_id', 'allow_billable')
def _compute_sale_line(self):
billable_tasks = self.filtered('allow_billable')
(self - billable_tasks).update({'sale_line_id': False})
@@ -10,7 +10,9 @@ class ProjectProductEmployeeMap(models.Model):
project_id = fields.Many2one('project.project', "Project", required=True)
employee_id = fields.Many2one('hr.employee', "Employee", required=True)
sale_line_id = fields.Many2one('sale.order.line', "Sales Order Item", compute="_compute_sale_line_id", store=True, readonly=False,
sale_line_id = fields.Many2one(
'sale.order.line', "Sales Order Item",
compute="_compute_sale_line_id", store=True, readonly=False,
domain="""[
('is_service', '=', True),
('is_expense', '=', False),
-2
View File
@@ -351,10 +351,8 @@ class TestCommonSaleTimesheet(TestSaleCommon):
})
self.env['sale.order.line'].create([{
'order_id': self.so.id,
'name': self.product_delivery_timesheet1.name,
'product_id': self.product_delivery_timesheet1.id,
'product_uom_qty': 10,
'price_unit': self.product_delivery_timesheet1.list_price
}, {
'order_id': self.so.id,
'product_id': self.product_delivery_timesheet2.id,
+10 -15
View File
@@ -9,6 +9,16 @@ class TestCommonReporting(TestCommonSaleTimesheet):
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
# Note: According to the installed module, a default pricelist is set
# on the created company (see create override on product module)
# Force the pricelist on the company to avoid having a random one
# with advanced discount configuration, etc...
cls.env['ir.property']._set_default(
'property_product_pricelist', 'res.partner',
cls.company_data['default_pricelist'],
cls.company_data['company'],
)
# expense product
cls.product_expense = cls.env['product.product'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'name': "Expense service",
@@ -52,19 +62,13 @@ class TestCommonReporting(TestCommonSaleTimesheet):
'analytic_account_id': cls.analytic_account_1.id,
})
cls.so_line_deliver_project = cls.env['sale.order.line'].create({
'name': cls.product_delivery_timesheet3.name,
'product_id': cls.product_delivery_timesheet3.id,
'product_uom_qty': 5,
'product_uom': cls.product_delivery_timesheet3.uom_id.id,
'price_unit': cls.product_delivery_timesheet3.list_price,
'order_id': cls.sale_order_1.id,
})
cls.so_line_deliver_task = cls.env['sale.order.line'].create({
'name': cls.product_delivery_timesheet2.name,
'product_id': cls.product_delivery_timesheet2.id,
'product_uom_qty': 7,
'product_uom': cls.product_delivery_timesheet2.uom_id.id,
'price_unit': cls.product_delivery_timesheet2.list_price,
'order_id': cls.sale_order_1.id,
})
@@ -75,19 +79,13 @@ class TestCommonReporting(TestCommonSaleTimesheet):
'analytic_account_id': cls.analytic_account_2.id,
})
cls.so_line_order_project = cls.env['sale.order.line'].create({
'name': cls.product_order_timesheet3.name,
'product_id': cls.product_order_timesheet3.id,
'product_uom_qty': 5,
'product_uom': cls.product_order_timesheet3.uom_id.id,
'price_unit': cls.product_order_timesheet3.list_price,
'order_id': cls.sale_order_2.id,
})
cls.so_line_order_task = cls.env['sale.order.line'].create({
'name': cls.product_order_timesheet2.name,
'product_id': cls.product_order_timesheet2.id,
'product_uom_qty': 7,
'product_uom': cls.product_order_timesheet2.uom_id.id,
'price_unit': cls.product_order_timesheet2.list_price,
'order_id': cls.sale_order_2.id,
})
@@ -98,11 +96,8 @@ class TestCommonReporting(TestCommonSaleTimesheet):
'analytic_account_id': cls.analytic_account_3.id,
})
cls.so_line_deliver_manual_project = cls.env['sale.order.line'].create({
'name': cls.product_delivery_manual3.name,
'product_id': cls.product_delivery_manual3.id,
'product_uom_qty': 11,
'product_uom': cls.product_delivery_manual3.uom_id.id,
'price_unit': cls.product_delivery_manual3.list_price,
'order_id': cls.sale_order_3.id,
})
@@ -36,20 +36,14 @@ class TestProjectBilling(TestCommonSaleTimesheet):
})
cls.so1_line_order_no_task = SaleOrderLine.create({
'name': cls.product_order_timesheet1.name,
'product_id': cls.product_order_timesheet1.id,
'product_uom_qty': 10,
'product_uom': cls.product_order_timesheet1.uom_id.id,
'price_unit': cls.product_order_timesheet1.list_price,
'order_id': cls.sale_order_1.id,
})
cls.so1_line_deliver_no_task = SaleOrderLine.create({
'name': cls.product_delivery_timesheet1.name,
'product_id': cls.product_delivery_timesheet1.id,
'product_uom_qty': 10,
'product_uom': cls.product_delivery_timesheet1.uom_id.id,
'price_unit': cls.product_delivery_timesheet1.list_price,
'order_id': cls.sale_order_1.id,
})
# Sale Order 2, creates 2 project billed at task rate
@@ -60,19 +54,13 @@ class TestProjectBilling(TestCommonSaleTimesheet):
})
cls.so2_line_deliver_project_task = SaleOrderLine.create({
'order_id': cls.sale_order_2.id,
'name': cls.product_delivery_timesheet3.name,
'product_id': cls.product_delivery_timesheet3.id,
'product_uom_qty': 5,
'product_uom': cls.product_delivery_timesheet3.uom_id.id,
'price_unit': cls.product_delivery_timesheet3.list_price
})
cls.so2_line_deliver_project_template = SaleOrderLine.create({
'order_id': cls.sale_order_2.id,
'name': cls.product_delivery_timesheet5.name,
'product_id': cls.product_delivery_timesheet5.id,
'product_uom_qty': 7,
'product_uom': cls.product_delivery_timesheet5.uom_id.id,
'price_unit': cls.product_delivery_timesheet5.list_price
})
cls.sale_order_2.action_confirm()
@@ -230,7 +218,7 @@ class TestProjectBilling(TestCommonSaleTimesheet):
self.assertFalse(line1.project_id, "Sale line 1 should be linked to the 'non billable' project")
self.assertEqual(line2.project_id, self.project_non_billable, "Sale line 3 should be linked to the 'non billable' project")
self.assertEqual(line1.price_unit, 15, "The unit price of SOL 1 should be 15")
self.assertEqual(line1.price_unit, 15.0, "The unit price of SOL 1 should be 15.0")
self.assertEqual(line1.product_uom_qty, 3, "The ordered qty of SOL 1 should be 3")
self.assertEqual(line2.product_uom_qty, 2, "The ordered qty of SOL 2 should be 2")
@@ -54,26 +54,16 @@ class TestReInvoice(TestCommonSaleTimesheet):
""" Test vendor bill at cost for product based on ordered and delivered quantities. """
# create SO line and confirm SO (with only one line)
sale_order_line1 = self.env['sale.order.line'].create({
'name': self.company_data['product_order_cost'].name,
'product_id': self.company_data['product_order_cost'].id,
'product_uom_qty': 2,
'product_uom': self.company_data['product_order_cost'].uom_id.id,
'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,
'product_uom_qty': 4,
'product_uom': self.company_data['product_delivery_cost'].uom_id.id,
'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.onchange_partner_id()
self.sale_order._compute_tax_id()
self.sale_order.action_confirm()
self.assertEqual(sale_order_line1.qty_delivered_method, 'timesheet', "Delivered quantity of 'service' SO line should be computed by timesheet amount")
@@ -156,26 +146,17 @@ class TestReInvoice(TestCommonSaleTimesheet):
"""
# create SO line and confirm SO (with only one line)
sale_order_line1 = 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,
'product_uom_qty': 2,
'qty_delivered': 1,
'product_uom': self.company_data['product_delivery_sales_price'].uom_id.id,
'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,
'product_uom_qty': 3,
'qty_delivered': 1,
'product_uom': self.company_data['product_order_sales_price'].uom_id.id,
'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._compute_tax_id()
self.sale_order.action_confirm()
# let's log some timesheets (on the project created by sale_order_line1)
@@ -252,15 +233,11 @@ class TestReInvoice(TestCommonSaleTimesheet):
""" Test invoicing vendor bill with no policy. Check nothing happen. """
# confirm SO
sale_order_line = self.env['sale.order.line'].create({
'name': self.company_data['product_order_no'].name,
'product_id': self.company_data['product_order_no'].id,
'product_uom_qty': 2,
'qty_delivered': 1,
'product_uom': self.company_data['product_order_no'].uom_id.id,
'price_unit': self.company_data['product_order_no'].list_price,
'order_id': self.sale_order.id,
})
self.sale_order._compute_tax_id()
self.sale_order.action_confirm()
# create invoice lines and validate it
@@ -420,11 +420,8 @@ class TestReporting(TestCommonReporting):
# simulate the auto creation of the SO line for expense, like we confirm a vendor bill.
so_line_expense = self.env['sale.order.line'].create({
'name': self.product_expense.name,
'product_id': self.product_expense.id,
'product_uom_qty': 0.0,
'product_uom': self.product_expense.uom_id.id,
'price_unit': self.product_expense.list_price, # reinvoice at sales price
'order_id': self.sale_order_1.id,
'is_expense': True,
})
@@ -575,11 +572,8 @@ class TestReporting(TestCommonReporting):
# simulate the auto creation of the SO line for expense, like we confirm a vendor bill.
so_line_expense = self.env['sale.order.line'].create({
'name': self.product_expense.name,
'product_id': self.product_expense.id,
'product_uom_qty': 0.0,
'product_uom': self.product_expense.uom_id.id,
'price_unit': self.product_expense.list_price, # reinvoice at sales price
'order_id': self.sale_order_2.id,
'is_expense': True,
})
@@ -701,11 +695,8 @@ class TestReporting(TestCommonReporting):
currency = self.env.company.currency_id
rounding = currency.rounding
so_line_deliver_no_task = self.env['sale.order.line'].create({
'name': self.product_delivery_manual1.name,
'product_id': self.product_delivery_manual1.id,
'product_uom_qty': 50,
'product_uom': self.product_delivery_manual1.uom_id.id,
'price_unit': self.product_delivery_manual1.list_price,
'order_id': self.sale_order_2.id,
})
so_line_deliver_no_task.write({'qty_delivered': 1.0})
@@ -27,8 +27,6 @@ class TestSaleService(TestCommonSaleTimesheet):
'name': self.product_delivery_timesheet2.name,
'product_id': self.product_delivery_timesheet2.id,
'product_uom_qty': 50,
'product_uom': self.product_delivery_timesheet2.uom_id.id,
'price_unit': self.product_delivery_timesheet2.list_price
})
self.sale_order.order_line._compute_product_updatable()
@@ -79,11 +77,8 @@ class TestSaleService(TestCommonSaleTimesheet):
})
self.env['sale.order.line'].create({
'name': product_service_task.name,
'product_id': product_service_task.id,
'product_uom_qty': 10,
'product_uom': product_service_task.uom_id.id,
'price_unit': product_service_task.list_price,
'order_id': self.sale_order.id,
})
@@ -102,11 +97,9 @@ class TestSaleService(TestCommonSaleTimesheet):
uom_days = self.env.ref('uom.product_uom_day')
sale_order_line = self.env['sale.order.line'].create({
'order_id': self.sale_order.id,
'name': self.product_delivery_timesheet3.name,
'product_id': self.product_delivery_timesheet3.id,
'product_uom_qty': 5,
'product_uom': uom_days.id,
'price_unit': self.product_delivery_timesheet3.list_price
})
self.sale_order.action_confirm()
task = self.env['project.task'].search([('sale_line_id', '=', sale_order_line.id)])
@@ -134,14 +127,10 @@ class TestSaleService(TestCommonSaleTimesheet):
def test_task_so_line_assignation(self):
# create SO line and confirm it
so_line_deliver_global_project = self.env['sale.order.line'].create({
'name': self.product_delivery_timesheet2.name,
'product_id': self.product_delivery_timesheet2.id,
'product_uom_qty': 10,
'product_uom': self.product_delivery_timesheet2.uom_id.id,
'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)])
@@ -180,14 +169,10 @@ class TestSaleService(TestCommonSaleTimesheet):
def test_delivered_quantity(self):
# create SO line and confirm it
so_line_deliver_new_task_project = self.env['sale.order.line'].create({
'name': self.product_delivery_timesheet3.name,
'product_id': self.product_delivery_timesheet3.id,
'product_uom_qty': 10,
'product_uom': self.product_delivery_timesheet3.uom_id.id,
'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)])
@@ -231,11 +216,8 @@ class TestSaleService(TestCommonSaleTimesheet):
the ordered quantity of a SO line that have created a task should update the planned hours of this task.
"""
so_line1 = self.env['sale.order.line'].create({
'name': self.product_delivery_timesheet3.name,
'product_id': self.product_delivery_timesheet3.id,
'product_uom_qty': 7,
'product_uom': self.product_delivery_timesheet3.uom_id.id,
'price_unit': self.product_delivery_timesheet3.list_price,
'order_id': self.sale_order.id,
})
@@ -318,43 +300,28 @@ class TestSaleService(TestCommonSaleTimesheet):
# create 5 so lines
so_line1 = self.env['sale.order.line'].create({
'name': self.product_delivery_timesheet5.name,
'product_id': self.product_delivery_timesheet5.id,
'product_uom_qty': 11,
'product_uom': self.product_delivery_timesheet5.uom_id.id,
'price_unit': self.product_delivery_timesheet5.list_price,
'order_id': self.sale_order.id,
})
so_line2 = self.env['sale.order.line'].create({
'name': self.product_order_timesheet4.name,
'product_id': self.product_order_timesheet4.id,
'product_uom_qty': 10,
'product_uom': self.product_order_timesheet4.uom_id.id,
'price_unit': self.product_order_timesheet4.list_price,
'order_id': self.sale_order.id,
})
so_line3 = self.env['sale.order.line'].create({
'name': self.product_delivery_timesheet5.name,
'product_id': self.product_delivery_timesheet5.id,
'product_uom_qty': 5,
'product_uom': self.product_delivery_timesheet5.uom_id.id,
'price_unit': self.product_delivery_timesheet5.list_price,
'order_id': self.sale_order.id,
})
so_line4 = self.env['sale.order.line'].create({
'name': self.product_delivery_manual3.name,
'product_id': self.product_delivery_manual3.id,
'product_uom_qty': 4,
'product_uom': self.product_delivery_manual3.uom_id.id,
'price_unit': self.product_delivery_manual3.list_price,
'order_id': self.sale_order.id,
})
so_line5 = self.env['sale.order.line'].create({
'name': product_deli_ts_tmpl.name,
'product_id': product_deli_ts_tmpl.id,
'product_uom_qty': 8,
'product_uom': product_deli_ts_tmpl.uom_id.id,
'price_unit': product_deli_ts_tmpl.list_price,
'order_id': self.sale_order.id,
})
@@ -412,27 +379,18 @@ class TestSaleService(TestCommonSaleTimesheet):
self.assertEqual(self.sale_order.analytic_account_id, self.analytic_account_sale, "Changing the project on the SO should set the analytic account accordingly.")
so_line1 = self.env['sale.order.line'].create({
'name': self.product_order_timesheet3.name,
'product_id': self.product_order_timesheet3.id,
'product_uom_qty': 11,
'product_uom': self.product_order_timesheet3.uom_id.id,
'price_unit': self.product_order_timesheet3.list_price,
'order_id': self.sale_order.id,
})
so_line2 = self.env['sale.order.line'].create({
'name': self.product_order_timesheet3.name,
'product_id': self.product_order_timesheet3.id,
'product_uom_qty': 10,
'product_uom': self.product_order_timesheet3.uom_id.id,
'price_unit': self.product_order_timesheet3.list_price,
'order_id': self.sale_order.id,
})
so_line3 = self.env['sale.order.line'].create({
'name': self.product_order_timesheet4.name,
'product_id': self.product_order_timesheet4.id,
'product_uom_qty': 5,
'product_uom': self.product_order_timesheet4.uom_id.id,
'price_unit': self.product_order_timesheet4.list_price,
'order_id': self.sale_order.id,
})
@@ -469,11 +427,8 @@ class TestSaleService(TestCommonSaleTimesheet):
"""
so_line1 = self.env['sale.order.line'].create({
'name': self.product_order_timesheet3.name,
'product_id': self.product_order_timesheet3.id,
'product_uom_qty': 10,
'product_uom': self.product_order_timesheet3.uom_id.id,
'price_unit': self.product_order_timesheet3.list_price,
'order_id': self.sale_order.id,
})
@@ -495,23 +450,16 @@ class TestSaleService(TestCommonSaleTimesheet):
""" Test if subtasks and tasks are billed on the correct SO line """
# create SO line and confirm it
so_line_deliver_new_task_project = self.env['sale.order.line'].create({
'name': self.product_delivery_timesheet3.name,
'product_id': self.product_delivery_timesheet3.id,
'product_uom_qty': 10,
'product_uom': self.product_delivery_timesheet3.uom_id.id,
'price_unit': self.product_delivery_timesheet3.list_price,
'order_id': self.sale_order.id,
})
so_line_deliver_new_task_project_2 = self.env['sale.order.line'].create({
'name': self.product_delivery_timesheet3.name + "(2)",
'product_id': self.product_delivery_timesheet3.id,
'product_uom_qty': 10,
'product_uom': self.product_delivery_timesheet3.uom_id.id,
'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
@@ -556,11 +504,8 @@ class TestSaleService(TestCommonSaleTimesheet):
""" Changing the ordered quantity of a SO line that have created a task should update the planned hours of this task """
sale_order_line = self.env['sale.order.line'].create({
'order_id': self.sale_order.id,
'name': self.product_delivery_timesheet2.name,
'product_id': self.product_delivery_timesheet2.id,
'product_uom_qty': 50,
'product_uom': self.product_delivery_timesheet2.uom_id.id,
'price_unit': self.product_delivery_timesheet2.list_price
})
self.sale_order.action_confirm()
@@ -580,11 +525,8 @@ class TestSaleService(TestCommonSaleTimesheet):
def test_copy_billable_project_and_task(self):
sale_order_line = self.env['sale.order.line'].create({
'order_id': self.sale_order.id,
'name': self.product_delivery_timesheet3.name,
'product_id': self.product_delivery_timesheet3.id,
'product_uom_qty': 5,
'product_uom': self.product_delivery_timesheet3.uom_id.id,
'price_unit': self.product_delivery_timesheet3.list_price
})
self.sale_order.action_confirm()
task = self.env['project.task'].search([('sale_line_id', '=', sale_order_line.id)])
@@ -35,23 +35,15 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
'pricelist_id': self.company_data['default_pricelist'].id,
})
so_line_ordered_project_only = self.env['sale.order.line'].create({
'name': self.product_order_timesheet4.name,
'product_id': self.product_order_timesheet4.id,
'product_uom_qty': 10,
'product_uom': self.product_order_timesheet4.uom_id.id,
'price_unit': self.product_order_timesheet4.list_price,
'order_id': sale_order.id,
})
so_line_ordered_global_project = self.env['sale.order.line'].create({
'name': self.product_order_timesheet2.name,
'product_id': self.product_order_timesheet2.id,
'product_uom_qty': 50,
'product_uom': self.product_order_timesheet2.uom_id.id,
'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)])
@@ -111,11 +103,8 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
# add so line with produdct "create task in new project".
so_line_ordered_task_in_project = self.env['sale.order.line'].create({
'name': self.product_order_timesheet3.name,
'product_id': self.product_order_timesheet3.id,
'product_uom_qty': 3,
'product_uom': self.product_order_timesheet3.uom_id.id,
'price_unit': self.product_order_timesheet3.list_price,
'order_id': sale_order.id,
})
@@ -173,23 +162,15 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
'pricelist_id': self.company_data['default_pricelist'].id,
})
so_line_deliver_global_project = self.env['sale.order.line'].create({
'name': self.product_delivery_timesheet2.name,
'product_id': self.product_delivery_timesheet2.id,
'product_uom_qty': 50,
'product_uom': self.product_delivery_timesheet2.uom_id.id,
'price_unit': self.product_delivery_timesheet2.list_price,
'order_id': sale_order.id,
})
so_line_deliver_task_project = self.env['sale.order.line'].create({
'name': self.product_delivery_timesheet3.name,
'product_id': self.product_delivery_timesheet3.id,
'product_uom_qty': 20,
'product_uom': self.product_delivery_timesheet3.uom_id.id,
'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()
@@ -252,11 +233,8 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
# add a line on SO
so_line_deliver_only_project = self.env['sale.order.line'].create({
'name': self.product_delivery_timesheet4.name,
'product_id': self.product_delivery_timesheet4.id,
'product_uom_qty': 5,
'product_uom': self.product_delivery_timesheet4.uom_id.id,
'price_unit': self.product_delivery_timesheet4.list_price,
'order_id': sale_order.id,
})
self.assertEqual(len(sale_order.project_ids), 2, "No new project should have been created by the SO, when selling 'project only' product, since it reuse the one from 'new task in new project'.")
@@ -307,19 +285,13 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
'pricelist_id': self.company_data['default_pricelist'].id,
})
so_line_manual_global_project = self.env['sale.order.line'].create({
'name': self.product_delivery_manual2.name,
'product_id': self.product_delivery_manual2.id,
'product_uom_qty': 50,
'product_uom': self.product_delivery_manual2.uom_id.id,
'price_unit': self.product_delivery_manual2.list_price,
'order_id': sale_order.id,
})
so_line_manual_only_project = self.env['sale.order.line'].create({
'name': self.product_delivery_manual4.name,
'product_id': self.product_delivery_manual4.id,
'product_uom_qty': 20,
'product_uom': self.product_delivery_manual4.uom_id.id,
'price_unit': self.product_delivery_manual4.list_price,
'order_id': sale_order.id,
})
@@ -400,23 +372,15 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
'display_type': 'line_section',
})
so_line_deliver_global_project = self.env['sale.order.line'].create({
'name': self.product_delivery_timesheet2.name,
'product_id': self.product_delivery_timesheet2.id,
'product_uom_qty': 50,
'product_uom': self.product_delivery_timesheet2.uom_id.id,
'price_unit': self.product_delivery_timesheet2.list_price,
'order_id': sale_order.id,
})
so_line_deliver_task_project = self.env['sale.order.line'].create({
'name': self.product_delivery_timesheet3.name,
'product_id': self.product_delivery_timesheet3.id,
'product_uom_qty': 20,
'product_uom': self.product_delivery_timesheet3.uom_id.id,
'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()
@@ -609,19 +573,13 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
'pricelist_id': self.company_data['default_pricelist'].id,
})
so1_product_global_project_so_line = self.env['sale.order.line'].create({
'name': self.product_delivery_timesheet2.name,
'product_id': self.product_delivery_timesheet2.id,
'product_uom_qty': 50,
'product_uom': self.product_delivery_timesheet2.uom_id.id,
'price_unit': self.product_delivery_timesheet2.list_price,
'order_id': sale_order1.id,
})
so2_product_global_project_so_line = self.env['sale.order.line'].create({
'name': self.product_delivery_timesheet2.name,
'product_id': self.product_delivery_timesheet2.id,
'product_uom_qty': 20,
'product_uom': self.product_delivery_timesheet2.uom_id.id,
'price_unit': self.product_delivery_timesheet2.list_price,
'order_id': sale_order2.id,
})
@@ -680,11 +638,8 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
uom_days = self.env.ref('uom.product_uom_day')
sale_order_line = self.env['sale.order.line'].create({
'order_id': sale_order.id,
'name': self.product_order_timesheet3.name,
'product_id': self.product_order_timesheet3.id,
'product_uom_qty': 1,
'product_uom': uom_days.id,
'price_unit': self.product_order_timesheet3.list_price,
})
sale_order.action_confirm()
task = sale_order_line.task_id
@@ -37,10 +37,8 @@ class TestUpsellWarning(TestCommonSaleTimesheet):
self.env['sale.order.line'].create({
'order_id': so.id,
'name': self.product_order_timesheet1.name,
'product_id': self.product_order_timesheet1.id,
'product_uom_qty': 10,
'price_unit': self.product_order_timesheet1.list_price,
})
so.action_confirm()
@@ -117,11 +117,8 @@ class ProjectCreateSalesOrder(models.TransientModel):
'client_order_ref': self.project_id.name,
'company_id': self.project_id.company_id.id,
})
sale_order.onchange_partner_id()
sale_order.onchange_partner_shipping_id()
# rewrite the user as the onchange_partner_id erases it
sale_order.write({'user_id': self.project_id.user_id.id})
sale_order.onchange_user_id()
# create the sale lines, the map (optional), and assign existing timesheet to sale lines
self._make_billable(sale_order)
@@ -173,6 +170,8 @@ class ProjectCreateSalesOrder(models.TransientModel):
sale_order_line.with_context({'no_update_planned_hours': True}).write({
'product_uom_qty': sale_order_line.qty_delivered
})
# Avoid recomputing price_unit
self.env.remove_to_compute(self.env['sale.order.line']._fields['price_unit'], sale_order_line)
self.project_id.write({
'sale_order_id': sale_order.id,
@@ -245,9 +244,10 @@ class ProjectCreateSalesOrder(models.TransientModel):
'so_line': map_entry.sale_line_id.id
})
map_entry.sale_line_id.with_context({'no_update_planned_hours': True}).write({
'product_uom_qty': map_entry.sale_line_id.qty_delivered
'product_uom_qty': map_entry.sale_line_id.qty_delivered,
})
# Avoid recomputing price_unit
self.env.remove_to_compute(self.env['sale.order.line']._fields['price_unit'], map_entry.sale_line_id)
return map_entries
+2 -1
View File
@@ -116,7 +116,8 @@ class StockMove(models.Model):
"The other possibility allows you to directly create a procurement on the source location (and thus ignore "
"its current stock) to gather products. If we want to chain moves and have this one to wait for the previous, "
"this second option should be chosen.")
scrapped = fields.Boolean('Scrapped', related='location_dest_id.scrap_location', readonly=True, store=True)
scrapped = fields.Boolean(
'Scrapped', related='location_dest_id.scrap_location', readonly=True, store=True)
scrap_ids = fields.One2many('stock.scrap', 'move_id')
group_id = fields.Many2one('procurement.group', 'Procurement Group', default=_default_group_id)
rule_id = fields.Many2one(
@@ -55,10 +55,8 @@ class TestDropship(common.TransactionCase):
# Create a new so line
sol2 = self.env['sale.order.line'].create({
'order_id': so.id,
'name': prod.name,
'product_id': prod.id,
'product_uom_qty': 3.00,
'product_uom': prod.uom_id.id,
'price_unit': 12,
})
# there is a new line
-2
View File
@@ -835,7 +835,6 @@ class WebsiteSale(http.Controller):
partner_id = self._checkout_form_save(mode, post, kw)
if mode[1] == 'billing':
order.partner_id = partner_id
order.with_context(not_self_saleperson=True).onchange_partner_id()
# This is the *only* thing that the front end user will see/edit anyway when choosing billing address
order.partner_invoice_id = partner_id
if not kw.get('use_same'):
@@ -925,7 +924,6 @@ class WebsiteSale(http.Controller):
if redirection:
return redirection
order.onchange_partner_shipping_id()
order.order_line._compute_tax_id()
request.session['sale_last_order_id'] = order.id
request.website.sale_get_order(update_pricelist=True)
@@ -183,7 +183,7 @@ class ProductTemplate(models.Model):
company_id = current_website.company_id
tax_display = self.user_has_groups('account.group_show_line_subtotals_tax_excluded') and 'total_excluded' or 'total_included'
fpos = self.env['account.fiscal.position'].sudo().get_fiscal_position(partner.id)
fpos = self.env['account.fiscal.position'].sudo()._get_fiscal_position(partner)
product_taxes = product.sudo().taxes_id.filtered(lambda x: x.company_id == company_id)
taxes = fpos.map_tax(product_taxes)
@@ -33,6 +33,13 @@ class ResConfigSettings(models.TransientModel):
cart_add_on_page = fields.Boolean("Stay on page after adding to cart", related='website_id.cart_add_on_page', readonly=False)
terms_url = fields.Char(compute='_compute_terms_url', string="URL", help="A preview will be available at this URL.")
module_delivery = fields.Boolean(
compute='_compute_module_delivery', store=True, readonly=False)
module_website_sale_delivery = fields.Boolean(
compute='_compute_module_delivery', store=True, readonly=False)
group_product_pricelist = fields.Boolean(
compute='_compute_group_product_pricelist', store=True, readonly=False)
@api.depends('website_id')
def _compute_terms_url(self):
for record in self:
@@ -53,30 +60,17 @@ class ResConfigSettings(models.TransientModel):
)
return res
@api.onchange('sale_delivery_settings')
def _onchange_sale_delivery_settings(self):
if self.sale_delivery_settings == 'none':
self.update({
'module_delivery': False,
'module_website_sale_delivery': False,
})
elif self.sale_delivery_settings == 'internal':
self.update({
'module_delivery': True,
'module_website_sale_delivery': False,
})
else:
self.update({
'module_delivery': True,
'module_website_sale_delivery': True,
})
@api.depends('sale_delivery_settings')
def _compute_module_delivery(self):
for wizard in self:
wizard.module_delivery = wizard.sale_delivery_settings in ['internal', 'website']
wizard.module_website_sale_delivery = wizard.sale_delivery_settings == 'website'
@api.onchange('group_discount_per_so_line')
def _onchange_group_discount_per_so_line(self):
if self.group_discount_per_so_line:
self.update({
'group_product_pricelist': True,
})
@api.depends('group_discount_per_so_line')
def _compute_group_product_pricelist(self):
self.filtered(lambda w: w.group_discount_per_so_line).update({
'group_product_pricelist': True,
})
def action_update_terms(self):
self.ensure_one()
+3 -3
View File
@@ -30,11 +30,11 @@ class SaleOrder(models.Model):
help='Website through which this order was placed.')
@api.model
def _default_note_url(self):
def _get_note_url(self):
website = self.env['website'].get_current_website()
if website:
return website.get_base_url()
return super()._default_note_url()
return super()._get_note_url()
@api.depends('order_line')
def _compute_website_order_line(self):
@@ -116,7 +116,7 @@ class SaleOrder(models.Model):
discount = 0
if order.pricelist_id.discount_policy == 'without_discount':
# This part is pretty much a copy-paste of the method '_onchange_discount' of
# This part is pretty much a copy-paste of the method '_compute_discount' of
# 'sale.order.line'.
price, rule_id = order.pricelist_id.with_context(product_context).get_product_price_rule(product, qty or 1.0, order.partner_id)
pu, currency = request.env['sale.order.line'].with_context(product_context)._get_real_price_currency(product, rule_id, qty, product.uom_id, order.pricelist_id.id)
+7 -12
View File
@@ -277,7 +277,7 @@ class Website(models.Model):
fpos_id = (
self.env['account.fiscal.position'].sudo()
.with_company(sale_order.company_id.id)
.get_fiscal_position(sale_order.partner_id.id, delivery_id=sale_order.partner_shipping_id.id)
._get_fiscal_position(sale_order.partner_id, delivery=sale_order.partner_shipping_id)
).id
if sale_order.fiscal_position_id.id != fpos_id:
sale_order = None
@@ -305,16 +305,11 @@ class Website(models.Model):
sale_order = self.env['sale.order'].with_company(request.website.company_id.id).with_user(SUPERUSER_ID).create(so_data)
# set fiscal position
if request.website.partner_id.id != partner.id:
sale_order.onchange_partner_shipping_id()
else: # For public user, fiscal position based on geolocation
if request.website.partner_id.id == partner.id: # For public user, fiscal position based on geolocation
country_code = request.session['geoip'].get('country_code')
if country_code:
country_id = request.env['res.country'].search([('code', '=', country_code)], limit=1).id
sale_order.fiscal_position_id = request.env['account.fiscal.position'].sudo().with_company(request.website.company_id.id)._get_fpos_by_region(country_id)
else:
# if no geolocation, use the public user fp
sale_order.onchange_partner_shipping_id()
request.session['sale_order_id'] = sale_order.id
@@ -333,11 +328,11 @@ class Website(models.Model):
fiscal_position = sale_order.fiscal_position_id.id
# change the partner, and trigger the onchange
sale_order.write({'partner_id': partner.id})
sale_order.with_context(not_self_saleperson=True).onchange_partner_id()
sale_order.write({'partner_invoice_id': partner.id})
sale_order.onchange_partner_shipping_id() # fiscal position
sale_order['payment_term_id'] = self.sale_get_payment_term(partner)
sale_order.write({
'partner_id': partner.id,
'partner_invoice_id': partner.id,
'payment_term_id': self.sale_get_payment_term(partner),
})
# check the pricelist : update it if the pricelist is not the 'forced' one
values = {}
@@ -308,7 +308,6 @@ class TestUi(HttpCaseWithUserDemo, HttpCaseWithUserPortal):
# activate b2c
config = self.env['res.config.settings'].create({})
config.show_line_subtotals_tax_selection = "tax_included"
config._onchange_sale_tax()
config.execute()
self.start_tour("/", 'shop_list_view_b2c', login="admin")
@@ -6,7 +6,7 @@ from odoo.tests import tagged
from odoo.addons.website.tools import MockRequest
@tagged('post_install', '-at_install')
@tagged('post_install', '-at_install', 'product_attribute')
class TestWebsiteSaleProductAttributeValueConfig(TestSaleProductAttributeValueCommon):
def test_get_combination_info(self):
@@ -132,7 +132,7 @@ class TestWebsiteSaleProductAttributeValueConfig(TestSaleProductAttributeValueCo
self.assertEqual(round(combination_info['price'], 2), 434.78, "434.78$ + 0% tax (mapped from fp 15% -> 0% for BE)")
self.assertEqual(round(combination_info['list_price'], 2), 434.78, "434.78$ + 0% tax (mapped from fp 15% -> 0% for BE)")
@tagged('post_install', '-at_install')
@tagged('post_install', '-at_install', 'product_pricelist')
class TestWebsiteSaleProductPricelist(TestSaleProductAttributeValueCommon):
def test_cart_update_with_fpos(self):
# We will test that the mapping of an 10% included tax by a 0% by a fiscal position is taken into account when updating the cart
@@ -174,18 +174,14 @@ class TestWebsiteSaleProductPricelist(TestSaleProductAttributeValueCommon):
'partner_id': self.env.user.partner_id.id,
})
sol = self.env['sale.order.line'].create({
'name': test_product.name,
'product_id': test_product.product_variant_id.id,
'product_uom_qty': 1,
'product_uom': test_product.uom_id.id,
'price_unit': test_product.list_price,
'order_id': so.id,
'tax_id': [(6, 0, [tax10.id])],
})
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()
sol._compute_tax_id()
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%)")
@@ -31,7 +31,6 @@ class MondialRelay(http.Controller):
})
if order.partner_shipping_id != partner_shipping:
order.partner_shipping_id = partner_shipping
order.onchange_partner_shipping_id()
return {
'address': request.env['ir.qweb']._render('website_sale.address_on_payment', {
+4 -3
View File
@@ -224,9 +224,10 @@ class Partner(models.Model):
contact_address = fields.Char(compute='_compute_contact_address', string='Complete Address')
# technical field used for managing commercial fields
commercial_partner_id = fields.Many2one('res.partner', string='Commercial Entity',
compute='_compute_commercial_partner', recursive=True,
store=True, index=True)
commercial_partner_id = fields.Many2one(
'res.partner', string='Commercial Entity',
compute='_compute_commercial_partner', store=True,
recursive=True, index=True)
commercial_company_name = fields.Char('Company Name Entity', compute='_compute_commercial_company_name',
store=True)
company_name = fields.Char('Company Name')
+21
View File
@@ -8,6 +8,27 @@ from odoo.tests.common import TransactionCase, HttpCase
from odoo import Command
class PerformanceCommon(TransactionCase):
def assertModelCreateMulti(self, model_name, vals_list=None):
"""Ensures model_name records can be created in batch.
:param str model_name: name of the model on which batch record creation should be verified
:param list vals_list: example list of values, if needed
"""
vals_list = vals_list or [dict(), dict()]
if not isinstance(vals_list, list):
raise ValueError("Invalid arguments for assertModelCreateMulti")
assert len(vals_list) > 1, "Batch creation cannot be tested with only one value."
def patched_create(self, values_list):
if len(values_list) != len(vals_list):
raise ValueError(f"Model {model_name} doesn't support batch records creation.")
# Do not create anything
return self
with patch('odoo.models.BaseModel.create', patched_create):
self.env[model_name].create(vals_list)
class TransactionCaseWithUserDemo(TransactionCase):
def setUp(self):