From 17bece3e797913bcba8dd7e07fc8541c0a45e3f7 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Fri, 17 Mar 2023 15:41:12 +0000 Subject: [PATCH] [REF] sale,*: remove locked (done) SO state It's not a specific state, it should be considered separately, as a boolean. This allows to reduce code complexity (flows should not rely (much) on the locked logic) and to have a clearer flow. Task-3163931 Part-of: odoo/odoo#115871 --- addons/delivery/views/sale_order_views.xml | 51 ++++---- addons/event_sale/report/event_sale_report.py | 20 ++- .../report/event_sale_report_views.xml | 2 +- .../models/project_project.py | 2 +- addons/sale/controllers/portal.py | 2 +- addons/sale/models/account_move.py | 13 +- addons/sale/models/account_move_line.py | 31 +++-- addons/sale/models/crm_team.py | 2 +- addons/sale/models/payment_transaction.py | 4 +- addons/sale/models/res_partner.py | 4 +- addons/sale/models/sale_order.py | 80 ++++++------ addons/sale/models/sale_order_line.py | 27 ++-- addons/sale/report/sale_report.py | 12 +- addons/sale/tests/test_payment_flow.py | 3 +- addons/sale/tests/test_sale_order.py | 10 +- addons/sale/views/sale_order_views.xml | 117 ++++++++++-------- addons/sale/views/sale_portal_templates.xml | 6 +- addons/sale_crm/models/crm_team.py | 6 +- addons/sale_loyalty/models/sale_order.py | 8 +- addons/sale_loyalty/models/sale_order_line.py | 6 +- .../sale_loyalty/views/sale_order_views.xml | 23 ++-- addons/sale_mrp/models/mrp_bom.py | 2 +- addons/sale_mrp/tests/test_sale_mrp_flow.py | 2 +- addons/sale_project/models/project.py | 8 +- .../security/sale_project_security.xml | 2 +- addons/sale_stock/models/sale_order.py | 2 +- addons/sale_stock/models/sale_order_line.py | 21 ++-- addons/sale_stock/tests/test_sale_stock.py | 56 +-------- addons/sale_stock/views/sale_order_views.xml | 2 +- addons/sale_timesheet/models/account.py | 2 +- addons/sale_timesheet/models/project.py | 11 +- .../models/project_sale_line_employee_map.py | 2 +- addons/sale_timesheet/models/sale_order.py | 2 +- .../sale_timesheet/tests/test_sale_service.py | 4 +- .../views/project_task_views.xml | 4 +- .../views/sale_timesheet_portal_templates.xml | 2 +- addons/stock_delivery/models/sale_order.py | 2 +- addons/website_sale/controllers/backend.py | 10 +- .../models/website_snippet_filter.py | 4 +- .../website_sale/report/sale_report_views.xml | 2 +- .../website_sale/views/sale_order_views.xml | 4 +- .../data/sale_order_demo.xml | 12 +- 42 files changed, 284 insertions(+), 301 deletions(-) diff --git a/addons/delivery/views/sale_order_views.xml b/addons/delivery/views/sale_order_views.xml index d21e76133c6..dcce887520c 100644 --- a/addons/delivery/views/sale_order_views.xml +++ b/addons/delivery/views/sale_order_views.xml @@ -11,31 +11,32 @@ - -
-
-
+
+ 0 +
+
+
diff --git a/addons/event_sale/report/event_sale_report.py b/addons/event_sale/report/event_sale_report.py index 92d034e845c..799aa2fcaf5 100644 --- a/addons/event_sale/report/event_sale_report.py +++ b/addons/event_sale/report/event_sale_report.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import fields, models, tools +from odoo.addons.sale.models.sale_order import SALE_ORDER_STATE class EventSaleReport(models.Model): @@ -31,13 +32,8 @@ class EventSaleReport(models.Model): sale_order_id = fields.Many2one('sale.order', readonly=True) sale_order_date = fields.Datetime('Order Date', readonly=True) sale_order_partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) - sale_order_state = fields.Selection([ - ('draft', 'Quotation'), - ('sent', 'Quotation Sent'), - ('sale', 'Sales Order'), - ('done', 'Locked'), - ('cancel', 'Cancelled'), - ], string='Sale Order Status', readonly=True) + sale_order_state = fields.Selection( + selection=SALE_ORDER_STATE, string='Sale Order Status', readonly=True) sale_order_user_id = fields.Many2one('res.users', string='Salesperson', readonly=True) sale_order_line_id = fields.Many2one('sale.order.line', readonly=True) sale_price = fields.Float('Revenues', readonly=True) @@ -66,7 +62,7 @@ class EventSaleReport(models.Model): def _with_clause(self, *with_): # Extra clauses formatted as `cte1 AS (SELECT ...)`, `cte2 AS (SELECT ...)`... return """ -WITH +WITH """ + ',\n '.join(with_) if with_ else '' def _select_clause(self, *select): @@ -74,19 +70,19 @@ WITH return """ SELECT ROW_NUMBER() OVER (ORDER BY event_registration.id) AS id, - + event_registration.id AS event_registration_id, event_registration.company_id AS company_id, event_registration.event_id AS event_id, event_registration.event_ticket_id AS event_ticket_id, event_registration.create_date AS event_registration_create_date, event_registration.name AS event_registration_name, - event_registration.state AS event_registration_state, + event_registration.state AS event_registration_state, event_registration.active AS active, event_registration.sale_order_id AS sale_order_id, event_registration.sale_order_line_id AS sale_order_line_id, event_registration.is_paid AS is_paid, - + event_event.event_type_id AS event_type_id, event_event.date_begin AS event_date_begin, event_event.date_end AS event_date_end, @@ -98,7 +94,7 @@ SELECT sale_order.partner_id AS sale_order_partner_id, sale_order.state AS sale_order_state, sale_order.user_id AS sale_order_user_id, - + sale_order_line.product_id AS product_id, CASE WHEN sale_order_line.product_uom_qty = 0 THEN 0 diff --git a/addons/event_sale/report/event_sale_report_views.xml b/addons/event_sale/report/event_sale_report_views.xml index 5847a5cf19b..ae9481508ae 100644 --- a/addons/event_sale/report/event_sale_report_views.xml +++ b/addons/event_sale/report/event_sale_report_views.xml @@ -79,7 +79,7 @@ diff --git a/addons/project_sale_expense/models/project_project.py b/addons/project_sale_expense/models/project_project.py index cb44926484b..b7353d47e89 100644 --- a/addons/project_sale_expense/models/project_project.py +++ b/addons/project_sale_expense/models/project_project.py @@ -35,7 +35,7 @@ class Project(models.Model): [ ('order_id', 'in', list(expenses_per_so_id.keys())), ('is_expense', '=', True), - ('state', 'in', ['sale', 'done']), + ('state', '=', 'sale'), ], ['order_id', 'product_id'], ['untaxed_amount_to_invoice:sum', 'untaxed_amount_invoiced:sum']) diff --git a/addons/sale/controllers/portal.py b/addons/sale/controllers/portal.py index 061a7d0194c..d1d34ca0480 100644 --- a/addons/sale/controllers/portal.py +++ b/addons/sale/controllers/portal.py @@ -40,7 +40,7 @@ class CustomerPortal(portal.CustomerPortal): def _prepare_orders_domain(self, partner): return [ ('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]), - ('state', 'in', ['sale', 'done']) + ('state', '=', 'sale'), ] def _get_sale_searchbar_sortings(self): diff --git a/addons/sale/models/account_move.py b/addons/sale/models/account_move.py index df44162a27a..4082e0c3e4f 100644 --- a/addons/sale/models/account_move.py +++ b/addons/sale/models/account_move.py @@ -63,17 +63,16 @@ class AccountMove(models.Model): res = super(AccountMove, self).action_post() down_payment_lines = self.line_ids.filtered('is_downpayment') for line in down_payment_lines: + if any(order.locked for order in line.sale_line_ids.order_id): + # We cannot change lines content on locked SO, changes on invoices are not + # forwarded to the SO if the SO is locked. + continue if not line.sale_line_ids.display_type: line.sale_line_ids._compute_name() - try: - line.sale_line_ids.tax_id = line.tax_ids - line.sale_line_ids.price_unit = line.price_unit - except UserError: - # a UserError here means the SO was locked, which prevents changing the taxes - # just ignore the error - this is a nice to have feature and should not be blocking - pass + line.sale_line_ids.tax_id = line.tax_ids + line.sale_line_ids.price_unit = line.price_unit return res def button_draft(self): diff --git a/addons/sale/models/account_move_line.py b/addons/sale/models/account_move_line.py index e64cc2747e9..012c0b9d5c8 100644 --- a/addons/sale/models/account_move_line.py +++ b/addons/sale/models/account_move_line.py @@ -76,15 +76,28 @@ class AccountMoveLine(models.Model): continue # raise if the sale order is not currently open - if sale_order.state != 'sale': - message_unconfirmed = _('The Sales Order %s linked to the Analytic Account %s must be validated before registering expenses.') - messages = { - 'draft': message_unconfirmed, - 'sent': message_unconfirmed, - 'done': _('The Sales Order %s linked to the Analytic Account %s is currently locked. You cannot register an expense on a locked Sales Order. Please create a new SO linked to this Analytic Account.'), - 'cancel': _('The Sales Order %s linked to the Analytic Account %s is cancelled. You cannot register an expense on a cancelled Sales Order.'), - } - raise UserError(messages[sale_order.state] % (sale_order.name, sale_order.analytic_account_id.name)) + if sale_order.state in ('draft', 'sent'): + raise UserError(_( + "The Sales Order %(order)s linked to the Analytic Account %(account)s must be" + " validated before registering expenses.", + order=sale_order.name, + account=sale_order.analytic_account_id.name, + )) + elif sale_order.state == 'cancel': + raise UserError(_( + "The Sales Order %(order)s linked to the Analytic Account %(account)s is cancelled." + " You cannot register an expense on a cancelled Sales Order.", + order=sale_order.name, + account=sale_order.analytic_account_id.name, + )) + elif sale_order.locked: + raise UserError(_( + "The Sales Order %(order)s linked to the Analytic Account %(account)s is currently locked." + " You cannot register an expense on a locked Sales Order." + " Please create a new SO linked to this Analytic Account.", + order=sale_order.name, + account=sale_order.analytic_account_id.name, + )) price = move_line._sale_get_invoice_price(sale_order) diff --git a/addons/sale/models/crm_team.py b/addons/sale/models/crm_team.py index 6b36dee08eb..aad2569f47a 100644 --- a/addons/sale/models/crm_team.py +++ b/addons/sale/models/crm_team.py @@ -130,7 +130,7 @@ class CrmTeam(models.Model): def _extra_sql_conditions(self): if self._in_sale_scope(): - return "AND state in ('sale', 'done')" + return "AND state = 'sale'" return super()._extra_sql_conditions() def _graph_title_and_key(self): diff --git a/addons/sale/models/payment_transaction.py b/addons/sale/models/payment_transaction.py index 1fb5a8922e2..221556cc541 100644 --- a/addons/sale/models/payment_transaction.py +++ b/addons/sale/models/payment_transaction.py @@ -151,7 +151,7 @@ class PaymentTransaction(models.Model): ('is_move_sent', '=', False), ('state', '=', 'posted'), ])), - ('sale_order_ids.state', 'in', ('sale', 'done')), + ('sale_order_ids.state', '=', 'sale'), ('last_state_change', '>=', retry_limit_date), ])._send_invoice() @@ -159,7 +159,7 @@ class PaymentTransaction(models.Model): for tx in self.filtered(lambda tx: tx.sale_order_ids): # Create invoices tx = tx.with_company(tx.company_id).with_context(company_id=tx.company_id.id) - confirmed_orders = tx.sale_order_ids.filtered(lambda so: so.state in ('sale', 'done')) + confirmed_orders = tx.sale_order_ids.filtered(lambda so: so.state == 'sale') if confirmed_orders: confirmed_orders._force_lines_to_invoice_policy_order() invoices = confirmed_orders.with_context( diff --git a/addons/sale/models/res_partner.py b/addons/sale/models/res_partner.py index de1fc21f665..f10da7aa9d5 100644 --- a/addons/sale/models/res_partner.py +++ b/addons/sale/models/res_partner.py @@ -44,7 +44,7 @@ class ResPartner(models.Model): SaleOrder = self.env['sale.order'] has_so = SaleOrder.search([ ('partner_id', 'child_of', self.commercial_partner_id.id), - ('state', 'in', ['sent', 'sale', 'done']) + ('state', 'in', ['sent', 'sale']) ], limit=1) return can_edit_vat and not bool(has_so) @@ -56,7 +56,7 @@ class ResPartner(models.Model): def _credit_debit_get(self): super()._credit_debit_get() - domain = [('partner_id', 'in', self.ids), ('state', 'in', ['sale', 'done'])] + domain = [('partner_id', 'in', self.ids), ('state', '=', 'sale')] group = self.env['sale.order']._read_group(domain, ['partner_id'], ['amount_to_invoice:sum']) for partner, amount_to_invoice_sum in group: partner.credit += amount_to_invoice_sum diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index 832edba7572..aab0b2253ed 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from datetime import timedelta @@ -8,7 +7,6 @@ from markupsafe import escape from odoo import api, fields, models, SUPERUSER_ID, _ from odoo.exceptions import AccessError, UserError, ValidationError from odoo.fields import Command -from odoo.osv import expression from odoo.tools import float_is_zero, format_amount, format_date, html_keep_url, is_html_empty from odoo.tools.sql import create_index @@ -16,12 +14,7 @@ from odoo.addons.payment import utils as payment_utils READONLY_FIELD_STATES = { state: [('readonly', True)] - for state in {'sale', 'done', 'cancel'} -} - -LOCKED_FIELD_STATES = { - state: [('readonly', True)] - for state in {'done', 'cancel'} + for state in {'sale', 'cancel'} } INVOICE_STATUS = [ @@ -31,6 +24,13 @@ INVOICE_STATUS = [ ('no', 'Nothing to Invoice') ] +SALE_ORDER_STATE = [ + ('draft', "Quotation"), + ('sent', "Quotation Sent"), + ('sale', "Sales Order"), + ('cancel', "Cancelled"), +] + class SaleOrder(models.Model): _name = 'sale.order' @@ -41,7 +41,7 @@ class SaleOrder(models.Model): _sql_constraints = [ ('date_order_conditional_required', - "CHECK((state IN ('sale', 'done') AND date_order IS NOT NULL) OR state NOT IN ('sale', 'done'))", + "CHECK((state = 'sale' AND date_order IS NOT NULL) OR state != 'sale')", "A confirmed sales order requires a confirmation date."), ] @@ -72,24 +72,18 @@ class SaleOrder(models.Model): states=READONLY_FIELD_STATES, domain="[('type', '!=', 'private'), ('company_id', 'in', (False, company_id))]") state = fields.Selection( - selection=[ - ('draft', "Quotation"), - ('sent', "Quotation Sent"), - ('sale', "Sales Order"), - ('done', "Locked"), - ('cancel', "Cancelled"), - ], + selection=SALE_ORDER_STATE, string="Status", readonly=True, copy=False, index=True, tracking=3, default='draft') + locked = fields.Boolean(default=False, copy=False, help="Locked orders cannot be modified.") client_order_ref = fields.Char(string="Customer Reference", copy=False) create_date = fields.Datetime( # Override of default create_date field from ORM string="Creation Date", index=True, readonly=True) commitment_date = fields.Datetime( string="Delivery Date", copy=False, - states=LOCKED_FIELD_STATES, 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.") @@ -144,14 +138,12 @@ class SaleOrder(models.Model): string="Invoice Address", compute='_compute_partner_invoice_id', store=True, readonly=False, required=True, precompute=True, - states=LOCKED_FIELD_STATES, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") partner_shipping_id = fields.Many2one( comodel_name='res.partner', string="Delivery Address", compute='_compute_partner_shipping_id', store=True, readonly=False, required=True, precompute=True, - states=LOCKED_FIELD_STATES, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",) fiscal_position_id = fields.Many2one( @@ -212,7 +204,6 @@ class SaleOrder(models.Model): comodel_name='sale.order.line', inverse_name='order_id', string="Order Lines", - states=LOCKED_FIELD_STATES, copy=True, auto_join=True) amount_untaxed = fields.Monetary(string="Untaxed Amount", store=True, compute='_compute_amounts', tracking=5) @@ -525,9 +516,8 @@ class SaleOrder(models.Model): - invoiced: if all SO lines are invoiced, the SO is invoiced. - upselling: if all SO lines are invoiced or upselling, the status is upselling. """ - unconfirmed_orders = self.filtered(lambda so: so.state not in ['sale', 'done']) - unconfirmed_orders.invoice_status = 'no' - confirmed_orders = self - unconfirmed_orders + confirmed_orders = self.filtered(lambda so: so.state == 'sale') + (self - confirmed_orders).invoice_status = 'no' if not confirmed_orders: return line_invoice_status_all = [ @@ -540,7 +530,7 @@ class SaleOrder(models.Model): ['order_id', 'invoice_status'])] for order in confirmed_orders: line_invoice_status = [d[1] for d in line_invoice_status_all if d[0] == order.id] - if order.state not in ('sale', 'done'): + if order.state != 'sale': order.invoice_status = 'no' elif any(invoice_status == 'to invoice' for invoice_status in line_invoice_status): order.invoice_status = 'to invoice' @@ -819,7 +809,7 @@ class SaleOrder(models.Model): :rtype: record of `mail.template` or `None` if not found """ self.ensure_one() - if self.env.context.get('proforma') or self.state not in ('sale', 'done'): + if self.env.context.get('proforma') or self.state != 'sale': return self.env.ref('sale.email_template_edi_sale', raise_if_not_found=False) else: return self._get_confirmation_template() @@ -845,7 +835,7 @@ class SaleOrder(models.Model): :raise: UserError if any given SO is not in draft state. """ - if self.filtered(lambda so: so.state != 'draft'): + if any(order.state != 'draft' for order in self): raise UserError(_("Only draft orders can be marked as sent directly.")) for order in self: @@ -860,12 +850,12 @@ class SaleOrder(models.Model): :return: True :rtype: bool - :raise: UserError if trying to confirm locked or cancelled SO's + :raise: UserError if trying to confirm cancelled SO's """ - if self._get_forbidden_state_confirm() & set(self.mapped('state')): + if not all(order._can_be_confirmed() for order in self): raise UserError(_( - "It is not allowed to confirm an order in the following states: %s", - ", ".join(self._get_forbidden_state_confirm()), + "The following orders are not in a state requiring confirmation: %s", + ", ".join(self.mapped('display_name')), )) self.order_line._validate_analytic_distribution() @@ -885,12 +875,13 @@ class SaleOrder(models.Model): self.with_context(context)._action_confirm() if self.env.user.has_group('sale.group_auto_done_setting'): - self.action_done() + self.action_lock() return True - def _get_forbidden_state_confirm(self): - return {'done', 'cancel'} + def _can_be_confirmed(self): + self.ensure_one() + return self.state in {'draft', 'sent'} def _prepare_confirmation_values(self): """ Prepare the sales order confirmation values. @@ -959,16 +950,16 @@ class SaleOrder(models.Model): subtype_xmlid='mail.mt_comment', ) - def action_done(self): + def action_lock(self): for order in self: tx = order.sudo().transaction_ids._get_last() if tx and tx.state == 'pending' and tx.provider_id.code == 'custom': tx._set_done() tx.write({'is_post_processed': True}) - self.write({'state': 'done'}) + self.locked = True def action_unlock(self): - self.write({'state': 'sale'}) + self.locked = False def action_cancel(self): """ Cancel SO after showing the cancel wizard when needed. (cfr :meth:`_show_cancel_wizard`) @@ -977,6 +968,8 @@ class SaleOrder(models.Model): note: self.ensure_one() if the wizard is shown. """ + if any(order.locked for order in self): + raise UserError(_("You cannot cancel a locked order. Please unlock it first.")) cancel_warning = self._show_cancel_wizard() if cancel_warning: self.ensure_one() @@ -1424,11 +1417,16 @@ class SaleOrder(models.Model): # PAYMENT # def _force_lines_to_invoice_policy_order(self): + """Force the qty_to_invoice to be computed as if the invoice_policy + was set to "Ordered quantities", independently of the product configuration. + + This is needed for the automatic invoice logic, as we want to automatically + invoice the full SO when it's paid. + """ for line in self.order_line: - if self.state in ['sale', 'done']: + if line.state == 'sale': + # No need to set 0 as it is already the standard logic in the compute method. line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced - else: - line.qty_to_invoice = 0 def payment_action_capture(self): """ Capture all transactions linked to this sale order. """ @@ -1534,7 +1532,7 @@ class SaleOrder(models.Model): def _get_report_base_filename(self): self.ensure_one() - return '%s %s' % (self.type_name, self.name) + return f'{self.type_name} {self.name}' #=== CORE METHODS OVERRIDES ===# @@ -1564,7 +1562,7 @@ class SaleOrder(models.Model): for order in self: name = order.name if order.partner_id.name: - name = '%s - %s' % (name, order.partner_id.name) + name = f'{name} - {order.partner_id.name}' res.append((order.id, name)) return res return super().name_get() diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 92df46de4a5..2132330a960 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -764,7 +764,7 @@ class SaleOrderLine(models.Model): calculated from the ordered quantity. Otherwise, the quantity delivered is used. """ for line in self: - if line.state in ['sale', 'done'] and not line.display_type: + if line.state == 'sale' and not line.display_type: if line.product_id.invoice_policy == 'order': line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced else: @@ -776,20 +776,19 @@ class SaleOrderLine(models.Model): def _compute_invoice_status(self): """ Compute the invoice status of a SO line. Possible statuses: - - no: if the SO is not in status 'sale' or 'done', we consider that there is nothing to + - no: if the SO is not in status 'sale', we consider that there is nothing to invoice. This is also the 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 `_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 - occurs only in state 'sale', so that when a SO is set to done, the upselling opportunity - is removed from the list. + occurs only in state 'sale', the upselling opportunity is removed from the list. - invoiced: the quantity invoiced is larger or equal to the quantity ordered. """ precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') for line in self: - if line.state not in ('sale', 'done'): + if line.state != 'sale': line.invoice_status = 'no' elif line.is_downpayment and line.untaxed_amount_to_invoice == 0: line.invoice_status = 'invoiced' @@ -835,7 +834,7 @@ class SaleOrderLine(models.Model): """ for line in self: amount_to_invoice = 0.0 - if line.state in ['sale', 'done']: + if line.state == 'sale': # Note: do not use price_subtotal field as it returns zero when the ordered quantity is # zero. It causes problem for expense line (e.i.: ordered qty = 0, deli qty = 4, # price_unit = 20 ; subtotal is zero), but when you can invoice the line, you see an @@ -888,7 +887,13 @@ class SaleOrderLine(models.Model): @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)): + if line.state == 'cancel': + line.product_updatable = False + elif line.state == 'sale' and ( + line.order_id.locked + or line.qty_invoiced > 0 + or line.qty_delivered > 0 + ): line.product_updatable = False else: line.product_updatable = True @@ -897,7 +902,7 @@ class SaleOrderLine(models.Model): def _compute_product_uom_readonly(self): for line in self: # line.ids checks whether it's a new record not yet saved - line.product_uom_readonly = line.ids and line.state in ['sale', 'done', 'cancel'] + line.product_uom_readonly = line.ids and line.state in ['sale', 'cancel'] #=== CONSTRAINT METHODS ===# @@ -967,7 +972,7 @@ class SaleOrderLine(models.Model): # Prevent writing on a locked SO. protected_fields = self._get_protected_fields() - if 'done' in self.mapped('state') and any(f in values.keys() for f in protected_fields): + if any(self.order_id.mapped('locked')) and any(f in values.keys() for f in protected_fields): protected_fields_modified = list(set(protected_fields) & set(values.keys())) if 'name' in protected_fields_modified and all(self.mapped('is_downpayment')): @@ -1031,7 +1036,7 @@ class SaleOrderLine(models.Model): """ return self.filtered( lambda line: - line.state in ('sale', 'done') + line.state == 'sale' and (line.invoice_lines or not line.is_downpayment) and not line.display_type ) @@ -1071,7 +1076,7 @@ class SaleOrderLine(models.Model): def _expected_date(self): self.ensure_one() - if self.state in ['sale', 'done'] and self.order_id.date_order: + if self.state == 'sale' and self.order_id.date_order: order_date = self.order_id.date_order else: order_date = fields.Datetime.now() diff --git a/addons/sale/report/sale_report.py b/addons/sale/report/sale_report.py index d94f62b51ff..ab8936f75d1 100644 --- a/addons/sale/report/sale_report.py +++ b/addons/sale/report/sale_report.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models, tools +from odoo.addons.sale.models.sale_order import SALE_ORDER_STATE class SaleReport(models.Model): @@ -13,7 +14,7 @@ class SaleReport(models.Model): @api.model def _get_done_states(self): - return ['sale', 'done'] + return ['sale'] # sale.order fields name = fields.Char(string="Order Reference", readonly=True) @@ -23,14 +24,7 @@ class SaleReport(models.Model): pricelist_id = fields.Many2one(comodel_name='product.pricelist', readonly=True) team_id = fields.Many2one(comodel_name='crm.team', string="Sales Team", readonly=True) user_id = fields.Many2one(comodel_name='res.users', string="Salesperson", readonly=True) - state = fields.Selection( - selection=[ - ('draft', 'Draft Quotation'), - ('sent', 'Quotation Sent'), - ('sale', 'Sales Order'), - ('done', 'Sales Done'), - ('cancel', 'Cancelled'), - ], string='Status', readonly=True) + state = fields.Selection(selection=SALE_ORDER_STATE, string="Status", readonly=True) analytic_account_id = fields.Many2one( comodel_name='account.analytic.account', string="Analytic Account", readonly=True) invoice_status = fields.Selection( diff --git a/addons/sale/tests/test_payment_flow.py b/addons/sale/tests/test_payment_flow.py index 19398a34f24..de47a75b2f0 100644 --- a/addons/sale/tests/test_payment_flow.py +++ b/addons/sale/tests/test_payment_flow.py @@ -238,7 +238,8 @@ class TestSalePayment(AccountPaymentCommon, SaleCommon, PaymentHttpCommon): with mute_logger('odoo.addons.sale.models.payment_transaction'): tx._reconcile_after_done() - self.assertEqual(self.sale_order.state, 'done') + self.assertEqual(self.sale_order.state, 'sale') + self.assertTrue(self.sale_order.locked) self.assertTrue(tx.invoice_ids) self.assertTrue(self.sale_order.invoice_ids) self.assertTrue(tx.invoice_ids.is_move_sent) diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index 1b1ec55c1bb..8fd2591eecb 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -160,14 +160,15 @@ class TestSaleOrder(SaleCommon): so_copy.with_user(self.sale_user).unlink() self.assertTrue(so_copy.unlink(), 'Sale: deleting a cancelled SO should be possible') - # SO in state 'sale' or 'done' cannot be deleted + # SO in state 'sale' cannot be deleted self.sale_order.action_confirm() self.assertTrue(self.sale_order.state == 'sale', 'Sale: SO should be in state "sale"') with self.assertRaises(UserError): self.sale_order.unlink() - self.sale_order.action_done() - self.assertTrue(self.sale_order.state == 'done', 'Sale: SO should be in state "done"') + self.sale_order.action_lock() + self.assertTrue(self.sale_order.state == 'sale') + self.assertTrue(self.sale_order.locked) with self.assertRaises(UserError): self.sale_order.unlink() @@ -312,7 +313,8 @@ class TestSaleOrder(SaleCommon): self.env.user.groups_id += self.env.ref('sale.group_auto_done_setting') self.sale_order.action_confirm() - self.assertEqual(self.sale_order.state, 'done', "The order wasn't automatically locked at confirmation.") + self.assertEqual(self.sale_order.state, 'sale') + self.assertTrue(self.sale_order.locked) with self.assertRaises(UserError): self.sale_order.action_confirm() diff --git a/addons/sale/views/sale_order_views.xml b/addons/sale/views/sale_order_views.xml index e057fc321be..7f539755604 100644 --- a/addons/sale/views/sale_order_views.xml +++ b/addons/sale/views/sale_order_views.xml @@ -93,7 +93,7 @@
+ options="{'classes': {'draft': 'info', 'cancel': 'default', 'sale': 'success'}}"/>
@@ -113,9 +113,9 @@ sample="1" decoration-muted="state == 'cancel'">
-
@@ -153,7 +153,7 @@ options="{'color_field': 'color'}" optional="hide"/>
+
+
+ + Locked +

@@ -279,20 +295,28 @@

- - - + + - -
+ +
- +
@@ -311,7 +335,7 @@ help="Recompute all prices based on this pricelist" class="btn-link mb-1 px-0" icon="fa-refresh" confirm="This will update all unit prices based on the currently set pricelist." - attrs="{'invisible': ['|', ('show_update_pricelist', '=', False), ('state', 'in', ['sale', 'done', 'cancel'])]}"/> + attrs="{'invisible': ['|', ('show_update_pricelist', '=', False), ('state', 'in', ['sale', 'cancel'])]}"/>
@@ -328,7 +352,7 @@ name="order_line" widget="section_and_note_one2many" mode="tree,kanban" - attrs="{'readonly': [('state', 'in', ('done','cancel'))]}" + attrs="{'readonly': ['|', ('state', '=', 'cancel'), ('locked', '=', True)]}" > @@ -376,22 +400,24 @@ }" /> -