[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:  Now:  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:
co-authored by
Victor Feyens vfe@odoo.com
commit
210c9daebc
@@ -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)]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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')
|
||||
@@ -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:
|
||||
|
||||
@@ -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, [
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user