On the commit 69dcdd4, the order='create_date desc' was removed and the way of getting the MO for the notification changed. Now, the context is used, it should be written_after instead of written_date. (The MO created is correct, only the notification is wrong) OPW-2877457 closes odoo/odoo#96856 X-original-commit: b80287b265a4c01babedcbb58459c35d36a48066 Signed-off-by: William Henrotin (whe) <whe@odoo.com>
356 lines
17 KiB
Python
356 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.tools.float_utils import float_round, float_is_zero
|
|
from odoo.exceptions import UserError
|
|
from odoo.osv.expression import AND
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
|
|
class StockPicking(models.Model):
|
|
_inherit = 'stock.picking'
|
|
|
|
purchase_id = fields.Many2one('purchase.order', related='move_ids.purchase_line_id.order_id',
|
|
string="Purchase Orders", readonly=True)
|
|
|
|
|
|
class StockMove(models.Model):
|
|
_inherit = 'stock.move'
|
|
|
|
purchase_line_id = fields.Many2one('purchase.order.line',
|
|
'Purchase Order Line', ondelete='set null', index='btree_not_null', readonly=True)
|
|
created_purchase_line_id = fields.Many2one('purchase.order.line',
|
|
'Created Purchase Order Line', ondelete='set null', readonly=True, copy=False)
|
|
|
|
@api.model
|
|
def _prepare_merge_moves_distinct_fields(self):
|
|
distinct_fields = super(StockMove, self)._prepare_merge_moves_distinct_fields()
|
|
distinct_fields += ['purchase_line_id', 'created_purchase_line_id']
|
|
return distinct_fields
|
|
|
|
@api.model
|
|
def _prepare_merge_negative_moves_excluded_distinct_fields(self):
|
|
return super()._prepare_merge_negative_moves_excluded_distinct_fields() + ['created_purchase_line_id']
|
|
|
|
def _get_price_unit(self):
|
|
""" Returns the unit price for the move"""
|
|
self.ensure_one()
|
|
if self.purchase_line_id and self.product_id.id == self.purchase_line_id.product_id.id:
|
|
price_unit_prec = self.env['decimal.precision'].precision_get('Product Price')
|
|
line = self.purchase_line_id
|
|
order = line.order_id
|
|
price_unit = line.price_unit
|
|
if line.taxes_id:
|
|
qty = line.product_qty or 1
|
|
price_unit = line.taxes_id.with_context(round=False).compute_all(price_unit, currency=line.order_id.currency_id, quantity=qty)['total_void']
|
|
price_unit = float_round(price_unit / qty, precision_digits=price_unit_prec)
|
|
if line.product_uom.id != line.product_id.uom_id.id:
|
|
price_unit *= line.product_uom.factor / line.product_id.uom_id.factor
|
|
if order.currency_id != order.company_id.currency_id:
|
|
# The date must be today, and not the date of the move since the move move is still
|
|
# in assigned state. However, the move date is the scheduled date until move is
|
|
# done, then date of actual move processing. See:
|
|
# https://github.com/odoo/odoo/blob/2f789b6863407e63f90b3a2d4cc3be09815f7002/addons/stock/models/stock_move.py#L36
|
|
price_unit = order.currency_id._convert(
|
|
price_unit, order.company_id.currency_id, order.company_id, fields.Date.context_today(self), round=False)
|
|
return price_unit
|
|
return super(StockMove, self)._get_price_unit()
|
|
|
|
def _generate_valuation_lines_data(self, partner_id, qty, debit_value, credit_value, debit_account_id, credit_account_id, description):
|
|
""" Overridden from stock_account to support amount_currency on valuation lines generated from po
|
|
"""
|
|
self.ensure_one()
|
|
|
|
rslt = super(StockMove, self)._generate_valuation_lines_data(partner_id, qty, debit_value, credit_value, debit_account_id, credit_account_id, description)
|
|
if self.purchase_line_id:
|
|
purchase_currency = self.purchase_line_id.currency_id
|
|
if purchase_currency != self.company_id.currency_id:
|
|
# Do not use price_unit since we want the price tax excluded. And by the way, qty
|
|
# is in the UOM of the product, not the UOM of the PO line.
|
|
purchase_price_unit = (
|
|
self.purchase_line_id.price_subtotal / self.purchase_line_id.product_uom_qty
|
|
if self.purchase_line_id.product_uom_qty
|
|
else self.purchase_line_id.price_unit
|
|
)
|
|
currency_move_valuation = purchase_currency.round(purchase_price_unit * abs(qty))
|
|
rslt['credit_line_vals']['amount_currency'] = rslt['credit_line_vals']['credit'] and -currency_move_valuation or currency_move_valuation
|
|
rslt['credit_line_vals']['currency_id'] = purchase_currency.id
|
|
rslt['debit_line_vals']['amount_currency'] = rslt['debit_line_vals']['credit'] and -currency_move_valuation or currency_move_valuation
|
|
rslt['debit_line_vals']['currency_id'] = purchase_currency.id
|
|
return rslt
|
|
|
|
def _prepare_extra_move_vals(self, qty):
|
|
vals = super(StockMove, self)._prepare_extra_move_vals(qty)
|
|
vals['purchase_line_id'] = self.purchase_line_id.id
|
|
return vals
|
|
|
|
def _prepare_move_split_vals(self, uom_qty):
|
|
vals = super(StockMove, self)._prepare_move_split_vals(uom_qty)
|
|
vals['purchase_line_id'] = self.purchase_line_id.id
|
|
return vals
|
|
|
|
def _prepare_procurement_values(self):
|
|
proc_values = super()._prepare_procurement_values()
|
|
if self.restrict_partner_id:
|
|
proc_values['supplierinfo_name'] = self.restrict_partner_id
|
|
self.restrict_partner_id = False
|
|
return proc_values
|
|
|
|
def _clean_merged(self):
|
|
super(StockMove, self)._clean_merged()
|
|
self.write({'created_purchase_line_id': False})
|
|
|
|
def _get_upstream_documents_and_responsibles(self, visited):
|
|
if self.created_purchase_line_id and self.created_purchase_line_id.state not in ('done', 'cancel') \
|
|
and (self.created_purchase_line_id.state != 'draft' or self._context.get('include_draft_documents')):
|
|
return [(self.created_purchase_line_id.order_id, self.created_purchase_line_id.order_id.user_id, visited)]
|
|
elif self.purchase_line_id and self.purchase_line_id.state not in ('done', 'cancel'):
|
|
return[(self.purchase_line_id.order_id, self.purchase_line_id.order_id.user_id, visited)]
|
|
else:
|
|
return super(StockMove, self)._get_upstream_documents_and_responsibles(visited)
|
|
|
|
def _get_related_invoices(self):
|
|
""" Overridden to return the vendor bills related to this stock move.
|
|
"""
|
|
rslt = super(StockMove, self)._get_related_invoices()
|
|
rslt += self.mapped('picking_id.purchase_id.invoice_ids').filtered(lambda x: x.state == 'posted')
|
|
return rslt
|
|
|
|
def _get_source_document(self):
|
|
res = super()._get_source_document()
|
|
return self.purchase_line_id.order_id or res
|
|
|
|
def _get_valuation_price_and_qty(self, related_aml, to_curr):
|
|
valuation_price_unit_total = 0
|
|
valuation_total_qty = 0
|
|
for val_stock_move in self:
|
|
# In case val_stock_move is a return move, its valuation entries have been made with the
|
|
# currency rate corresponding to the original stock move
|
|
valuation_date = val_stock_move.origin_returned_move_id.date or val_stock_move.date
|
|
svl = val_stock_move.with_context(active_test=False).mapped('stock_valuation_layer_ids').filtered(
|
|
lambda l: l.quantity)
|
|
layers_qty = sum(svl.mapped('quantity'))
|
|
layers_values = sum(svl.mapped('value'))
|
|
valuation_price_unit_total += related_aml.company_currency_id._convert(
|
|
layers_values, to_curr, related_aml.company_id, valuation_date, round=False,
|
|
)
|
|
valuation_total_qty += layers_qty
|
|
if float_is_zero(valuation_total_qty, precision_rounding=related_aml.product_uom_id.rounding or related_aml.product_id.uom_id.rounding):
|
|
raise UserError(
|
|
_('Odoo is not able to generate the anglo saxon entries. The total valuation of %s is zero.') % related_aml.product_id.display_name)
|
|
return valuation_price_unit_total, valuation_total_qty
|
|
|
|
|
|
class StockWarehouse(models.Model):
|
|
_inherit = 'stock.warehouse'
|
|
|
|
buy_to_resupply = fields.Boolean('Buy to Resupply', default=True,
|
|
help="When products are bought, they can be delivered to this warehouse")
|
|
buy_pull_id = fields.Many2one('stock.rule', 'Buy rule')
|
|
|
|
def _get_global_route_rules_values(self):
|
|
rules = super(StockWarehouse, self)._get_global_route_rules_values()
|
|
location_id = self.in_type_id.default_location_dest_id
|
|
rules.update({
|
|
'buy_pull_id': {
|
|
'depends': ['reception_steps', 'buy_to_resupply'],
|
|
'create_values': {
|
|
'action': 'buy',
|
|
'picking_type_id': self.in_type_id.id,
|
|
'group_propagation_option': 'none',
|
|
'company_id': self.company_id.id,
|
|
'route_id': self._find_global_route('purchase_stock.route_warehouse0_buy', _('Buy')).id,
|
|
'propagate_cancel': self.reception_steps != 'one_step',
|
|
},
|
|
'update_values': {
|
|
'active': self.buy_to_resupply,
|
|
'name': self._format_rulename(location_id, False, 'Buy'),
|
|
'location_dest_id': location_id.id,
|
|
'propagate_cancel': self.reception_steps != 'one_step',
|
|
}
|
|
}
|
|
})
|
|
return rules
|
|
|
|
def _get_all_routes(self):
|
|
routes = super(StockWarehouse, self)._get_all_routes()
|
|
routes |= self.filtered(lambda self: self.buy_to_resupply and self.buy_pull_id and self.buy_pull_id.route_id).mapped('buy_pull_id').mapped('route_id')
|
|
return routes
|
|
|
|
def get_rules_dict(self):
|
|
result = super(StockWarehouse, self).get_rules_dict()
|
|
for warehouse in self:
|
|
result[warehouse.id].update(warehouse._get_receive_rules_dict())
|
|
return result
|
|
|
|
def _get_routes_values(self):
|
|
routes = super(StockWarehouse, self)._get_routes_values()
|
|
routes.update(self._get_receive_routes_values('buy_to_resupply'))
|
|
return routes
|
|
|
|
def _update_name_and_code(self, name=False, code=False):
|
|
res = super(StockWarehouse, self)._update_name_and_code(name, code)
|
|
warehouse = self[0]
|
|
#change the buy stock rule name
|
|
if warehouse.buy_pull_id and name:
|
|
warehouse.buy_pull_id.write({'name': warehouse.buy_pull_id.name.replace(warehouse.name, name, 1)})
|
|
return res
|
|
|
|
|
|
class ReturnPicking(models.TransientModel):
|
|
_inherit = "stock.return.picking"
|
|
|
|
def _prepare_move_default_values(self, return_line, new_picking):
|
|
vals = super(ReturnPicking, self)._prepare_move_default_values(return_line, new_picking)
|
|
vals['purchase_line_id'] = return_line.move_id.purchase_line_id.id
|
|
return vals
|
|
|
|
|
|
class Orderpoint(models.Model):
|
|
_inherit = "stock.warehouse.orderpoint"
|
|
|
|
show_supplier = fields.Boolean('Show supplier column', compute='_compute_show_suppplier')
|
|
supplier_id = fields.Many2one(
|
|
'product.supplierinfo', string='Product Supplier', check_company=True,
|
|
domain="['|', ('product_id', '=', product_id), '&', ('product_id', '=', False), ('product_tmpl_id', '=', product_tmpl_id)]")
|
|
vendor_id = fields.Many2one(related='supplier_id.partner_id', string="Vendor", store=True)
|
|
purchase_visibility_days = fields.Float(default=0.0, help="Visibility Days applied on the purchase routes.")
|
|
|
|
@api.depends('product_id.purchase_order_line_ids', 'product_id.purchase_order_line_ids.state')
|
|
def _compute_qty(self):
|
|
""" Extend to add more depends values """
|
|
return super()._compute_qty()
|
|
|
|
@api.depends('supplier_id')
|
|
def _compute_lead_days(self):
|
|
return super()._compute_lead_days()
|
|
|
|
def _compute_visibility_days(self):
|
|
res = super()._compute_visibility_days()
|
|
for orderpoint in self:
|
|
if 'buy' in orderpoint.rule_ids.mapped('action'):
|
|
orderpoint.visibility_days = orderpoint.purchase_visibility_days
|
|
return res
|
|
|
|
def _set_visibility_days(self):
|
|
res = super()._set_visibility_days()
|
|
for orderpoint in self:
|
|
if 'buy' in orderpoint.rule_ids.mapped('action'):
|
|
orderpoint.purchase_visibility_days = orderpoint.visibility_days
|
|
return res
|
|
|
|
def _compute_days_to_order(self):
|
|
res = super()._compute_days_to_order()
|
|
for orderpoint in self:
|
|
if 'buy' in orderpoint.rule_ids.mapped('action'):
|
|
orderpoint.days_to_order = orderpoint.company_id.days_to_purchase
|
|
return res
|
|
|
|
@api.depends('route_id')
|
|
def _compute_show_suppplier(self):
|
|
buy_route = []
|
|
for res in self.env['stock.rule'].search_read([('action', '=', 'buy')], ['route_id']):
|
|
buy_route.append(res['route_id'][0])
|
|
for orderpoint in self:
|
|
orderpoint.show_supplier = orderpoint.route_id.id in buy_route
|
|
|
|
def action_view_purchase(self):
|
|
""" This function returns an action that display existing
|
|
purchase orders of given orderpoint.
|
|
"""
|
|
result = self.env['ir.actions.act_window']._for_xml_id('purchase.purchase_rfq')
|
|
|
|
# Remvove the context since the action basically display RFQ and not PO.
|
|
result['context'] = {}
|
|
order_line_ids = self.env['purchase.order.line'].search([('orderpoint_id', '=', self.id)])
|
|
purchase_ids = order_line_ids.mapped('order_id')
|
|
|
|
result['domain'] = "[('id','in',%s)]" % (purchase_ids.ids)
|
|
|
|
return result
|
|
|
|
def _get_lead_days_values(self):
|
|
values = super()._get_lead_days_values()
|
|
if self.supplier_id:
|
|
values['supplierinfo'] = self.supplier_id
|
|
return values
|
|
|
|
def _get_replenishment_order_notification(self):
|
|
self.ensure_one()
|
|
domain = [('orderpoint_id', 'in', self.ids)]
|
|
if self.env.context.get('written_after'):
|
|
domain = AND([domain, [('write_date', '>', self.env.context.get('written_after'))]])
|
|
order = self.env['purchase.order.line'].search(domain, limit=1).order_id
|
|
if order:
|
|
action = self.env.ref('purchase.action_rfq_form')
|
|
return {
|
|
'type': 'ir.actions.client',
|
|
'tag': 'display_notification',
|
|
'params': {
|
|
'title': _('The following replenishment order has been generated'),
|
|
'message': '%s',
|
|
'links': [{
|
|
'label': order.display_name,
|
|
'url': f'#action={action.id}&id={order.id}&model=purchase.order',
|
|
}],
|
|
'sticky': False,
|
|
}
|
|
}
|
|
return super()._get_replenishment_order_notification()
|
|
|
|
def _prepare_procurement_values(self, date=False, group=False):
|
|
values = super()._prepare_procurement_values(date=date, group=group)
|
|
values['supplierinfo_id'] = self.supplier_id
|
|
return values
|
|
|
|
def _quantity_in_progress(self):
|
|
res = super()._quantity_in_progress()
|
|
qty_by_product_location, dummy = self.product_id._get_quantity_in_progress(self.location_id.ids)
|
|
for orderpoint in self:
|
|
product_qty = qty_by_product_location.get((orderpoint.product_id.id, orderpoint.location_id.id), 0.0)
|
|
product_uom_qty = orderpoint.product_id.uom_id._compute_quantity(product_qty, orderpoint.product_uom, round=False)
|
|
res[orderpoint.id] += product_uom_qty
|
|
return res
|
|
|
|
def _set_default_route_id(self):
|
|
route_id = self.env['stock.rule'].search([
|
|
('action', '=', 'buy')
|
|
]).route_id
|
|
orderpoint_wh_supplier = self.filtered(lambda o: o.product_id.seller_ids)
|
|
if route_id and orderpoint_wh_supplier:
|
|
orderpoint_wh_supplier.route_id = route_id[0].id
|
|
return super()._set_default_route_id()
|
|
|
|
def _get_orderpoint_procurement_date(self):
|
|
date = super()._get_orderpoint_procurement_date()
|
|
if any(rule.action == 'buy' for rule in self.rule_ids):
|
|
date -= relativedelta(days=self.company_id.po_lead)
|
|
return date
|
|
|
|
|
|
class StockLot(models.Model):
|
|
_inherit = 'stock.lot'
|
|
|
|
purchase_order_ids = fields.Many2many('purchase.order', string="Purchase Orders", compute='_compute_purchase_order_ids', readonly=True, store=False)
|
|
purchase_order_count = fields.Integer('Purchase order count', compute='_compute_purchase_order_ids')
|
|
|
|
@api.depends('name')
|
|
def _compute_purchase_order_ids(self):
|
|
for lot in self:
|
|
stock_moves = self.env['stock.move.line'].search([
|
|
('lot_id', '=', lot.id),
|
|
('state', '=', 'done')
|
|
]).mapped('move_id')
|
|
stock_moves = stock_moves.search([('id', 'in', stock_moves.ids)]).filtered(
|
|
lambda move: move.picking_id.location_id.usage == 'supplier' and move.state == 'done')
|
|
lot.purchase_order_ids = stock_moves.mapped('purchase_line_id.order_id')
|
|
lot.purchase_order_count = len(lot.purchase_order_ids)
|
|
|
|
def action_view_po(self):
|
|
self.ensure_one()
|
|
action = self.env["ir.actions.actions"]._for_xml_id("purchase.purchase_form_action")
|
|
action['domain'] = [('id', 'in', self.mapped('purchase_order_ids.id'))]
|
|
action['context'] = dict(self._context, create=False)
|
|
return action
|