19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:12 +01:00
parent 79f83631d5
commit 73afc09215
6267 changed files with 1534193 additions and 1130106 deletions

View file

@ -1,10 +1,11 @@
# -*- 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.fields import Command
from odoo.exceptions import UserError
from odoo.tools import float_compare
_logger = logging.getLogger(__name__)
@ -20,14 +21,12 @@ class SaleOrder(models.Model):
picking_policy = fields.Selection([
('direct', 'As soon as possible'),
('one', 'When all products are ready')],
string='Shipping Policy', required=True, readonly=True, default='direct',
states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
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,
'stock.warehouse', string='Warehouse',
compute='_compute_warehouse_id', store=True, readonly=False, precompute=True,
states={'sale': [('readonly', True)], 'done': [('readonly', True)], 'cancel': [('readonly', False)]},
check_company=True)
picking_ids = fields.One2many('stock.picking', 'sale_id', string='Transfers')
delivery_count = fields.Integer(string='Delivery Orders', compute='_compute_picking_ids')
@ -36,8 +35,19 @@ class SaleOrder(models.Model):
('started', 'Started'),
('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)
], string='Delivery Status', compute='_compute_delivery_status', store=True,
help="Blue: Not Delivered/Started\n\
Orange: Partially Delivered\n\
Green: Fully Delivered")
late_availability = fields.Boolean(
string="Late Availability",
compute='_compute_late_availability',
search='_search_late_availability',
help="True if any related picking has late availability"
)
stock_reference_ids = fields.Many2many(
'stock.reference', 'stock_reference_sale_rel',
'sale_id', 'reference_id', string='References', 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 "
@ -63,12 +73,12 @@ class SaleOrder(models.Model):
UPDATE sale_order so
SET warehouse_id = COALESCE(wh.id, %s)
FROM stock_warehouse wh
WHERE so.company_id = wh.company_id and so.warehouse_id IS NULL AND wh.active
WHERE so.company_id = wh.company_id and so.warehouse_id IS NULL and wh.active
"""
params = [default_warehouse.id]
_logger.debug("Initializing column '%s' in table '%s'", column_name, self._table)
self._cr.execute(query, params)
self.env.cr.execute(query, params)
@api.depends('picking_ids.date_done')
def _compute_effective_date(self):
@ -96,24 +106,68 @@ class SaleOrder(models.Model):
def _compute_expected_date(self):
super(SaleOrder, self)._compute_expected_date()
@api.depends('picking_ids.products_availability_state')
def _compute_late_availability(self):
for order in self:
order.late_availability = any(
picking.products_availability_state == 'late' for picking in order.picking_ids
)
def _search_late_availability(self, operator, value):
if operator not in ('=', '!=') or not isinstance(value, bool):
return NotImplemented
sub_query = self.env['stock.picking']._search([
('sale_id', '!=', False), ('products_availability_state', operator, 'late')
])
return [('picking_ids', 'in', sub_query)]
def _select_expected_date(self, expected_dates):
if self.picking_policy == "direct":
return super()._select_expected_date(expected_dates)
return max(expected_dates)
def write(self, values):
@api.constrains('warehouse_id', 'state', 'order_line')
def _check_warehouse(self):
""" Ensure that the warehouse is set in case of storable products """
orders_without_wh = self.filtered(lambda order: order.state not in ('draft', 'cancel') and not order.warehouse_id)
company_ids_with_wh = {
company_id.id for [company_id] in self.env['stock.warehouse']._read_group(
domain=[('company_id', 'in', orders_without_wh.company_id.ids)],
groupby=['company_id'],
)
}
other_company = set()
for order_line in orders_without_wh.order_line:
if order_line.product_id.type != 'consu':
continue
if order_line.route_ids.company_id and order_line.route_ids.company_id != order_line.company_id:
other_company.add(order_line.route_ids.company_id.id)
continue
if order_line.order_id.company_id.id in company_ids_with_wh:
raise UserError(_('You must set a warehouse on your sale order to proceed.'))
self.env['stock.warehouse'].with_company(order_line.order_id.company_id)._warehouse_redirect_warning()
other_company_warehouses = self.env['stock.warehouse'].search([('company_id', 'in', list(other_company))])
if any(c not in other_company_warehouses.company_id.ids for c in other_company):
raise UserError(_("You must have a warehouse for line using a delivery in different company."))
def write(self, vals):
values = vals
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'):
if values.get('partner_shipping_id') and self.env.context.get('update_delivery_shipping_partner'):
for order in self:
order.picking_ids.partner_id = values.get('partner_shipping_id')
elif 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'))
addresses = (record.partner_shipping_id.display_name, new_partner.display_name)
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.""") % addresses
From <strong>"%(old_address)s"</strong> to <strong>"%(new_address)s"</strong>,
You should probably update the partner on this document.""",
old_address=record.partner_shipping_id.display_name, new_address=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:
@ -121,17 +175,20 @@ class SaleOrder(models.Model):
# 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
moves = order.order_line.move_ids.filtered(
lambda m: m.state not in ('done', 'cancel') and m.location_dest_id.usage == 'customer'
)
moves.date_deadline = deadline_datetime or order.expected_date
res = super(SaleOrder, self).write(values)
res = super().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 = {}
order.order_line.fetch(['product_uom_id', 'product_uom_qty', 'display_type', 'is_downpayment'])
for order_line in order.order_line:
if order_line.display_type:
if order_line.display_type or order_line.is_downpayment:
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:
if float_compare(order_line.product_uom_qty, pre_order_line_qty.get(order_line, 0.0), precision_rounding=order_line.product_uom_id.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')
@ -166,7 +223,7 @@ class SaleOrder(models.Model):
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')
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:
@ -184,8 +241,8 @@ class SaleOrder(models.Model):
res['warning'] = {
'title': _('Warning!'),
'message': _(
'Do not forget to change the partner on the following delivery orders: %s'
) % (','.join(pickings.mapped('name')))
'Do not forget to change the partner on the following delivery orders: %s',
','.join(pickings.mapped('name')))
}
return res
@ -232,12 +289,16 @@ class SaleOrder(models.Model):
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)
action['context'] = dict(
default_partner_id=self.partner_id.id,
default_picking_type_id=picking_id.picking_type_id.id,
)
return action
def _prepare_invoice(self):
invoice_vals = super(SaleOrder, self)._prepare_invoice()
invoice_vals['invoice_incoterm_id'] = self.incoterm.id
invoice_vals['delivery_date'] = self.effective_date
return invoice_vals
def _log_decrease_ordered_quantity(self, documents, cancel=False):
@ -258,3 +319,18 @@ class SaleOrder(models.Model):
return self.env['ir.qweb']._render('sale_stock.exception_on_so', values)
self.env['stock.picking']._log_activity(_render_note_exception_quantity_so, documents)
def _is_display_stock_in_catalog(self):
return True
# TODO: rename the parameter from reference to references in master for improved readability
def _add_reference(self, reference):
""" link the given references to the list of references. """
self.ensure_one()
self.stock_reference_ids = [Command.link(stock_reference.id) for stock_reference in reference]
# TODO: rename the parameter from reference to references in master for improved readability
def _remove_reference(self, reference):
""" remove the given references from the list of references. """
self.ensure_one()
self.stock_reference_ids = [Command.unlink(stock_reference.id) for stock_reference in reference]