# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import json import logging from odoo import api, fields, models, _ from odoo.tools import float_compare _logger = logging.getLogger(__name__) class SaleOrder(models.Model): _inherit = "sale.order" incoterm = fields.Many2one( 'account.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions.") incoterm_location = fields.Char(string='Incoterm Location') picking_policy = fields.Selection([ ('direct', 'As soon as possible'), ('one', 'When all products are ready')], string='Shipping Policy', required=True, default='direct', help="If you deliver all products at once, the delivery order will be scheduled based on the greatest " "product lead time. Otherwise, it will be based on the shortest.") warehouse_id = fields.Many2one( 'stock.warehouse', string='Warehouse', required=True, compute='_compute_warehouse_id', store=True, readonly=False, precompute=True, check_company=True) picking_ids = fields.One2many('stock.picking', 'sale_id', string='Transfers') delivery_count = fields.Integer(string='Delivery Orders', compute='_compute_picking_ids') delivery_status = fields.Selection([ ('pending', 'Not Delivered'), ('partial', 'Partially Delivered'), ('full', 'Fully Delivered'), ], string='Delivery Status', compute='_compute_delivery_status', store=True) procurement_group_id = fields.Many2one('procurement.group', 'Procurement Group', copy=False) effective_date = fields.Datetime("Effective Date", compute='_compute_effective_date', store=True, help="Completion date of the first delivery order.") expected_date = fields.Datetime( help="Delivery date you can promise to the customer, computed from the minimum lead time of " "the order lines in case of Service products. In case of shipping, the shipping policy of " "the order will be taken into account to either use the minimum or maximum lead time of " "the order lines.") json_popover = fields.Char('JSON data for the popover widget', compute='_compute_json_popover') show_json_popover = fields.Boolean('Has late picking', compute='_compute_json_popover') def _init_column(self, column_name): """ Ensure the default warehouse_id is correctly assigned At column initialization, the ir.model.fields for res.users.property_warehouse_id isn't created, which means trying to read the property field to get the default value will crash. We therefore enforce the default here, without going through the default function on the warehouse_id field. """ if column_name != "warehouse_id": return super(SaleOrder, self)._init_column(column_name) field = self._fields[column_name] default = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1) value = field.convert_to_write(default, self) value = field.convert_to_column(value, self) if value is not None: _logger.debug("Table '%s': setting default value of new column %s to %r", self._table, column_name, value) query = f'UPDATE "{self._table}" SET "{column_name}" = %s WHERE "{column_name}" IS NULL' self._cr.execute(query, (value,)) @api.depends('picking_ids.date_done') def _compute_effective_date(self): for order in self: pickings = order.picking_ids.filtered(lambda x: x.state == 'done' and x.location_dest_id.usage == 'customer') dates_list = [date for date in pickings.mapped('date_done') if date] order.effective_date = min(dates_list, default=False) @api.depends('picking_ids', 'picking_ids.state') def _compute_delivery_status(self): for order in self: if not order.picking_ids or all(p.state == 'cancel' for p in order.picking_ids): order.delivery_status = False elif all(p.state in ['done', 'cancel'] for p in order.picking_ids): order.delivery_status = 'full' elif any(p.state == 'done' for p in order.picking_ids): order.delivery_status = 'partial' else: order.delivery_status = 'pending' @api.depends('picking_policy') def _compute_expected_date(self): super(SaleOrder, self)._compute_expected_date() for order in self: dates_list = [] for line in order.order_line.filtered(lambda x: x.state != 'cancel' and not x._is_delivery() and not x.display_type): dt = line._expected_date() dates_list.append(dt) if dates_list: expected_date = min(dates_list) if order.picking_policy == 'direct' else max(dates_list) order.expected_date = fields.Datetime.to_string(expected_date) def write(self, values): if values.get('order_line') and self.state == 'sale': for order in self: pre_order_line_qty = {order_line: order_line.product_uom_qty for order_line in order.mapped('order_line') if not order_line.is_expense} if values.get('partner_shipping_id'): new_partner = self.env['res.partner'].browse(values.get('partner_shipping_id')) for record in self: picking = record.mapped('picking_ids').filtered(lambda x: x.state not in ('done', 'cancel')) message = _("""The delivery address has been changed on the Sales Order
From "%s" To "%s", You should probably update the partner on this document.""", record.partner_shipping_id.display_name, new_partner.display_name) picking.activity_schedule('mail.mail_activity_data_warning', note=message, user_id=self.env.user.id) if 'commitment_date' in values: # protagate commitment_date as the deadline of the related stock move. # TODO: Log a note on each down document deadline_datetime = values.get('commitment_date') for order in self: order.order_line.move_ids.date_deadline = deadline_datetime or order.expected_date res = super(SaleOrder, self).write(values) if values.get('order_line') and self.state == 'sale': rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure') for order in self: to_log = {} for order_line in order.order_line: if order_line.display_type: continue if float_compare(order_line.product_uom_qty, pre_order_line_qty.get(order_line, 0.0), precision_rounding=order_line.product_uom.rounding or rounding) < 0: to_log[order_line] = (order_line.product_uom_qty, pre_order_line_qty.get(order_line, 0.0)) if to_log: documents = self.env['stock.picking'].sudo()._log_activity_get_documents(to_log, 'move_ids', 'UP') documents = {k: v for k, v in documents.items() if k[0].state != 'cancel'} order._log_decrease_ordered_quantity(documents) return res def _compute_json_popover(self): for order in self: late_stock_picking = order.picking_ids.filtered(lambda p: p.delay_alert_date) order.json_popover = json.dumps({ 'popoverTemplate': 'sale_stock.DelayAlertWidget', 'late_elements': [{ 'id': late_move.id, 'name': late_move.display_name, 'model': 'stock.picking', } for late_move in late_stock_picking ] }) order.show_json_popover = bool(late_stock_picking) def _action_confirm(self): self.order_line._action_launch_stock_rule() return super(SaleOrder, self)._action_confirm() @api.depends('picking_ids') def _compute_picking_ids(self): for order in self: order.delivery_count = len(order.picking_ids) @api.depends('user_id', 'company_id') def _compute_warehouse_id(self): for order in self: default_warehouse_id = self.env['ir.default'].with_company( order.company_id.id)._get_model_defaults('sale.order').get('warehouse_id') if order.state in ['draft', 'sent'] or not order.ids: # Should expect empty if default_warehouse_id is not None: order.warehouse_id = default_warehouse_id else: order.warehouse_id = order.user_id.with_company(order.company_id.id)._get_default_warehouse_id() @api.onchange('partner_shipping_id') def _onchange_partner_shipping_id(self): res = {} pickings = self.picking_ids.filtered( lambda p: p.state not in ['done', 'cancel'] and p.partner_id != self.partner_shipping_id ) if pickings: res['warning'] = { 'title': _('Warning!'), 'message': _( 'Do not forget to change the partner on the following delivery orders: %s', ','.join(pickings.mapped('name'))) } return res def action_view_delivery(self): return self._get_action_view_picking(self.picking_ids) def _action_cancel(self): documents = None for sale_order in self: if sale_order.state == 'sale' and sale_order.order_line: sale_order_lines_quantities = {order_line: (order_line.product_uom_qty, 0) for order_line in sale_order.order_line} documents = self.env['stock.picking'].with_context(include_draft_documents=True)._log_activity_get_documents(sale_order_lines_quantities, 'move_ids', 'UP') self.picking_ids.filtered(lambda p: p.state != 'done').action_cancel() if documents: filtered_documents = {} for (parent, responsible), rendering_context in documents.items(): if parent._name == 'stock.picking': if parent.state == 'cancel': continue filtered_documents[(parent, responsible)] = rendering_context self._log_decrease_ordered_quantity(filtered_documents, cancel=True) return super()._action_cancel() def _get_action_view_picking(self, pickings): ''' This function returns an action that display existing delivery orders of given sales order ids. It can either be a in a list or in a form view, if there is only one delivery order to show. ''' action = self.env["ir.actions.actions"]._for_xml_id("stock.action_picking_tree_all") if len(pickings) > 1: action['domain'] = [('id', 'in', pickings.ids)] elif pickings: form_view = [(self.env.ref('stock.view_picking_form').id, 'form')] if 'views' in action: action['views'] = form_view + [(state,view) for state,view in action['views'] if view != 'form'] else: action['views'] = form_view action['res_id'] = pickings.id # Prepare the context. picking_id = pickings.filtered(lambda l: l.picking_type_id.code == 'outgoing') if picking_id: picking_id = picking_id[0] else: picking_id = pickings[0] action['context'] = dict(self._context, default_partner_id=self.partner_id.id, default_picking_type_id=picking_id.picking_type_id.id, default_origin=self.name, default_group_id=picking_id.group_id.id) return action def _prepare_invoice(self): invoice_vals = super(SaleOrder, self)._prepare_invoice() invoice_vals['invoice_incoterm_id'] = self.incoterm.id return invoice_vals def _log_decrease_ordered_quantity(self, documents, cancel=False): def _render_note_exception_quantity_so(rendering_context): order_exceptions, visited_moves = rendering_context visited_moves = list(visited_moves) visited_moves = self.env[visited_moves[0]._name].concat(*visited_moves) order_line_ids = self.env['sale.order.line'].browse([order_line.id for order in order_exceptions.values() for order_line in order[0]]) sale_order_ids = order_line_ids.mapped('order_id') impacted_pickings = visited_moves.filtered(lambda m: m.state not in ('done', 'cancel')).mapped('picking_id') values = { 'sale_order_ids': sale_order_ids, 'order_exceptions': order_exceptions.values(), 'impacted_pickings': impacted_pickings, 'cancel': cancel } return self.env['ir.qweb']._render('sale_stock.exception_on_so', values) self.env['stock.picking']._log_activity(_render_note_exception_quantity_so, documents)