diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 931ea0482a0..3a4c9117e44 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -468,7 +468,7 @@ class AccountChartTemplate(models.Model): # When we install the CoA of first company, set the currency to price types and pricelists if company.id == 1: - for reference in ['product.list_price', 'product.standard_price', 'product.list0']: + for reference in ['product.list_price', 'product.standard_price']: try: tmp2 = self.env.ref(reference).write({'currency_id': self.currency_id.id}) except ValueError: diff --git a/addons/delivery/tests/test_delivery_cost.py b/addons/delivery/tests/test_delivery_cost.py index 569b99cfdb2..236d004524e 100644 --- a/addons/delivery/tests/test_delivery_cost.py +++ b/addons/delivery/tests/test_delivery_cost.py @@ -16,7 +16,6 @@ class TestDeliveryCost(common.TransactionCase): self.Product = self.env['product.product'] self.partner_18 = self.env['res.partner'].create({'name': 'My Test Customer'}) - self.pricelist = self.env.ref('product.list0') self.product_4 = self.env['product.product'].create({'name': 'A product to deliver'}) self.product_uom_unit = self.env.ref('uom.product_uom_unit') self.product_delivery_normal = self.env['product.product'].create({ @@ -48,7 +47,6 @@ class TestDeliveryCost(common.TransactionCase): self.env.cr.execute( "UPDATE res_company SET currency_id = %s WHERE id = %s", [self.env.ref('base.USD').id, self.env.company.id]) - self.pricelist.currency_id = self.env.ref('base.USD').id self.env.user.groups_id |= self.env.ref('uom.group_uom') def test_00_delivery_cost(self): @@ -59,7 +57,6 @@ class TestDeliveryCost(common.TransactionCase): 'partner_id': self.partner_18.id, 'partner_invoice_id': self.partner_18.id, 'partner_shipping_id': self.partner_18.id, - 'pricelist_id': self.pricelist.id, 'order_line': [(0, 0, { 'name': 'PC Assamble + 2GB RAM', 'product_id': self.product_4.id, @@ -118,7 +115,6 @@ class TestDeliveryCost(common.TransactionCase): 'partner_id': self.partner_4.id, 'partner_invoice_id': self.partner_address_13.id, 'partner_shipping_id': self.partner_address_13.id, - 'pricelist_id': self.pricelist.id, 'order_line': [(0, 0, { 'name': 'Service on demand', 'product_id': self.product_consultant.id, @@ -276,7 +272,6 @@ class TestDeliveryCost(common.TransactionCase): self.env.user.groups_id += self.env.ref('product.group_product_pricelist') order_form = Form(self.env['sale.order'].with_context(tracking_disable=True)) order_form.partner_id = self.partner_18 - order_form.pricelist_id = self.pricelist order_form.fiscal_position_id = fiscal_position # Try adding delivery product as a normal product diff --git a/addons/delivery/tests/test_delivery_stock_move.py b/addons/delivery/tests/test_delivery_stock_move.py index 78c8c0b1ba7..db9c3ea9357 100644 --- a/addons/delivery/tests/test_delivery_stock_move.py +++ b/addons/delivery/tests/test_delivery_stock_move.py @@ -16,7 +16,6 @@ class StockMoveInvoice(AccountTestInvoicingCommon): cls.AccountJournal = cls.env['account.journal'] cls.partner_18 = cls.env['res.partner'].create({'name': 'My Test Customer'}) - cls.pricelist_id = cls.env.ref('product.list0') cls.product_11 = cls.env['product.product'].create({'name': 'A product to deliver'}) cls.product_cable_management_box = cls.env['product.product'].create({ 'name': 'Another product to deliver', @@ -44,7 +43,6 @@ class StockMoveInvoice(AccountTestInvoicingCommon): 'partner_id': self.partner_18.id, 'partner_invoice_id': self.partner_18.id, 'partner_shipping_id': self.partner_18.id, - 'pricelist_id': self.pricelist_id.id, 'order_line': [(0, 0, { 'name': 'Cable Management Box', 'product_id': self.product_cable_management_box.id, @@ -113,7 +111,6 @@ class StockMoveInvoice(AccountTestInvoicingCommon): 'partner_id': self.partner_18.id, 'partner_invoice_id': self.partner_18.id, 'partner_shipping_id': self.partner_18.id, - 'pricelist_id': self.pricelist_id.id, 'order_line': [(0, 0, { 'name': 'Cable Management Box', 'product_id': self.product_cable_management_box.id, diff --git a/addons/event_sale/data/event_sale_demo.xml b/addons/event_sale/data/event_sale_demo.xml index 683bc2835f8..10c4b134dc5 100644 --- a/addons/event_sale/data/event_sale_demo.xml +++ b/addons/event_sale/data/event_sale_demo.xml @@ -30,7 +30,6 @@ - sale @@ -57,7 +56,6 @@ - sale @@ -83,7 +81,6 @@ - sale @@ -101,7 +98,6 @@ - sale @@ -118,7 +114,6 @@ - sale @@ -135,7 +130,6 @@ - sale @@ -153,7 +147,6 @@ - sale @@ -178,7 +171,6 @@ - sale diff --git a/addons/event_sale/tests/test_event_sale.py b/addons/event_sale/tests/test_event_sale.py index e5878091911..bf980043934 100644 --- a/addons/event_sale/tests/test_event_sale.py +++ b/addons/event_sale/tests/test_event_sale.py @@ -330,7 +330,7 @@ class TestEventSale(TestEventSaleCommon): def test_ticket_price_with_pricelist_and_tax(self): self.env.user.partner_id.country_id = False - pricelist = self.env['product.pricelist'].search([], limit=1) + pricelist = self.env['product.pricelist'].create({'name': 'Base Pricelist'}) tax = self.env['account.tax'].create({ 'name': "Tax 10", diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py index bbab3b251a4..0e296fea8e6 100644 --- a/addons/point_of_sale/models/pos_config.py +++ b/addons/point_of_sale/models/pos_config.py @@ -36,9 +36,6 @@ class PosConfig(models.Model): ('journal_id.currency_id', 'in', (False, self.env.company.currency_id.id)), ]) - def _default_pricelist(self): - return self.env['product.pricelist'].search([('company_id', 'in', (False, self.env.company.id)), ('currency_id', '=', self.env.company.currency_id.id)], limit=1) - def _get_group_pos_manager(self): return self.env.ref('point_of_sale.group_pos_manager') @@ -112,9 +109,9 @@ class PosConfig(models.Model): pos_session_username = fields.Char(compute='_compute_current_session_user') pos_session_state = fields.Char(compute='_compute_current_session_user') pos_session_duration = fields.Char(compute='_compute_current_session_user') - pricelist_id = fields.Many2one('product.pricelist', string='Default Pricelist', required=True, default=_default_pricelist, - help="The pricelist used if no customer is selected or if the customer has no Sale Pricelist configured.") - available_pricelist_ids = fields.Many2many('product.pricelist', string='Available Pricelists', default=_default_pricelist, + pricelist_id = fields.Many2one('product.pricelist', string='Default Pricelist', + help="The pricelist used if no customer is selected or if the customer has no Sale Pricelist configured if any.") + available_pricelist_ids = fields.Many2many('product.pricelist', string='Available Pricelists', help="Make several pricelists available in the Point of Sale. You can also apply a pricelist to specific customers from their contact form (in Sales tab). To be valid, this pricelist must be listed here as an available pricelist. Otherwise the default pricelist will apply.") company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company) group_pos_manager_id = fields.Many2one('res.groups', string='Point of Sale Manager Group', default=_get_group_pos_manager, @@ -302,7 +299,7 @@ class PosConfig(models.Model): @api.constrains('pricelist_id', 'use_pricelist', 'available_pricelist_ids', 'journal_id', 'invoice_journal_id', 'payment_method_ids') def _check_currencies(self): for config in self: - if config.use_pricelist and config.pricelist_id not in config.available_pricelist_ids: + if config.use_pricelist and config.pricelist_id and config.pricelist_id not in config.available_pricelist_ids: raise ValidationError(_("The default pricelist must be included in the available pricelists.")) # Check if the config's payment methods are compatible with its currency diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 7500422d06e..bf3aa700c5d 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -31,7 +31,7 @@ class PosOrder(models.Model): taxes = line.tax_ids.filtered(lambda t: t.company_id.id == line.order_id.company_id.id) taxes = fiscal_position_id.map_tax(taxes) price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) - taxes = taxes.compute_all(price, line.order_id.pricelist_id.currency_id, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)['taxes'] + taxes = taxes.compute_all(price, line.order_id.currency_id, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)['taxes'] return sum(tax.get('amount', 0.0) for tax in taxes) @api.model @@ -46,7 +46,7 @@ class PosOrder(models.Model): 'partner_id': ui_order['partner_id'] or False, 'date_order': ui_order['creation_date'].replace('T', ' ')[:19], 'fiscal_position_id': ui_order['fiscal_position_id'], - 'pricelist_id': ui_order['pricelist_id'], + 'pricelist_id': ui_order.get('pricelist_id'), 'amount_paid': ui_order['amount_paid'], 'amount_total': ui_order['amount_total'], 'amount_tax': ui_order['amount_tax'], @@ -167,7 +167,7 @@ class PosOrder(models.Model): :param draft: Indicate that the pos_order is not validated yet. :type draft: bool. """ - prec_acc = order.pricelist_id.currency_id.decimal_places + prec_acc = order.currency_id.decimal_places order_bank_statement_lines= self.env['pos.payment'].search([('pos_order_id', '=', order.id)]) order_bank_statement_lines.unlink() @@ -248,7 +248,7 @@ class PosOrder(models.Model): help="Allows to know if all the total cost of the order lines have already been computed") lines = fields.One2many('pos.order.line', 'order_id', string='Order Lines', states={'draft': [('readonly', False)]}, readonly=True, copy=True) company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True) - pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', required=True, states={ + pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', states={ 'draft': [('readonly', False)]}, readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', change_default=True, index='btree_not_null', states={'draft': [('readonly', False)], 'paid': [('readonly', False)]}) sequence_number = fields.Integer(string='Sequence Number', help='A session-unique sequence number for the order', default=1) @@ -367,9 +367,9 @@ class PosOrder(models.Model): @api.onchange('payment_ids', 'lines') def _onchange_amount_all(self): for order in self: - if not order.pricelist_id.currency_id: + if not order.currency_id: raise UserError(_("You can't: create a pos order from the backend interface, or unset the pricelist, or create a pos.order in a python test with Form tool, or edit the form view in studio if no PoS order exist")) - currency = order.pricelist_id.currency_id + currency = order.currency_id order.amount_paid = sum(payment.amount for payment in order.payment_ids) order.amount_return = sum(payment.amount < 0 and payment.amount or 0 for payment in order.payment_ids) order.amount_tax = currency.round(sum(self._amount_line_tax(line, order.fiscal_position_id) for line in order.lines)) @@ -392,12 +392,11 @@ class PosOrder(models.Model): amounts[order['order_id'][0]]['taxes'] = order['price_subtotal_incl'] - order['price_subtotal'] for order in self: - currency = order.pricelist_id.currency_id order.write({ 'amount_paid': amounts[order.id]['paid'], 'amount_return': amounts[order.id]['return'], - 'amount_tax': currency.round(amounts[order.id]['taxes']), - 'amount_total': currency.round(amounts[order.id]['taxed']) + 'amount_tax': order.currency_id.round(amounts[order.id]['taxes']), + 'amount_total': order.currency_id.round(amounts[order.id]['taxed']) }) @api.onchange('partner_id') @@ -605,8 +604,7 @@ class PosOrder(models.Model): 'ref': self.name, 'partner_id': self.partner_id.id, 'partner_bank_id': self._get_partner_bank_id(), - # considering partner's sale pricelist's currency - 'currency_id': self.pricelist_id.currency_id.id, + 'currency_id': self.currency_id.id, 'invoice_user_id': self.user_id.id, 'invoice_date': invoice_date.astimezone(timezone).date(), 'fiscal_position_id': self.fiscal_position_id.id, @@ -1169,7 +1167,7 @@ class PosOrderLine(models.Model): fpos = self.order_id.fiscal_position_id tax_ids_after_fiscal_position = fpos.map_tax(self.tax_ids) price = self.price_unit * (1 - (self.discount or 0.0) / 100.0) - taxes = tax_ids_after_fiscal_position.compute_all(price, self.order_id.pricelist_id.currency_id, self.qty, product=self.product_id, partner=self.order_id.partner_id) + taxes = tax_ids_after_fiscal_position.compute_all(price, self.order_id.currency_id, self.qty, product=self.product_id, partner=self.order_id.partner_id) return { 'price_subtotal_incl': taxes['total_included'], 'price_subtotal': taxes['total_excluded'], @@ -1179,12 +1177,9 @@ class PosOrderLine(models.Model): @api.onchange('product_id') def _onchange_product_id(self): if self.product_id: - if not self.order_id.pricelist_id: - raise UserError( - _('You have to select a pricelist in the sale form !\n' - 'Please set one before choosing a product.')) price = self.order_id.pricelist_id._get_product_price( - self.product_id, self.qty or 1.0) + self.product_id, self.qty or 1.0, currency=self.currency_id + ) self.tax_ids = self.product_id.taxes_id.filtered(lambda r: not self.company_id or r.company_id == self.company_id) tax_ids_after_fiscal_position = self.order_id.fiscal_position_id.map_tax(self.tax_ids) self.price_unit = self.env['account.tax']._fix_tax_included_price_company(price, self.tax_ids, tax_ids_after_fiscal_position, self.company_id) @@ -1193,12 +1188,10 @@ class PosOrderLine(models.Model): @api.onchange('qty', 'discount', 'price_unit', 'tax_ids') def _onchange_qty(self): if self.product_id: - if not self.order_id.pricelist_id: - raise UserError(_('You have to select a pricelist in the sale form.')) price = self.price_unit * (1 - (self.discount or 0.0) / 100.0) self.price_subtotal = self.price_subtotal_incl = price * self.qty if (self.tax_ids): - taxes = self.tax_ids.compute_all(price, self.order_id.pricelist_id.currency_id, self.qty, product=self.product_id, partner=False) + taxes = self.tax_ids.compute_all(price, self.order_id.currency_id, self.qty, product=self.product_id, partner=False) self.price_subtotal = taxes['total_excluded'] self.price_subtotal_incl = taxes['total_included'] @@ -1410,8 +1403,8 @@ class ReportSaleDetails(models.AbstractModel): products_sold = {} taxes = {} for order in orders: - if user_currency != order.pricelist_id.currency_id: - total += order.pricelist_id.currency_id._convert( + if user_currency != order.currency_id: + total += order.currency_id._convert( order.amount_total, user_currency, order.company_id, order.date_order or fields.Date.today()) else: total += order.amount_total diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetPricelistButton.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetPricelistButton.js index 80fa78c6295..2da6a0bcb2b 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetPricelistButton.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetPricelistButton.js @@ -24,10 +24,20 @@ class SetPricelistButton extends PosComponent { const selectionList = this.env.pos.pricelists.map((pricelist) => ({ id: pricelist.id, label: pricelist.name, - isSelected: pricelist.id === this.currentOrder.pricelist.id, + isSelected: this.currentOrder.pricelist + && pricelist.id === this.currentOrder.pricelist.id, item: pricelist, })); + if (!this.env.pos.default_pricelist) { + selectionList.push({ + id: null, + label: this.env._t('Default Price'), + isSelected: !this.currentOrder.pricelist, + item: null, + }) + } + const { confirmed, payload: selectedPricelist } = await this.showPopup("SelectionPopup", { title: this.env._t("Select the pricelist"), list: selectionList, @@ -43,7 +53,7 @@ SetPricelistButton.template = "SetPricelistButton"; ProductScreen.addControlButton({ component: SetPricelistButton, condition: function () { - return this.env.pos.config.use_pricelist && this.env.pos.pricelists.length > 1; + return this.env.pos.config.use_pricelist && this.env.pos.pricelists.length > 0; }, }); diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 942bf4c4e37..857db75e6a7 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1470,13 +1470,16 @@ export class Product extends PosModel { // product.pricelist.item records are loaded with a search_read // and were automatically sorted based on their _order by the // ORM. After that they are added in this order to the pricelists. - get_price(pricelist, quantity, price_extra) { + get_price(pricelist, quantity, price_extra, recurring=false) { var self = this; var date = moment(); // In case of nested pricelists, it is necessary that all pricelists are made available in - // the POS. Display a basic alert to the user in this case. - if (!pricelist) { + // the POS. Display a basic alert to the user in the case where there is a pricelist item + // but we can't load the base pricelist to get the price when calling this method again. + // As this method is also call without pricelist available in the POS, we can't just check + // the absence of pricelist. + if (recurring && !pricelist) { alert( _t( "An error occurred when loading product prices. " + @@ -1492,16 +1495,18 @@ export class Product extends PosModel { category = category.parent; } - var pricelist_items = _.filter( - self.applicablePricelistItems[pricelist.id], - function (item) { + var pricelist_items = []; + if (pricelist) { + var pricelist_items = _.filter( + self.applicablePricelistItems[pricelist.id], + function (item) { return ( (!item.categ_id || _.contains(category_ids, item.categ_id[0])) && (!item.date_start || moment.utc(item.date_start).isSameOrBefore(date)) && (!item.date_end || moment.utc(item.date_end).isSameOrAfter(date)) ); - } - ); + }); + } var price = self.lst_price; if (price_extra) { @@ -1517,7 +1522,7 @@ export class Product extends PosModel { return pricelist.id === rule.base_pricelist_id[0]; }); if (base_pricelist) { - price = self.get_price(base_pricelist, quantity); + price = self.get_price(base_pricelist, quantity, undefined, true); } } else if (rule.base === "standard_price") { price = self.standard_price; @@ -2227,7 +2232,7 @@ export class Orderline extends PosModel { }; } display_discount_policy() { - return this.order.pricelist.discount_policy; + return (this.order.pricelist ? this.order.pricelist.discount_policy : "with_discount" ); } compute_fixed_price(price) { var order = this.order; diff --git a/addons/point_of_sale/tests/test_frontend.py b/addons/point_of_sale/tests/test_frontend.py index 882dcbbc89e..47a9ad58085 100644 --- a/addons/point_of_sale/tests/test_frontend.py +++ b/addons/point_of_sale/tests/test_frontend.py @@ -439,7 +439,10 @@ class TestPointOfSaleHttpCommon(AccountTestInvoicingHttpCommon): 'type': 'sale', 'company_id': main_company.id}) - all_pricelists = env['product.pricelist'].search([('id', '!=', excluded_pricelist.id)]) + all_pricelists = env['product.pricelist'].search([ + ('id', '!=', excluded_pricelist.id), + '|', ('company_id', '=', main_company.id), ('company_id', '=', False) + ]) all_pricelists.write(dict(currency_id=main_company.currency_id.id)) src_tax = env['account.tax'].create({'name': "SRC", 'amount': 10}) diff --git a/addons/point_of_sale/tests/test_point_of_sale.py b/addons/point_of_sale/tests/test_point_of_sale.py index 83cf6c0ca16..478f08357b7 100644 --- a/addons/point_of_sale/tests/test_point_of_sale.py +++ b/addons/point_of_sale/tests/test_point_of_sale.py @@ -29,8 +29,8 @@ class TestPointOfSale(TransactionCase): self.env.user.company_id = self.company1 - def test_default_pricelist_with_company(self): - """ Verify that the default pricelist belongs to the same company as the config """ + def test_no_default_pricelist(self): + """ Verify that the default pricelist isn't automatically set in the config """ company1_pricelist = self.env["product.pricelist"].create({ "name": "company 1 pricelist", "currency_id": self.currency.id, @@ -38,26 +38,10 @@ class TestPointOfSale(TransactionCase): "sequence": 2, }) - # make sure this doesn't pick the company2 pricelist + # make sure this doesn't pick a pricelist as default new_config = self.env["pos.config"].create({ - "name": "usd config" + "name": "usd config", "available_pricelist_ids": [(6, 0, [company1_pricelist.id])] }) - self.assertEqual(new_config.pricelist_id, company1_pricelist, - "POS config incorrectly has pricelist %s" % new_config.pricelist_id.display_name) - - def test_default_pricelist_without_company(self): - """ Verify that a default pricelist without a company works """ - universal_pricelist = self.env["product.pricelist"].create({ - "name": "universal pricelist", - "currency_id": self.currency.id, - "sequence": 2, - }) - - # make sure this doesn't pick the company2 pricelist - new_config = self.env["pos.config"].create({ - "name": "usd config" - }) - - self.assertEqual(new_config.pricelist_id, universal_pricelist, + self.assertEqual(new_config.pricelist_id, self.env['product.pricelist'], "POS config incorrectly has pricelist %s" % new_config.pricelist_id.display_name) diff --git a/addons/point_of_sale/tests/test_point_of_sale_flow.py b/addons/point_of_sale/tests/test_point_of_sale_flow.py index 0730d3d8e76..17842c592f2 100644 --- a/addons/point_of_sale/tests/test_point_of_sale_flow.py +++ b/addons/point_of_sale/tests/test_point_of_sale_flow.py @@ -17,7 +17,7 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): def compute_tax(self, product, price, qty=1, taxes=None): if not taxes: taxes = product.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id) - currency = self.pos_config.pricelist_id.currency_id + currency = self.pos_config.currency_id res = taxes.compute_all(price, currency, qty, product=product) untax = res['total_excluded'] return untax, sum(tax.get('amount', 0.0) for tax in res['taxes']) @@ -147,7 +147,6 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): [0, 0, {'lot_name': '1002'}], ] })], - 'pricelist_id': 1, 'amount_paid': 12.0, 'amount_total': 12.0, 'amount_tax': 0.0, @@ -570,7 +569,6 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): 'amount_total': untax + atax, 'creation_date': fields.Datetime.to_string(fields.Datetime.now()), 'fiscal_position_id': False, - 'pricelist_id': self.pos_config.available_pricelist_ids[0].id, 'lines': [[0, 0, {'discount': 0, @@ -604,7 +602,6 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): 'amount_total': untax + atax, 'creation_date': fields.Datetime.to_string(fields.Datetime.now()), 'fiscal_position_id': False, - 'pricelist_id': self.pos_config.available_pricelist_ids[0].id, 'lines': [[0, 0, {'discount': 0, @@ -638,7 +635,6 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): 'amount_total': untax + atax, 'creation_date': fields.Datetime.to_string(fields.Datetime.now()), 'fiscal_position_id': False, - 'pricelist_id': self.pos_config.available_pricelist_ids[0].id, 'lines': [[0, 0, {'discount': 0, @@ -715,7 +711,7 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): }) # make a config that has currency different from the company - eur_pricelist = self.partner1.property_product_pricelist.copy(default={'currency_id': self.env.ref('base.EUR').id}) + eur_pricelist = self.env['product.pricelist'].create({'name': 'Test EUR Pricelist', 'currency_id': self.env.ref('base.EUR').id}) sale_journal = self.env['account.journal'].create({ 'name': 'PoS Sale EUR', 'type': 'sale', @@ -923,7 +919,6 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): 'amount_total': untax + atax, 'creation_date': fields.Datetime.to_string(fields.Datetime.now()), 'fiscal_position_id': False, - 'pricelist_id': self.pos_config.available_pricelist_ids[0].id, 'lines': [[0, 0, {'discount': 0, @@ -1135,7 +1130,6 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): 'amount_total': 750, 'creation_date': fields.Datetime.to_string(fields.Datetime.now()), 'fiscal_position_id': False, - 'pricelist_id': self.pos_config.available_pricelist_ids[0].id, 'lines': [[0, 0, { 'discount': 0, 'id': 42, @@ -1195,7 +1189,6 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): 'amount_total': untax + atax, 'creation_date': fields.Datetime.to_string(fields.Datetime.now()), 'fiscal_position_id': False, - 'pricelist_id': self.pos_config.available_pricelist_ids[0].id, 'lines': [(0, 0, { 'discount': 0, 'id': 42, @@ -1277,7 +1270,6 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): 'amount_total': 750, 'creation_date': fields.Datetime.to_string(fields.Datetime.now()), 'fiscal_position_id': False, - 'pricelist_id': self.pos_config.available_pricelist_ids[0].id, 'lines': [[0, 0, { 'discount': 0, 'id': 42, diff --git a/addons/pos_restaurant/tests/test_frontend.py b/addons/pos_restaurant/tests/test_frontend.py index 69ad27877ad..919792d719e 100644 --- a/addons/pos_restaurant/tests/test_frontend.py +++ b/addons/pos_restaurant/tests/test_frontend.py @@ -26,7 +26,6 @@ class TestFrontend(odoo.tests.HttpCase): 'printer_ids': [(4, printer.id)], 'iface_start_categ_id': drinks_category.id, 'start_category': True, - 'pricelist_id': self.env.ref('product.list0').id, }) main_floor = self.env['restaurant.floor'].create({ diff --git a/addons/pos_sale/models/pos_order.py b/addons/pos_sale/models/pos_order.py index d9df856501f..f25e0117e83 100644 --- a/addons/pos_sale/models/pos_order.py +++ b/addons/pos_sale/models/pos_order.py @@ -22,11 +22,11 @@ class PosOrder(models.Model): values.setdefault('crm_team_id', session.config_id.crm_team_id.id) return values - @api.depends('pricelist_id.currency_id', 'date_order', 'company_id') + @api.depends('date_order', 'company_id') def _compute_currency_rate(self): for order in self: date_order = order.date_order or fields.Datetime.now() - order.currency_rate = self.env['res.currency']._get_conversion_rate(order.company_id.currency_id, order.pricelist_id.currency_id, order.company_id, date_order) + order.currency_rate = self.env['res.currency']._get_conversion_rate(order.company_id.currency_id, order.currency_id, order.company_id, date_order) def _prepare_invoice_vals(self): invoice_vals = super(PosOrder, self)._prepare_invoice_vals() diff --git a/addons/product/data/product_data.xml b/addons/product/data/product_data.xml index afc860df10f..a08d50a4daa 100644 --- a/addons/product/data/product_data.xml +++ b/addons/product/data/product_data.xml @@ -43,15 +43,5 @@ there yet otherwise people who have installed the server (even with the without- parameter) will see those record just disappear. --> - - - Public Pricelist - 1 - - - - diff --git a/addons/product/data/product_demo.xml b/addons/product/data/product_demo.xml index 573d00030c8..a8a3f7f9c43 100644 --- a/addons/product/data/product_demo.xml +++ b/addons/product/data/product_demo.xml @@ -810,13 +810,5 @@ - - property_product_pricelist - - - - - - diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py index 65dc65f3e2e..2d07295f3a7 100644 --- a/addons/product/models/product_pricelist.py +++ b/addons/product/models/product_pricelist.py @@ -58,69 +58,98 @@ class Pricelist(models.Model): def name_get(self): return [(pricelist.id, '%s (%s)' % (pricelist.name, pricelist.currency_id.name)) for pricelist in self] - def _get_products_price(self, products, quantity, uom=None, date=False, **kwargs): + def _get_products_price(self, products, *args, **kwargs): """Compute the pricelist prices for the specified products, qty & uom. - Note: self.ensure_one() + Note: self and self.ensure_one() - :returns: dict{product_id: product price}, considering the current pricelist - :rtype: dict + :param products: recordset of products (product.product/product.template) + :param float quantity: quantity of products requested (in given uom) + :param currency: record of currency (res.currency) (optional) + :param uom: unit of measure (uom.uom record) (optional) + If not specified, prices returned are expressed in product uoms + :param date: date to use for price computation and currency conversions (optional) + :type date: date or datetime + + :returns: {product_id: product price}, considering the current pricelist if any + :rtype: dict(int, float) """ - self.ensure_one() + self and self.ensure_one() # self is at most one record return { product_id: res_tuple[0] - for product_id, res_tuple in self._compute_price_rule( - products, - quantity, - uom=uom, - date=date, - **kwargs - ).items() + for product_id, res_tuple in self._compute_price_rule(products, *args, **kwargs).items() } - def _get_product_price(self, product, quantity, uom=None, date=False, **kwargs): + def _get_product_price(self, product, *args, **kwargs): """Compute the pricelist price for the specified product, qty & uom. - Note: self.ensure_one() + Note: self and self.ensure_one() - :returns: unit price of the product, considering pricelist rules + :param products: recordset of products (product.product/product.template) + :param float quantity: quantity of products requested (in given uom) + :param currency: record of currency (res.currency) (optional) + :param uom: unit of measure (uom.uom record) (optional) + If not specified, prices returned are expressed in product uoms + :param date: date to use for price computation and currency conversions (optional) + :type date: date or datetime + + :returns: unit price of the product, considering pricelist rules if any :rtype: float """ - self.ensure_one() - return self._compute_price_rule( - product, quantity, uom=uom, date=date, **kwargs - )[product.id][0] + self and self.ensure_one() # self is at most one record + return self._compute_price_rule(product, *args, **kwargs)[product.id][0] - def _get_product_price_rule(self, product, quantity, uom=None, date=False, **kwargs): + def _get_product_price_rule(self, product, *args, **kwargs): """Compute the pricelist price & rule for the specified product, qty & uom. - Note: self.ensure_one() + Note: self and self.ensure_one() + + :param products: recordset of products (product.product/product.template) + :param float quantity: quantity of products requested (in given uom) + :param currency: record of currency (res.currency) (optional) + :param uom: unit of measure (uom.uom record) (optional) + If not specified, prices returned are expressed in product uoms + :param date: date to use for price computation and currency conversions (optional) + :type date: date or datetime :returns: (product unit price, applied pricelist rule id) :rtype: tuple(float, int) """ - self.ensure_one() - return self._compute_price_rule(product, quantity, uom=uom, date=date, **kwargs)[product.id] + self and self.ensure_one() # self is at most one record + return self._compute_price_rule(product, *args, **kwargs)[product.id] - def _get_product_rule(self, product, quantity, uom=None, date=False, **kwargs): + def _get_product_rule(self, product, *args, **kwargs): """Compute the pricelist price & rule for the specified product, qty & uom. - Note: self.ensure_one() + Note: self and self.ensure_one() + + :param products: recordset of products (product.product/product.template) + :param float quantity: quantity of products requested (in given uom) + :param currency: record of currency (res.currency) (optional) + :param uom: unit of measure (uom.uom record) (optional) + If not specified, prices returned are expressed in product uoms + :param date: date to use for price computation and currency conversions (optional) + :type date: date or datetime :returns: applied pricelist rule id :rtype: int or False """ - self.ensure_one() - return self._compute_price_rule( - product, quantity, uom=uom, date=date, compute_price=False, **kwargs - )[product.id][1] + self and self.ensure_one() # self is at most one record + return self._compute_price_rule(product, *args, **kwargs)[product.id][1] - def _compute_price_rule(self, products, qty, uom=None, date=False, compute_price=True, **kwargs): + def _compute_price_rule( + self, products, quantity, currency=None, uom=None, date=False, compute_price=True, + **kwargs + ): """ Low-level method - Mono pricelist, multi products Returns: dict{product_id: (price, suitable_rule) for the given pricelist} + Note: self and self.ensure_one() + :param products: recordset of products (product.product/product.template) - :param float qty: quantity of products requested (in given uom) + :param float quantity: quantity of products requested (in given uom) + :param currency: record of currency (res.currency) + note: currency.ensure_one() :param uom: unit of measure (uom.uom record) If not specified, prices returned are expressed in product uoms :param date: date to use for price computation and currency conversions @@ -130,7 +159,10 @@ class Pricelist(models.Model): :returns: product_id: (price, pricelist_rule) :rtype: dict """ - self.ensure_one() + self and self.ensure_one() # self is at most one record + + currency = currency or self.currency_id or self.env.company.currency_id + currency.ensure_one() if not products: return {} @@ -152,9 +184,11 @@ class Pricelist(models.Model): # Compute quantity in product uom because pricelist rules are specified # w.r.t product default UoM (min_quantity, price_surchage, ...) if target_uom != product_uom: - qty_in_product_uom = target_uom._compute_quantity(qty, product_uom, raise_if_failure=False) + qty_in_product_uom = target_uom._compute_quantity( + quantity, product_uom, raise_if_failure=False + ) else: - qty_in_product_uom = qty + qty_in_product_uom = quantity for rule in rules: if rule._is_applicable_for(product, qty_in_product_uom): @@ -162,7 +196,8 @@ class Pricelist(models.Model): break if compute_price: - price = suitable_rule._compute_price(product, qty, target_uom, date=date, currency=self.currency_id) + price = suitable_rule._compute_price( + product, quantity, target_uom, date=date, currency=currency) else: # Skip price computation when only the rule is requested. price = 0.0 @@ -172,7 +207,7 @@ class Pricelist(models.Model): # Split methods to ease (community) overrides def _get_applicable_rules(self, products, date, **kwargs): - self.ensure_one() + self and self.ensure_one() # self is at most one record # Do not filter out archived pricelist items, since it means current pricelist is also archived # We do not want the computation of prices for archived pricelist to always fallback on the Sales price # because no rule was found (thanks to the automatic orm filtering on active field) @@ -181,6 +216,7 @@ class Pricelist(models.Model): ) def _get_applicable_rules_domain(self, products, date, **kwargs): + self and self.ensure_one() # self is at most one record if products._name == 'product.template': templates_domain = ('product_tmpl_id', 'in', products.ids) products_domain = ('product_id.product_tmpl_id', 'in', products.ids) @@ -198,13 +234,13 @@ class Pricelist(models.Model): ] # Multi pricelists price|rule computation - def _price_get(self, product, qty, **kwargs): + def _price_get(self, product, quantity, **kwargs): """ Multi pricelist, mono product - returns price per pricelist """ return { key: price[0] - for key, price in self._compute_price_rule_multi(product, qty, **kwargs)[product.id].items()} + for key, price in self._compute_price_rule_multi(product, quantity, **kwargs)[product.id].items()} - def _compute_price_rule_multi(self, products, qty, uom=None, date=False, **kwargs): + def _compute_price_rule_multi(self, products, quantity, uom=None, date=False, **kwargs): """ Low-level method - Multi pricelist, multi products Returns: dict{product_id: dict{pricelist_id: (price, suitable_rule)} }""" if not self.ids: @@ -213,7 +249,7 @@ class Pricelist(models.Model): pricelists = self results = {} for pricelist in pricelists: - subres = pricelist._compute_price_rule(products, qty, uom=uom, date=date, **kwargs) + subres = pricelist._compute_price_rule(products, quantity, uom=uom, date=date, **kwargs) for product_id, price in subres.items(): results.setdefault(product_id, {}) results[product_id][pricelist.id] = price @@ -228,9 +264,8 @@ class Pricelist(models.Model): First, the pricelist of the specific property (res_id set), this one is created when saving a pricelist on the partner form view. Else, it will return the pricelist of the partner country group - Else, it will return the generic property (res_id not set), this one - is created on the company creation. - Else, it will return the first available pricelist + Else, it will return the generic property (res_id not set) + Else, it will return the first available pricelist if any :param int company_id: if passed, used for looking up properties, instead of current user's company diff --git a/addons/product/models/product_pricelist_item.py b/addons/product/models/product_pricelist_item.py index cd6ff4e858a..de656a57cea 100644 --- a/addons/product/models/product_pricelist_item.py +++ b/addons/product/models/product_pricelist_item.py @@ -345,19 +345,23 @@ class PricelistItem(models.Model): def _compute_price(self, product, quantity, uom, date, currency=None): """Compute the unit price of a product in the context of a pricelist application. + Note: self and self.ensure_one() + :param product: recordset of product (product.product/product.template) :param float qty: quantity of products requested (in given uom) :param uom: unit of measure (uom.uom record) :param datetime date: date to use for price computation and currency conversions - :param currency: pricelist currency (for the specific case where self is empty) + :param currency: currency (for the case where self is empty) - :returns: price according to pricelist rule, expressed in pricelist currency + :returns: price according to pricelist rule or the product price, expressed in the param + currency, the pricelist currency or the company currency :rtype: float """ + self and self.ensure_one() # self is at most one record product.ensure_one() uom.ensure_one() - currency = currency or self.currency_id + currency = currency or self.currency_id or self.env.company.currency_id currency.ensure_one() # Pricelist specific values are specified according to product UoM @@ -410,7 +414,9 @@ class PricelistItem(models.Model): rule_base = self.base or 'list_price' if rule_base == 'pricelist' and self.base_pricelist_id: - price = self.base_pricelist_id._get_product_price(product, quantity, uom, date) + price = self.base_pricelist_id._get_product_price( + product, quantity, currency=self.currency_id, uom=uom, date=date + ) src_currency = self.base_pricelist_id.currency_id elif rule_base == "standard_price": src_currency = product.cost_currency_id diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index 476c1997159..356f509ea4a 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -1306,9 +1306,6 @@ class ProductTemplate(models.Model): self.ensure_one() # YTI TODO: During website_sale cleaning, we should get rid of those crappy context thing pricelist = self._get_contextual_pricelist() - if not pricelist: - return 0.0 - quantity = self.env.context.get('quantity', 1.0) uom = self.env['uom.uom'].browse(self.env.context.get('uom')) date = self.env.context.get('date') diff --git a/addons/product/models/res_company.py b/addons/product/models/res_company.py index e9420ed5133..3047c3af7bd 100644 --- a/addons/product/models/res_company.py +++ b/addons/product/models/res_company.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, models, _ +from odoo import _, api, models class ResCompany(models.Model): @@ -9,59 +8,40 @@ class ResCompany(models.Model): @api.model_create_multi def create(self, vals_list): - companies = super(ResCompany, self).create(vals_list) - ProductPricelist = self.env['product.pricelist'] - for new_company in companies: - pricelist = ProductPricelist.search([ - ('currency_id', '=', new_company.currency_id.id), - ('company_id', '=', False) - ], limit=1) - if not pricelist: - params = {'currency': new_company.currency_id.name} - pricelist = ProductPricelist.create({ - 'name': _("Default %(currency)s pricelist") % params, - 'currency_id': new_company.currency_id.id, - }) - self.env['ir.property']._set_default( - 'property_product_pricelist', - 'res.partner', - pricelist, - new_company, - ) + companies = super().create(vals_list) + companies._activate_or_create_pricelists() return companies - def write(self, values): - # When we modify the currency of the company, we reflect the change on the list0 pricelist, if - # that pricelist is not used by another company. Otherwise, we create a new pricelist for the - # given currency. - ProductPricelist = self.env['product.pricelist'] - currency_id = values.get('currency_id') - main_pricelist = self.env.ref('product.list0', False) - if currency_id and main_pricelist: - nb_companies = self.search_count([]) - for company in self: - existing_pricelist = ProductPricelist.search( - [('company_id', 'in', (False, company.id)), - ('currency_id', 'in', (currency_id, company.currency_id.id))]) - if existing_pricelist and any(currency_id == x.currency_id.id for x in existing_pricelist): - continue - if currency_id == company.currency_id.id: - continue - currency_match = main_pricelist.currency_id == company.currency_id - company_match = (main_pricelist.company_id == company or - (main_pricelist.company_id.id is False and nb_companies == 1)) - if currency_match and company_match: - main_pricelist.write({'currency_id': currency_id}) - else: - params = {'currency': self.env['res.currency'].browse(currency_id).name} - pricelist = ProductPricelist.create({ - 'name': _("Default %(currency)s pricelist") % params, - 'currency_id': currency_id, - }) - self.env['ir.property']._set_default( - 'property_product_pricelist', - 'res.partner', - pricelist, - company, - ) - return super(ResCompany, self).write(values) + def _activate_or_create_pricelists(self): + """ Manage the default pricelists for needed companies. """ + if self.user_has_groups('product.group_product_pricelist'): + companies = self or self.env['res.company'].search([]) + ProductPricelist = self.env['product.pricelist'].sudo() + # Activate existing default pricelists + default_pricelists_sudo = ProductPricelist.with_context(active_test=False).search( + [('item_ids', '=', False), ('company_id', 'in', companies.ids)] + ).filtered(lambda pl: pl.currency_id == pl.company_id.currency_id) + default_pricelists_sudo.action_unarchive() + companies_without_pricelist = companies.filtered( + lambda c: c.id not in default_pricelists_sudo.company_id.ids + ) + # Create missing default pricelists + ProductPricelist.create([ + company._get_default_pricelist_vals() for company in companies_without_pricelist + ]) + + def _get_default_pricelist_vals(self): + """Add values to the default pricelist at company creation or activation of the pricelist + + Note: self.ensure_one() + + :rtype: dict + """ + self.ensure_one() + values = {} + values.update({ + 'name': _("Default %s pricelist", self.currency_id.name), + 'currency_id': self.currency_id.id, + 'company_id': self.id, + }) + return values diff --git a/addons/product/models/res_config_settings.py b/addons/product/models/res_config_settings.py index e514e338f85..fc34357c15b 100644 --- a/addons/product/models/res_config_settings.py +++ b/addons/product/models/res_config_settings.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import _, api, fields, models class ResConfigSettings(models.TransientModel): @@ -43,8 +43,16 @@ class ResConfigSettings(models.TransientModel): @api.onchange('group_product_pricelist') def _onchange_group_sale_pricelist(self): - if not self.group_product_pricelist and self.group_sale_pricelist: - self.group_sale_pricelist = False + if not self.group_product_pricelist: + if self.group_sale_pricelist: + self.group_sale_pricelist = False + active_pricelist = self.env['product.pricelist'].sudo().search([('active', '=', True)]) + if active_pricelist: + return { + 'warning': { + 'message': _("You are deactivating the pricelist feature. " + "Every active pricelist will be archived.") + }} @api.onchange('product_pricelist_setting') def _onchange_product_pricelist_setting(self): @@ -58,3 +66,8 @@ class ResConfigSettings(models.TransientModel): if not self.group_discount_per_so_line: pl = self.env['product.pricelist'].search([('discount_policy', '=', 'without_discount')]) pl.write({'discount_policy': 'with_discount'}) + + if self.group_product_pricelist: + self.env['res.company']._activate_or_create_pricelists() + else: + self.env['product.pricelist'].sudo().search([]).action_archive() diff --git a/addons/product/models/res_currency.py b/addons/product/models/res_currency.py index 9ff0fc56a23..5df9248c45f 100644 --- a/addons/product/models/res_currency.py +++ b/addons/product/models/res_currency.py @@ -12,3 +12,4 @@ class ResCurrency(models.Model): super()._activate_group_multi_currency() group_user = self.env.ref('base.group_user').sudo() group_user._apply_group(self.env.ref('product.group_product_pricelist')) + self.env['res.company']._activate_or_create_pricelists() diff --git a/addons/product/tests/common.py b/addons/product/tests/common.py index dae33376a8b..4b9a24ee172 100644 --- a/addons/product/tests/common.py +++ b/addons/product/tests/common.py @@ -47,17 +47,9 @@ class ProductCommon( @classmethod def _archive_other_pricelists(cls): - """Do not raise if there is no pricelist(s) for a given website""" - website_sale = cls.env['ir.module.module']._get('website_sale') - if website_sale.state == 'installed': - archive_context = patch('odoo.addons.website_sale.models.product_pricelist.ProductPricelist._check_website_pricelist') - else: - archive_context = nullcontext() - - with archive_context: - cls.env['product.pricelist'].search([ - ('id', '!=', cls.pricelist.id), - ]).action_archive() + cls.env['product.pricelist'].search([ + ('id', '!=', cls.pricelist.id), + ]).action_archive() class ProductAttributesCommon(ProductCommon): diff --git a/addons/product/tests/test_product_pricelist.py b/addons/product/tests/test_product_pricelist.py index 57a0f9e84af..bd8567ffaf7 100644 --- a/addons/product/tests/test_product_pricelist.py +++ b/addons/product/tests/test_product_pricelist.py @@ -200,11 +200,11 @@ class TestProductPricelist(ProductCommon): product = self.product_multi_price price = self.customer_pricelist._get_product_price(product, quantity=1.0) msg = "Wrong price: Multi Product Price. should be 99 instead of %s" % price - self.assertEqual(float_compare(price, 99, precision_digits=2), 0) + self.assertEqual(float_compare(price, 99, precision_digits=2), 0, msg) price = self.business_pricelist._get_product_price(product, quantity=1.0) msg = "Wrong price: Multi Product Price. should be 50 instead of %s" % price - self.assertEqual(float_compare(price, 50, precision_digits=2), 0) + self.assertEqual(float_compare(price, 50, precision_digits=2), 0, msg) def test_20_price_different_currency_pricelist(self): pricelist = self.env['product.pricelist'].create({ @@ -249,3 +249,37 @@ class TestProductPricelist(ProductCommon): price = pricelist._get_product_price(self.monitor, quantity=1.0) # product price use the currency of the pricelist self.assertEqual(price, 10090) + + def test_price_without_pricelist_fallback_product_price(self): + ProductPricelist = self.env['product.pricelist'] + spam = self.env['product.product'].create({ + 'name': '1 tonne of spam', + 'uom_id': self.uom_ton.id, + 'uom_po_id': self.uom_ton.id, + 'list_price': 100, + 'type': 'consu' + }) + self.assertEqual( + ProductPricelist._get_product_price(self.monitor, quantity=1.0), + self.monitor.list_price, + msg="without pricelist, the price should be the same as the list price", + ) + self.assertEqual( + ProductPricelist._get_product_price(self.monitor, quantity=1.0, currency=self.new_currency), + self.monitor.list_price*10, + msg="without pricelist but with a currency different than the product one, the price " + "should be the same as the list price converted with the currency rate", + ) + self.assertEqual( + ProductPricelist._get_product_price(spam, quantity=1.0, uom=self.uom_kgm), + spam.list_price / 1000, + msg="the product price should be converted using the specified uom", + ) + self.assertEqual( + ProductPricelist._get_product_price( + spam, quantity=1.0, currency=self.new_currency, uom=self.uom_kgm + ), + spam.list_price / 100, + msg="the product price should be converted using the specified uom and converted to the" + " correct currency", + ) diff --git a/addons/repair/data/mail_template_data.xml b/addons/repair/data/mail_template_data.xml index cb05cf86397..c7a70cd4742 100644 --- a/addons/repair/data/mail_template_data.xml +++ b/addons/repair/data/mail_template_data.xml @@ -14,7 +14,7 @@ Hello Brandon Freeman,
Here is your repair order RO/00004 - amounting in $ 100.00.
+ amounting in $ 100.00.
.
diff --git a/addons/repair/models/repair.py b/addons/repair/models/repair.py index ef7452b97d5..ca08c0a2be2 100644 --- a/addons/repair/models/repair.py +++ b/addons/repair/models/repair.py @@ -83,9 +83,13 @@ class Repair(models.Model): copy=True) pricelist_id = fields.Many2one( 'product.pricelist', 'Pricelist', - default=lambda self: self.env['product.pricelist'].search([('company_id', 'in', [self.env.company.id, False])], limit=1).id, help='Pricelist of the selected partner.', check_company=True) - currency_id = fields.Many2one(related='pricelist_id.currency_id') + currency_id = fields.Many2one( + comodel_name='res.currency', + compute='_compute_currency_id', + store=True, + precompute=True, + ondelete='restrict') partner_invoice_id = fields.Many2one('res.partner', 'Invoicing Address', check_company=True) invoice_method = fields.Selection([ ("none", "No Invoice"), @@ -147,27 +151,31 @@ class Repair(models.Model): if order.partner_id: order.default_address_id = order.partner_id.address_get(['contact'])['contact'] + @api.depends('pricelist_id', 'company_id') + def _compute_currency_id(self): + for order in self: + order.currency_id = order.pricelist_id.currency_id.id or order.company_id.currency_id.id + @api.depends('picking_id', 'picking_id.state') def _compute_is_returned(self): self.is_returned = False returned = self.filtered(lambda r: r.picking_id and r.picking_id.state == 'done') returned.is_returned = True - @api.depends('operations.price_subtotal', 'invoice_method', 'fees_lines.price_subtotal', 'pricelist_id.currency_id') + @api.depends('operations.price_subtotal', 'invoice_method', 'fees_lines.price_subtotal', 'currency_id') def _amount_untaxed(self): for order in self: total = sum(operation.price_subtotal for operation in order.operations) total += sum(fee.price_subtotal for fee in order.fees_lines) - currency = order.pricelist_id.currency_id or self.env.company.currency_id - order.amount_untaxed = currency.round(total) + order.amount_untaxed = order.currency_id.round(total) @api.depends('operations.price_unit', 'operations.product_uom_qty', 'operations.product_id', 'fees_lines.price_unit', 'fees_lines.product_uom_qty', 'fees_lines.product_id', - 'pricelist_id.currency_id', 'partner_id') + 'currency_id', 'partner_id') def _amount_tax(self): for order in self: val = 0.0 - currency = order.pricelist_id.currency_id or self.env.company.currency_id + currency = order.currency_id for operation in order.operations: if operation.tax_id: tax_calculate = operation.tax_id.compute_all(operation.price_unit, currency, operation.product_uom_qty, operation.product_id, order.partner_id) @@ -183,8 +191,7 @@ class Repair(models.Model): @api.depends('amount_untaxed', 'amount_tax') def _amount_total(self): for order in self: - currency = order.pricelist_id.currency_id or self.env.company.currency_id - order.amount_total = currency.round(order.amount_untaxed + order.amount_tax) + order.amount_total = order.currency_id.round(order.amount_untaxed + order.amount_tax) _sql_constraints = [ ('name', 'unique (name)', 'The name of the Repair Order must be unique!'), @@ -373,7 +380,7 @@ class Repair(models.Model): raise UserError(_('You have to select an invoice address in the repair form.')) narration = repair.quotation_notes - currency = repair.pricelist_id.currency_id + currency = repair.currency_id company = repair.env.company if (partner_invoice.id, currency.id, company.id) not in grouped_invoices_vals: @@ -711,7 +718,7 @@ class RepairLine(models.Model): @api.depends('price_unit', 'repair_id', 'product_uom_qty', 'product_id', 'tax_id', 'repair_id.invoice_method') def _compute_price_total_and_subtotal(self): for line in self: - taxes = line.tax_id.compute_all(line.price_unit, line.repair_id.pricelist_id.currency_id, line.product_uom_qty, line.product_id, line.repair_id.partner_id) + taxes = line.tax_id.compute_all(line.price_unit, line.repair_id.currency_id, line.product_uom_qty, line.product_id, line.repair_id.partner_id) line.price_subtotal = taxes['total_excluded'] line.price_total = taxes['total_included'] @@ -774,30 +781,14 @@ class RepairLine(models.Model): 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 - pricelist = self.repair_id.pricelist_id - if not pricelist: - warning = { - 'title': _('No pricelist found.'), - 'message': - _('You have to select a pricelist in the Repair form !\n Please set one before choosing a product.')} - return {'warning': warning} - else: - self._onchange_product_uom() + self._onchange_product_uom() @api.onchange('product_uom') def _onchange_product_uom(self): - pricelist = self.repair_id.pricelist_id - if pricelist and self.product_id and self.type != 'remove': - price = pricelist._get_product_price(self.product_id, self.product_uom_qty, uom=self.product_uom) - if price is False: - warning = { - 'title': _('No valid pricelist line found.'), - 'message': - _("Couldn't find a pricelist line matching this product and quantity.\nYou have to change either the product, the quantity or the pricelist.")} - return {'warning': warning} - else: - self.price_unit = price + if self.product_id and self.type != 'remove': + price = self.repair_id.pricelist_id._get_product_price( + self.product_id, self.product_uom_qty, uom=self.product_uom) + self.price_unit = price class RepairFee(models.Model): @@ -833,7 +824,7 @@ class RepairFee(models.Model): @api.depends('price_unit', 'repair_id', 'product_uom_qty', 'product_id', 'tax_id') def _compute_price_total_and_subtotal(self): for fee in self: - taxes = fee.tax_id.compute_all(fee.price_unit, fee.repair_id.pricelist_id.currency_id, fee.product_uom_qty, fee.product_id, fee.repair_id.partner_id) + taxes = fee.tax_id.compute_all(fee.price_unit, fee.repair_id.currency_id, fee.product_uom_qty, fee.product_id, fee.repair_id.partner_id) fee.price_subtotal = taxes['total_excluded'] fee.price_total = taxes['total_included'] @@ -853,7 +844,6 @@ class RepairFee(models.Model): partner = self.repair_id.partner_id partner_invoice = self.repair_id.partner_invoice_id or partner - pricelist = self.repair_id.pricelist_id if partner and self.product_id: fpos = self.env['account.fiscal.position']._get_fiscal_position(partner_invoice, delivery=self.repair_id.address_id) @@ -870,29 +860,14 @@ class RepairFee(models.Model): else: self.name += '\n' + self.product_id.description_sale - warning = False - if not pricelist: - warning = { - 'title': _('No pricelist found.'), - 'message': - _('You have to select a pricelist in the Repair form !\n Please set one before choosing a product.')} - return {'warning': warning} - else: - self._onchange_product_uom() + self._onchange_product_uom() @api.onchange('product_uom') def _onchange_product_uom(self): - pricelist = self.repair_id.pricelist_id - if pricelist and self.product_id: - price = pricelist._get_product_price(self.product_id, self.product_uom_qty, uom=self.product_uom) - if price is False: - warning = { - 'title': _('No valid pricelist line found.'), - 'message': - _("Couldn't find a pricelist line matching this product and quantity.\nYou have to change either the product, the quantity or the pricelist.")} - return {'warning': warning} - else: - self.price_unit = price + if self.product_id: + price = self.repair_id.pricelist_id._get_product_price( + self.product_id, self.product_uom_qty, uom=self.product_uom) + self.price_unit = price class RepairTags(models.Model): diff --git a/addons/repair/report/repair_templates_repair_order.xml b/addons/repair/report/repair_templates_repair_order.xml index 06638905192..29665895125 100644 --- a/addons/repair/report/repair_templates_repair_order.xml +++ b/addons/repair/report/repair_templates_repair_order.xml @@ -84,7 +84,7 @@ - +
@@ -108,7 +108,7 @@ + t-options='{"widget": "monetary", "display_currency": o.currency_id}'/> @@ -124,21 +124,21 @@ Total Without Taxes + t-options='{"widget": "monetary", "display_currency": o.currency_id}'/> Taxes + t-options='{"widget": "monetary", "display_currency": o.currency_id}'/> Total + t-options='{"widget": "monetary", "display_currency": o.currency_id}'/> diff --git a/addons/sale/controllers/portal.py b/addons/sale/controllers/portal.py index 724a18b2622..503eaa46d48 100644 --- a/addons/sale/controllers/portal.py +++ b/addons/sale/controllers/portal.py @@ -210,7 +210,7 @@ class CustomerPortal(portal.CustomerPortal): providers_sudo, logged_in=logged_in, sale_order_id=order_sudo.id ), 'amount': order_sudo.amount_total, - 'currency': order_sudo.pricelist_id.currency_id, + 'currency': order_sudo.currency_id, 'partner_id': order_sudo.partner_id.id, 'access_token': order_sudo.access_token, 'transaction_route': order_sudo.get_portal_url(suffix='/transaction'), diff --git a/addons/sale/data/mail_template_data.xml b/addons/sale/data/mail_template_data.xml index be542b12212..993f4000982 100644 --- a/addons/sale/data/mail_template_data.xml +++ b/addons/sale/data/mail_template_data.xml @@ -20,14 +20,14 @@ (with reference: ) - amounting in $ 10.00 is available. + amounting in $ 10.00 is available. quotation (with reference: S00052 ) - amounting in $ 10.00 is ready for review. + amounting in $ 10.00 is ready for review.

Do not hesitate to contact us if you have any questions. diff --git a/addons/sale/data/sale_demo.xml b/addons/sale/data/sale_demo.xml index 79ce4a16484..93fd0c76951 100644 --- a/addons/sale/data/sale_demo.xml +++ b/addons/sale/data/sale_demo.xml @@ -29,7 +29,6 @@ - @@ -63,7 +62,6 @@ - @@ -91,7 +89,6 @@ - @@ -116,7 +113,6 @@ - @@ -155,7 +151,6 @@ - @@ -174,7 +169,6 @@ - @@ -193,7 +187,6 @@ - @@ -231,7 +224,6 @@ - @@ -259,7 +251,6 @@ - @@ -286,7 +277,6 @@ - @@ -315,7 +305,6 @@ - @@ -342,7 +331,6 @@ - @@ -369,7 +357,6 @@ - @@ -394,7 +381,6 @@ - @@ -421,7 +407,6 @@ - @@ -448,7 +433,6 @@ - @@ -475,7 +459,6 @@ - @@ -502,7 +485,6 @@ - @@ -529,7 +511,6 @@ - sent @@ -563,7 +544,6 @@ - diff --git a/addons/sale/models/product_template.py b/addons/sale/models/product_template.py index f95ca313b61..03b7b74c684 100644 --- a/addons/sale/models/product_template.py +++ b/addons/sale/models/product_template.py @@ -157,7 +157,10 @@ class ProductTemplate(models.Model): }] return res - def _get_combination_info(self, combination=False, product_id=False, add_qty=1, pricelist=False, parent_combination=False, only_template=False): + def _get_combination_info( + self, combination=False, product_id=False, add_qty=1, pricelist=False, + parent_combination=False, only_template=False + ): """ Return info about a given combination. Note: this method does not take into account whether the combination is @@ -209,12 +212,13 @@ class ProductTemplate(models.Model): # get the name before the change of context to benefit from prefetch display_name = self.display_name - display_image = True quantity = self.env.context.get('quantity', add_qty) product_template = self combination = combination or product_template.env['product.template.attribute.value'] + pricelist = pricelist or self.env['product.pricelist'] + if not product_id and not combination and not only_template: combination = product_template._get_first_possible_combination(parent_combination) @@ -242,10 +246,7 @@ class ProductTemplate(models.Model): no_variant_attributes_price_extra=tuple(no_variant_attributes_price_extra) ) list_price = product.price_compute('list_price')[product.id] - if pricelist: - price = pricelist._get_product_price(product, quantity) - else: - price = list_price + price = pricelist._get_product_price(product, quantity) display_image = bool(product.image_128) display_name = product.display_name price_extra = (product.price_extra or 0.0) + (sum(no_variant_attributes_price_extra) or 0.0) @@ -254,28 +255,26 @@ class ProductTemplate(models.Model): product_template = product_template.with_context(current_attributes_price_extra=current_attributes_price_extra) price_extra = sum(current_attributes_price_extra) list_price = product_template.price_compute('list_price')[product_template.id] - if pricelist: - price = pricelist._get_product_price(product_template, quantity) - else: - price = list_price + price = pricelist._get_product_price(product_template, quantity) display_image = bool(product_template.image_128) combination_name = combination._get_combination_name() if combination_name: display_name = "%s (%s)" % (display_name, combination_name) - if pricelist and pricelist.currency_id != product_template.currency_id: + currency = pricelist.currency_id or self.env.company.currency_id + if currency != product_template.currency_id: list_price = product_template.currency_id._convert( - list_price, pricelist.currency_id, product_template._get_current_company(pricelist=pricelist), + list_price, currency, product_template._get_current_company(pricelist=pricelist), fields.Date.today() ) price_extra = product_template.currency_id._convert( - price_extra, pricelist.currency_id, product_template._get_current_company(pricelist=pricelist), + price_extra, currency, product_template._get_current_company(pricelist=pricelist), fields.Date.today() ) price_without_discount = list_price if pricelist and pricelist.discount_policy == 'without_discount' else price - has_discounted_price = (pricelist or product_template).currency_id.compare_amounts(price_without_discount, price) == 1 + has_discounted_price = currency.compare_amounts(price_without_discount, price) == 1 return { 'product_id': product.id, diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index 99375caa545..2c1860216d3 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -172,15 +172,18 @@ class SaleOrder(models.Model): comodel_name='product.pricelist', string="Pricelist", compute='_compute_pricelist_id', - store=True, readonly=False, precompute=True, check_company=True, required=True, # Unrequired company + store=True, readonly=False, precompute=True, check_company=True, # Unrequired company states=READONLY_FIELD_STATES, tracking=1, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", 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, precompute=True, ondelete="restrict") + comodel_name='res.currency', + compute='_compute_currency_id', + store=True, + precompute=True, + ondelete='restrict' + ) currency_rate = fields.Float( string="Currency Rate", compute='_compute_currency_rate', @@ -284,10 +287,11 @@ class SaleOrder(models.Model): show_update_fpos = fields.Boolean( string="Has Fiscal Position Changed", store=False) # True if the fiscal position was changed + has_active_pricelist = fields.Boolean( + compute='_compute_has_active_pricelist') show_update_pricelist = fields.Boolean( string="Has Pricelist Changed", store=False) # True if the pricelist was changed - def init(self): create_index(self._cr, 'sale_order_date_order_id_idx', 'sale_order', ["date_order desc", "id desc"]) @@ -368,15 +372,22 @@ class SaleOrder(models.Model): order = order.with_company(order.company_id) order.payment_term_id = order.partner_id.property_payment_term_id - @api.depends('partner_id') + @api.depends('partner_id', 'company_id') def _compute_pricelist_id(self): for order in self: + if order.state != 'draft': + continue if not order.partner_id: order.pricelist_id = False continue order = order.with_company(order.company_id) order.pricelist_id = order.partner_id.property_product_pricelist + @api.depends('pricelist_id', 'company_id') + def _compute_currency_id(self): + for order in self: + order.currency_id = order.pricelist_id.currency_id or order.company_id.currency_id + @api.depends('currency_id', 'date_order', 'company_id') def _compute_currency_rate(self): cache = {} @@ -398,6 +409,14 @@ class SaleOrder(models.Model): ) order.currency_rate = cache[key] + @api.depends('company_id') + def _compute_has_active_pricelist(self): + for order in self: + order.has_active_pricelist = bool(self.env['product.pricelist'].search( + [('company_id', 'in', (False, order.company_id.id)), ('active', '=', True)], + limit=1, + )) + @api.depends('partner_id') def _compute_user_id(self): for order in self: @@ -619,6 +638,19 @@ class SaleOrder(models.Model): } } + @api.onchange('company_id') + def _onchange_company_id_warning(self): + self.show_update_pricelist = True + if self.order_line and self.state == 'draft': + return { + 'warning': { + 'title': _("Warning for the change of your quotation's company"), + 'message': _("Changing the company of an existing quotation might need some " + "manual adjustments in the details of the lines. You might " + "consider updating the prices."), + } + } + @api.onchange('fiscal_position_id') def _onchange_fpos_id_show_update_fpos(self): if self.order_line and ( @@ -655,8 +687,7 @@ class SaleOrder(models.Model): @api.onchange('pricelist_id') def _onchange_pricelist_id_show_update_prices(self): - if self.order_line and self.pricelist_id and self._origin.pricelist_id != self.pricelist_id: - self.show_update_pricelist = True + self.show_update_pricelist = bool(self.order_line) #=== CRUD METHODS ===# @@ -936,10 +967,13 @@ class SaleOrder(models.Model): self._recompute_prices() if self.pricelist_id: - self.message_post(body=_( + message = _( "Product prices have been recomputed according to pricelist %s.", self.pricelist_id._get_html_link(), - )) + ) + else: + message = _("Product prices have been recomputed.") + self.message_post(body=message) def _recompute_prices(self): lines_to_recompute = self.order_line.filtered(lambda line: not line.display_type) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 1e6adcb837d..3c377948137 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -396,7 +396,7 @@ class SaleOrderLine(models.Model): else: line.pricelist_item_id = line.order_id.pricelist_id._get_product_rule( line.product_id, - line.product_uom_qty or 1.0, + quantity=line.product_uom_qty or 1.0, uom=line.product_uom, date=line.order_id.date_order, ) @@ -408,7 +408,7 @@ class SaleOrderLine(models.Model): # manually edited if line.qty_invoiced > 0: continue - if not line.product_uom or not line.product_id or not line.order_id.pricelist_id: + if not line.product_uom or not line.product_id: line.price_unit = 0.0 else: price = line.with_company(line.company_id)._get_display_price() @@ -459,11 +459,11 @@ class SaleOrderLine(models.Model): pricelist_rule = self.pricelist_item_id order_date = self.order_id.date_order or fields.Date.today() product = self.product_id.with_context(**self._get_product_price_context()) - qty = self.product_uom_qty or 1.0 + quantity = self.product_uom_qty or 1.0 uom = self.product_uom or self.product_id.uom_id price = pricelist_rule._compute_price( - product, qty, uom, order_date, currency=self.currency_id) + product, quantity, uom, order_date, currency=self.currency_id) return price @@ -502,7 +502,7 @@ class SaleOrderLine(models.Model): pricelist_rule = self.pricelist_item_id order_date = self.order_id.date_order or fields.Date.today() product = self.product_id.with_context(**self._get_product_price_context()) - qty = self.product_uom_qty or 1.0 + quantity = self.product_uom_qty or 1.0 uom = self.product_uom if pricelist_rule: @@ -512,14 +512,14 @@ class SaleOrderLine(models.Model): # to show the discount to the customer. while pricelist_item.base == 'pricelist' and pricelist_item.base_pricelist_id.discount_policy == 'without_discount': rule_id = pricelist_item.base_pricelist_id._get_product_rule( - product, qty, uom=uom, date=order_date) + product, quantity, currency=self.currency_id, uom=uom, date=order_date) pricelist_item = self.env['product.pricelist.item'].browse(rule_id) pricelist_rule = pricelist_item price = pricelist_rule._compute_base_price( product, - qty, + quantity, uom, order_date, target_currency=self.currency_id, diff --git a/addons/sale/report/ir_actions_report_templates.xml b/addons/sale/report/ir_actions_report_templates.xml index 2388c86bcb6..4b0a86070c8 100644 --- a/addons/sale/report/ir_actions_report_templates.xml +++ b/addons/sale/report/ir_actions_report_templates.xml @@ -133,7 +133,7 @@ Subtotal diff --git a/addons/sale/tests/test_sale_prices.py b/addons/sale/tests/test_sale_prices.py index 22d2eb27b74..a5d11f8bb42 100644 --- a/addons/sale/tests/test_sale_prices.py +++ b/addons/sale/tests/test_sale_prices.py @@ -486,6 +486,7 @@ class TestSalePrices(SaleCommon): """ sale_order = self.sale_order so_amount = sale_order.amount_total + start_so_amount = so_amount sale_order._recompute_prices() self.assertEqual( sale_order.amount_total, so_amount, @@ -512,6 +513,17 @@ class TestSalePrices(SaleCommon): self.assertEqual(sale_order.amount_undiscounted, so_amount) self.assertEqual(sale_order.amount_total, 0.95*so_amount) + # Test taking off the pricelist + sale_order.pricelist_id = False + sale_order._recompute_prices() + + self.assertTrue(all(line.discount == 0 for line in sale_order.order_line)) + self.assertEqual(sale_order.amount_undiscounted, so_amount) + self.assertEqual( + sale_order.amount_total, start_so_amount, + "The SO amount without pricelist should be the same than with an empty pricelist" + ) + # Taxes tests: # We do not rely on accounting common on purpose to avoid # all the useless setup not needed here. diff --git a/addons/sale/tests/test_sale_product_attribute_value_config.py b/addons/sale/tests/test_sale_product_attribute_value_config.py index fdc93d2f0ec..48f134649c7 100644 --- a/addons/sale/tests/test_sale_product_attribute_value_config.py +++ b/addons/sale/tests/test_sale_product_attribute_value_config.py @@ -8,6 +8,13 @@ from odoo.tests import tagged class TestSaleProductAttributeValueCommon(TestProductAttributeValueCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.env['product.pricelist'].sudo().search([]).action_archive() + cls.env['product.pricelist'].create({'name': 'Base Pricelist'}) + @classmethod def _setup_currency(cls, currency_ratio=2): """Get or create a currency. This makes the test non-reliant on demo. diff --git a/addons/sale/views/sale_order_views.xml b/addons/sale/views/sale_order_views.xml index 06388a92e41..c518ab929c4 100644 --- a/addons/sale/views/sale_order_views.xml +++ b/addons/sale/views/sale_order_views.xml @@ -264,9 +264,14 @@