254 lines
13 KiB
Python
254 lines
13 KiB
Python
|
# -*- 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<br/>
|
||
|
From <strong>"%s"</strong> To <strong>"%s"</strong>,
|
||
|
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)
|