Behavior prior to this commit:
When a PO is generated (via reordering rule, mto rule, or manual replenish),
the description on the PO line includes the internal notes.
Behavior after this commit:
The description on the PO line does not include the internal notes.
Note:
Per c324526ad0, the description on the picking also does not include
the internal notes (anymore), and the test has been updated to reflect
that.
opw-24222438
closes odoo/odoo#64883
X-original-commit: 5db67fb2931517ae94fe0396ef80e9ad997e59cd
Signed-off-by: Nicolas Galler <ngaller@users.noreply.github.com>
543 lines
28 KiB
Python
543 lines
28 KiB
Python
# -*- coding: utf-8 -*-
|
||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
from odoo import api, fields, models, SUPERUSER_ID, _
|
||
from odoo.tools.float_utils import float_compare
|
||
from datetime import datetime
|
||
from dateutil.relativedelta import relativedelta
|
||
from odoo.exceptions import UserError
|
||
|
||
from odoo.addons.purchase.models.purchase import PurchaseOrder as Purchase
|
||
|
||
|
||
class PurchaseOrder(models.Model):
|
||
_inherit = 'purchase.order'
|
||
|
||
@api.model
|
||
def _default_picking_type(self):
|
||
return self._get_picking_type(self.env.context.get('company_id') or self.env.company.id)
|
||
|
||
incoterm_id = fields.Many2one('account.incoterms', 'Incoterm', states={'done': [('readonly', True)]}, help="International Commercial Terms are a series of predefined commercial terms used in international transactions.")
|
||
|
||
picking_count = fields.Integer(compute='_compute_picking', string='Picking count', default=0, store=True)
|
||
picking_ids = fields.Many2many('stock.picking', compute='_compute_picking', string='Receptions', copy=False, store=True)
|
||
|
||
picking_type_id = fields.Many2one('stock.picking.type', 'Deliver To', states=Purchase.READONLY_STATES, required=True, default=_default_picking_type, domain="['|', ('warehouse_id', '=', False), ('warehouse_id.company_id', '=', company_id)]",
|
||
help="This will determine operation type of incoming shipment")
|
||
default_location_dest_id_usage = fields.Selection(related='picking_type_id.default_location_dest_id.usage', string='Destination Location Type',
|
||
help="Technical field used to display the Drop Ship Address", readonly=True)
|
||
group_id = fields.Many2one('procurement.group', string="Procurement Group", copy=False)
|
||
is_shipped = fields.Boolean(compute="_compute_is_shipped")
|
||
effective_date = fields.Datetime("Effective Date", compute='_compute_effective_date', store=True, copy=False,
|
||
help="Completion date of the first receipt order.")
|
||
on_time_rate = fields.Float(related='partner_id.on_time_rate')
|
||
|
||
@api.depends('order_line.move_ids.picking_id')
|
||
def _compute_picking(self):
|
||
for order in self:
|
||
pickings = order.order_line.move_ids.picking_id
|
||
order.picking_ids = pickings
|
||
order.picking_count = len(pickings)
|
||
|
||
@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 == 'internal' and x.date_done)
|
||
order.effective_date = min(pickings.mapped('date_done'), default=False)
|
||
|
||
@api.depends('picking_ids', 'picking_ids.state')
|
||
def _compute_is_shipped(self):
|
||
for order in self:
|
||
if order.picking_ids and all(x.state in ['done', 'cancel'] for x in order.picking_ids):
|
||
order.is_shipped = True
|
||
else:
|
||
order.is_shipped = False
|
||
|
||
@api.onchange('picking_type_id')
|
||
def _onchange_picking_type_id(self):
|
||
if self.picking_type_id.default_location_dest_id.usage != 'customer':
|
||
self.dest_address_id = False
|
||
|
||
@api.onchange('company_id')
|
||
def _onchange_company_id(self):
|
||
p_type = self.picking_type_id
|
||
if not(p_type and p_type.code == 'incoming' and (p_type.warehouse_id.company_id == self.company_id or not p_type.warehouse_id)):
|
||
self.picking_type_id = self._get_picking_type(self.company_id.id)
|
||
|
||
# --------------------------------------------------
|
||
# CRUD
|
||
# --------------------------------------------------
|
||
|
||
def write(self, vals):
|
||
if vals.get('order_line') and self.state == 'purchase':
|
||
for order in self:
|
||
pre_order_line_qty = {order_line: order_line.product_qty for order_line in order.mapped('order_line')}
|
||
res = super(PurchaseOrder, self).write(vals)
|
||
if vals.get('order_line') and self.state == 'purchase':
|
||
for order in self:
|
||
to_log = {}
|
||
for order_line in order.order_line:
|
||
if pre_order_line_qty.get(order_line, False) and float_compare(pre_order_line_qty[order_line], order_line.product_qty, precision_rounding=order_line.product_uom.rounding) > 0:
|
||
to_log[order_line] = (order_line.product_qty, pre_order_line_qty[order_line])
|
||
if to_log:
|
||
order._log_decrease_ordered_quantity(to_log)
|
||
return res
|
||
|
||
# --------------------------------------------------
|
||
# Actions
|
||
# --------------------------------------------------
|
||
|
||
def button_approve(self, force=False):
|
||
result = super(PurchaseOrder, self).button_approve(force=force)
|
||
self._create_picking()
|
||
return result
|
||
|
||
def button_cancel(self):
|
||
for order in self:
|
||
for move in order.order_line.mapped('move_ids'):
|
||
if move.state == 'done':
|
||
raise UserError(_('Unable to cancel purchase order %s as some receptions have already been done.') % (order.name))
|
||
# If the product is MTO, change the procure_method of the closest move to purchase to MTS.
|
||
# The purpose is to link the po that the user will manually generate to the existing moves's chain.
|
||
if order.state in ('draft', 'sent', 'to approve', 'purchase'):
|
||
for order_line in order.order_line:
|
||
order_line.move_ids._action_cancel()
|
||
if order_line.move_dest_ids:
|
||
move_dest_ids = order_line.move_dest_ids
|
||
if order_line.propagate_cancel:
|
||
move_dest_ids._action_cancel()
|
||
else:
|
||
move_dest_ids.write({'procure_method': 'make_to_stock'})
|
||
move_dest_ids._recompute_state()
|
||
|
||
for pick in order.picking_ids.filtered(lambda r: r.state != 'cancel'):
|
||
pick.action_cancel()
|
||
|
||
order.order_line.write({'move_dest_ids':[(5,0,0)]})
|
||
|
||
return super(PurchaseOrder, self).button_cancel()
|
||
|
||
def action_view_picking(self):
|
||
""" This function returns an action that display existing picking orders of given purchase order ids. When only one found, show the picking immediately.
|
||
"""
|
||
result = self.env["ir.actions.actions"]._for_xml_id('stock.action_picking_tree_all')
|
||
# override the context to get rid of the default filtering on operation type
|
||
result['context'] = {'default_partner_id': self.partner_id.id, 'default_origin': self.name, 'default_picking_type_id': self.picking_type_id.id}
|
||
pick_ids = self.mapped('picking_ids')
|
||
# choose the view_mode accordingly
|
||
if not pick_ids or len(pick_ids) > 1:
|
||
result['domain'] = "[('id','in',%s)]" % (pick_ids.ids)
|
||
elif len(pick_ids) == 1:
|
||
res = self.env.ref('stock.view_picking_form', False)
|
||
form_view = [(res and res.id or False, 'form')]
|
||
if 'views' in result:
|
||
result['views'] = form_view + [(state,view) for state,view in result['views'] if view != 'form']
|
||
else:
|
||
result['views'] = form_view
|
||
result['res_id'] = pick_ids.id
|
||
return result
|
||
|
||
def _prepare_invoice(self):
|
||
invoice_vals = super()._prepare_invoice()
|
||
invoice_vals['invoice_incoterm_id'] = self.incoterm_id.id
|
||
return invoice_vals
|
||
|
||
# --------------------------------------------------
|
||
# Business methods
|
||
# --------------------------------------------------
|
||
|
||
def _log_decrease_ordered_quantity(self, purchase_order_lines_quantities):
|
||
|
||
def _keys_in_sorted(move):
|
||
""" sort by picking and the responsible for the product the
|
||
move.
|
||
"""
|
||
return (move.picking_id.id, move.product_id.responsible_id.id)
|
||
|
||
def _keys_in_groupby(move):
|
||
""" group by picking and the responsible for the product the
|
||
move.
|
||
"""
|
||
return (move.picking_id, move.product_id.responsible_id)
|
||
|
||
def _render_note_exception_quantity_po(order_exceptions):
|
||
order_line_ids = self.env['purchase.order.line'].browse([order_line.id for order in order_exceptions.values() for order_line in order[0]])
|
||
purchase_order_ids = order_line_ids.mapped('order_id')
|
||
move_ids = self.env['stock.move'].concat(*rendering_context.keys())
|
||
impacted_pickings = move_ids.mapped('picking_id')._get_impacted_pickings(move_ids) - move_ids.mapped('picking_id')
|
||
values = {
|
||
'purchase_order_ids': purchase_order_ids,
|
||
'order_exceptions': order_exceptions.values(),
|
||
'impacted_pickings': impacted_pickings,
|
||
}
|
||
return self.env.ref('purchase_stock.exception_on_po')._render(values=values)
|
||
|
||
documents = self.env['stock.picking']._log_activity_get_documents(purchase_order_lines_quantities, 'move_ids', 'DOWN', _keys_in_sorted, _keys_in_groupby)
|
||
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.env['stock.picking']._log_activity(_render_note_exception_quantity_po, filtered_documents)
|
||
|
||
def _get_destination_location(self):
|
||
self.ensure_one()
|
||
if self.dest_address_id:
|
||
return self.dest_address_id.property_stock_customer.id
|
||
return self.picking_type_id.default_location_dest_id.id
|
||
|
||
@api.model
|
||
def _get_picking_type(self, company_id):
|
||
picking_type = self.env['stock.picking.type'].search([('code', '=', 'incoming'), ('warehouse_id.company_id', '=', company_id)])
|
||
if not picking_type:
|
||
picking_type = self.env['stock.picking.type'].search([('code', '=', 'incoming'), ('warehouse_id', '=', False)])
|
||
return picking_type[:1]
|
||
|
||
def _prepare_picking(self):
|
||
if not self.group_id:
|
||
self.group_id = self.group_id.create({
|
||
'name': self.name,
|
||
'partner_id': self.partner_id.id
|
||
})
|
||
if not self.partner_id.property_stock_supplier.id:
|
||
raise UserError(_("You must set a Vendor Location for this partner %s", self.partner_id.name))
|
||
return {
|
||
'picking_type_id': self.picking_type_id.id,
|
||
'partner_id': self.partner_id.id,
|
||
'user_id': False,
|
||
'date': self.date_order,
|
||
'origin': self.name,
|
||
'location_dest_id': self._get_destination_location(),
|
||
'location_id': self.partner_id.property_stock_supplier.id,
|
||
'company_id': self.company_id.id,
|
||
}
|
||
|
||
def _create_picking(self):
|
||
StockPicking = self.env['stock.picking']
|
||
for order in self:
|
||
if any(product.type in ['product', 'consu'] for product in order.order_line.product_id):
|
||
order = order.with_company(order.company_id)
|
||
pickings = order.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
|
||
if not pickings:
|
||
res = order._prepare_picking()
|
||
picking = StockPicking.with_user(SUPERUSER_ID).create(res)
|
||
else:
|
||
picking = pickings[0]
|
||
moves = order.order_line._create_stock_moves(picking)
|
||
moves = moves.filtered(lambda x: x.state not in ('done', 'cancel'))._action_confirm()
|
||
seq = 0
|
||
for move in sorted(moves, key=lambda move: move.date):
|
||
seq += 5
|
||
move.sequence = seq
|
||
moves._action_assign()
|
||
picking.message_post_with_view('mail.message_origin_link',
|
||
values={'self': picking, 'origin': order},
|
||
subtype_id=self.env.ref('mail.mt_note').id)
|
||
return True
|
||
|
||
def _add_picking_info(self, activity):
|
||
"""Helper method to add picking info to the Date Updated activity when
|
||
vender updates date_planned of the po lines.
|
||
"""
|
||
validated_picking = self.picking_ids.filtered(lambda p: p.state == 'done')
|
||
if validated_picking:
|
||
activity.note += _("<p>Those dates couldn’t be modified accordingly on the receipt %s which had already been validated.</p>") % validated_picking[0].name
|
||
elif not self.picking_ids:
|
||
activity.note += _("<p>Corresponding receipt not found.</p>")
|
||
else:
|
||
activity.note += _("<p>Those dates have been updated accordingly on the receipt %s.</p>") % self.picking_ids[0].name
|
||
|
||
def _create_update_date_activity(self, updated_dates):
|
||
activity = super()._create_update_date_activity(updated_dates)
|
||
self._add_picking_info(activity)
|
||
|
||
def _update_update_date_activity(self, updated_dates, activity):
|
||
# remove old picking info to update it
|
||
note_lines = activity.note.split('<p>')
|
||
note_lines.pop()
|
||
activity.note = '<p>'.join(note_lines)
|
||
super()._update_update_date_activity(updated_dates, activity)
|
||
self._add_picking_info(activity)
|
||
|
||
@api.model
|
||
def _get_orders_to_remind(self):
|
||
"""When auto sending reminder mails, don't send for purchase order with
|
||
validated receipts."""
|
||
return super()._get_orders_to_remind().filtered(lambda p: not p.effective_date)
|
||
|
||
|
||
class PurchaseOrderLine(models.Model):
|
||
_inherit = 'purchase.order.line'
|
||
|
||
qty_received_method = fields.Selection(selection_add=[('stock_moves', 'Stock Moves')])
|
||
|
||
move_ids = fields.One2many('stock.move', 'purchase_line_id', string='Reservation', readonly=True, copy=False)
|
||
orderpoint_id = fields.Many2one('stock.warehouse.orderpoint', 'Orderpoint')
|
||
move_dest_ids = fields.One2many('stock.move', 'created_purchase_line_id', 'Downstream Moves')
|
||
product_description_variants = fields.Char('Custom Description')
|
||
propagate_cancel = fields.Boolean('Propagate cancellation', default=True)
|
||
|
||
def _compute_qty_received_method(self):
|
||
super(PurchaseOrderLine, self)._compute_qty_received_method()
|
||
for line in self.filtered(lambda l: not l.display_type):
|
||
if line.product_id.type in ['consu', 'product']:
|
||
line.qty_received_method = 'stock_moves'
|
||
|
||
@api.depends('move_ids.state', 'move_ids.product_uom_qty', 'move_ids.product_uom')
|
||
def _compute_qty_received(self):
|
||
super(PurchaseOrderLine, self)._compute_qty_received()
|
||
for line in self:
|
||
if line.qty_received_method == 'stock_moves':
|
||
total = 0.0
|
||
# In case of a BOM in kit, the products delivered do not correspond to the products in
|
||
# the PO. Therefore, we can skip them since they will be handled later on.
|
||
for move in line.move_ids.filtered(lambda m: m.product_id == line.product_id):
|
||
if move.state == 'done':
|
||
if move.location_dest_id.usage == "supplier":
|
||
if move.to_refund:
|
||
total -= move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom)
|
||
elif move.origin_returned_move_id and move.origin_returned_move_id._is_dropshipped() and not move._is_dropshipped_returned():
|
||
# Edge case: the dropship is returned to the stock, no to the supplier.
|
||
# In this case, the received quantity on the PO is set although we didn't
|
||
# receive the product physically in our stock. To avoid counting the
|
||
# quantity twice, we do nothing.
|
||
pass
|
||
elif (
|
||
move.location_dest_id.usage == "internal"
|
||
and move.to_refund
|
||
and move.location_dest_id
|
||
not in self.env["stock.location"].search(
|
||
[("id", "child_of", move.warehouse_id.view_location_id.id)]
|
||
)
|
||
):
|
||
total -= move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom)
|
||
else:
|
||
total += move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom)
|
||
line._track_qty_received(total)
|
||
line.qty_received = total
|
||
|
||
@api.model_create_multi
|
||
def create(self, vals_list):
|
||
lines = super(PurchaseOrderLine, self).create(vals_list)
|
||
lines.filtered(lambda l: l.order_id.state == 'purchase')._create_or_update_picking()
|
||
return lines
|
||
|
||
def write(self, values):
|
||
for line in self.filtered(lambda l: not l.display_type):
|
||
# PO date_planned overrides any PO line date_planned values
|
||
if values.get('date_planned'):
|
||
new_date = fields.Datetime.to_datetime(values['date_planned'])
|
||
self._update_move_date_deadline(new_date)
|
||
result = super(PurchaseOrderLine, self).write(values)
|
||
if 'product_qty' in values:
|
||
self.filtered(lambda l: l.order_id.state == 'purchase')._create_or_update_picking()
|
||
return result
|
||
|
||
# --------------------------------------------------
|
||
# Business methods
|
||
# --------------------------------------------------
|
||
|
||
def _update_move_date_deadline(self, new_date):
|
||
""" Updates corresponding move picking line deadline dates that are not yet completed. """
|
||
moves_to_update = self.move_ids.filtered(lambda m: m.state not in ('done', 'cancel'))
|
||
if not moves_to_update:
|
||
moves_to_update = self.move_dest_ids.filtered(lambda m: m.state not in ('done', 'cancel'))
|
||
for move in moves_to_update:
|
||
move.date_deadline = new_date + relativedelta(days=move.company_id.po_lead)
|
||
|
||
def _create_or_update_picking(self):
|
||
for line in self:
|
||
if line.product_id and line.product_id.type in ('product', 'consu'):
|
||
# Prevent decreasing below received quantity
|
||
if float_compare(line.product_qty, line.qty_received, line.product_uom.rounding) < 0:
|
||
raise UserError(_('You cannot decrease the ordered quantity below the received quantity.\n'
|
||
'Create a return first.'))
|
||
|
||
if float_compare(line.product_qty, line.qty_invoiced, line.product_uom.rounding) == -1:
|
||
# If the quantity is now below the invoiced quantity, create an activity on the vendor bill
|
||
# inviting the user to create a refund.
|
||
line.invoice_lines[0].move_id.activity_schedule(
|
||
'mail.mail_activity_data_warning',
|
||
note=_('The quantities on your purchase order indicate less than billed. You should ask for a refund.'))
|
||
|
||
# If the user increased quantity of existing line or created a new line
|
||
pickings = line.order_id.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel') and x.location_dest_id.usage in ('internal', 'transit', 'customer'))
|
||
picking = pickings and pickings[0] or False
|
||
if not picking:
|
||
res = line.order_id._prepare_picking()
|
||
picking = self.env['stock.picking'].create(res)
|
||
|
||
moves = line._create_stock_moves(picking)
|
||
moves._action_confirm()._action_assign()
|
||
|
||
def _get_stock_move_price_unit(self):
|
||
self.ensure_one()
|
||
line = self[0]
|
||
order = line.order_id
|
||
price_unit = line.price_unit
|
||
if line.taxes_id:
|
||
price_unit = line.taxes_id.with_context(round=False).compute_all(
|
||
price_unit, currency=line.order_id.currency_id, quantity=1.0, product=line.product_id, partner=line.order_id.partner_id
|
||
)['total_void']
|
||
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:
|
||
price_unit = order.currency_id._convert(
|
||
price_unit, order.company_id.currency_id, self.company_id, self.date_order or fields.Date.today(), round=False)
|
||
return price_unit
|
||
|
||
def _prepare_stock_moves(self, picking):
|
||
""" Prepare the stock moves data for one order line. This function returns a list of
|
||
dictionary ready to be used in stock.move's create()
|
||
"""
|
||
self.ensure_one()
|
||
res = []
|
||
if self.product_id.type not in ['product', 'consu']:
|
||
return res
|
||
|
||
qty = 0.0
|
||
price_unit = self._get_stock_move_price_unit()
|
||
outgoing_moves, incoming_moves = self._get_outgoing_incoming_moves()
|
||
for move in outgoing_moves:
|
||
qty -= move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP')
|
||
for move in incoming_moves:
|
||
qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP')
|
||
|
||
move_dests = self.move_dest_ids
|
||
if not move_dests:
|
||
move_dests = self.move_ids.move_dest_ids.filtered(lambda m: m.state != 'cancel' and not m.location_dest_id.usage == 'supplier')
|
||
|
||
if not move_dests:
|
||
qty_to_attach = 0
|
||
qty_to_push = self.product_qty - qty
|
||
else:
|
||
move_dests_initial_demand = self.product_id.uom_id._compute_quantity(
|
||
sum(move_dests.filtered(lambda m: m.state != 'cancel' and not m.location_dest_id.usage == 'supplier').mapped('product_qty')),
|
||
self.product_uom, rounding_method='HALF-UP')
|
||
qty_to_attach = move_dests_initial_demand - qty
|
||
qty_to_push = self.product_qty - move_dests_initial_demand
|
||
|
||
if float_compare(qty_to_attach, 0.0, precision_rounding=self.product_uom.rounding) > 0:
|
||
product_uom_qty, product_uom = self.product_uom._adjust_uom_quantities(qty_to_attach, self.product_id.uom_id)
|
||
res.append(self._prepare_stock_move_vals(picking, price_unit, product_uom_qty, product_uom))
|
||
if float_compare(qty_to_push, 0.0, precision_rounding=self.product_uom.rounding) > 0:
|
||
product_uom_qty, product_uom = self.product_uom._adjust_uom_quantities(qty_to_push, self.product_id.uom_id)
|
||
extra_move_vals = self._prepare_stock_move_vals(picking, price_unit, product_uom_qty, product_uom)
|
||
extra_move_vals['move_dest_ids'] = False # don't attach
|
||
res.append(extra_move_vals)
|
||
return res
|
||
|
||
def _prepare_stock_move_vals(self, picking, price_unit, product_uom_qty, product_uom):
|
||
self.ensure_one()
|
||
product = self.product_id.with_context(lang=self.order_id.dest_address_id.lang or self.env.user.lang)
|
||
date_planned = self.date_planned or self.order_id.date_planned
|
||
return {
|
||
# truncate to 2000 to avoid triggering index limit error
|
||
# TODO: remove index in master?
|
||
'name': (self.name or '')[:2000],
|
||
'product_id': self.product_id.id,
|
||
'date': date_planned,
|
||
'date_deadline': date_planned + relativedelta(days=self.order_id.company_id.po_lead),
|
||
'location_id': self.order_id.partner_id.property_stock_supplier.id,
|
||
'location_dest_id': (self.orderpoint_id and not (self.move_ids | self.move_dest_ids)) and self.orderpoint_id.location_id.id or self.order_id._get_destination_location(),
|
||
'picking_id': picking.id,
|
||
'partner_id': self.order_id.dest_address_id.id,
|
||
'move_dest_ids': [(4, x) for x in self.move_dest_ids.ids],
|
||
'state': 'draft',
|
||
'purchase_line_id': self.id,
|
||
'company_id': self.order_id.company_id.id,
|
||
'price_unit': price_unit,
|
||
'picking_type_id': self.order_id.picking_type_id.id,
|
||
'group_id': self.order_id.group_id.id,
|
||
'origin': self.order_id.name,
|
||
'description_picking': product.description_pickingin or self.name,
|
||
'propagate_cancel': self.propagate_cancel,
|
||
'route_ids': self.order_id.picking_type_id.warehouse_id and [(6, 0, [x.id for x in self.order_id.picking_type_id.warehouse_id.route_ids])] or [],
|
||
'warehouse_id': self.order_id.picking_type_id.warehouse_id.id,
|
||
'product_uom_qty': product_uom_qty,
|
||
'product_uom': product_uom.id,
|
||
}
|
||
|
||
@api.model
|
||
def _prepare_purchase_order_line_from_procurement(self, product_id, product_qty, product_uom, company_id, values, po):
|
||
line_description = ''
|
||
if values.get('product_description_variants'):
|
||
line_description = values['product_description_variants']
|
||
supplier = values.get('supplier')
|
||
res = self._prepare_purchase_order_line(product_id, product_qty, product_uom, company_id, supplier, po)
|
||
# We need to keep the vendor name set in _prepare_purchase_order_line. To avoid redundancy
|
||
# in the line name, we add the line_description only if different from the product name.
|
||
# This way, we shoud not lose any valuable information.
|
||
if line_description and product_id.name != line_description:
|
||
res['name'] += '\n' + line_description
|
||
res['move_dest_ids'] = [(4, x.id) for x in values.get('move_dest_ids', [])]
|
||
res['orderpoint_id'] = values.get('orderpoint_id', False) and values.get('orderpoint_id').id
|
||
res['propagate_cancel'] = values.get('propagate_cancel')
|
||
res['product_description_variants'] = values.get('product_description_variants')
|
||
return res
|
||
|
||
def _create_stock_moves(self, picking):
|
||
values = []
|
||
for line in self.filtered(lambda l: not l.display_type):
|
||
for val in line._prepare_stock_moves(picking):
|
||
values.append(val)
|
||
line.move_dest_ids.created_purchase_line_id = False
|
||
|
||
return self.env['stock.move'].create(values)
|
||
|
||
def _find_candidate(self, product_id, product_qty, product_uom, location_id, name, origin, company_id, values):
|
||
""" Return the record in self where the procument with values passed as
|
||
args can be merged. If it returns an empty record then a new line will
|
||
be created.
|
||
"""
|
||
description_picking = ''
|
||
if values.get('product_description_variants'):
|
||
description_picking = values['product_description_variants']
|
||
lines = self.filtered(
|
||
lambda l: l.propagate_cancel == values['propagate_cancel']
|
||
and ((values['orderpoint_id'] and not values['move_dest_ids']) and l.orderpoint_id == values['orderpoint_id'] or True)
|
||
)
|
||
|
||
# In case 'product_description_variants' is in the values, we also filter on the PO line
|
||
# name. This way, we can merge lines with the same description. To do so, we need the
|
||
# product name in the context of the PO partner.
|
||
if lines and values.get('product_description_variants'):
|
||
partner = self.mapped('order_id.partner_id')[:1]
|
||
product_lang = product_id.with_context(
|
||
lang=partner.lang,
|
||
partner_id=partner.id,
|
||
)
|
||
name = product_lang.display_name
|
||
if product_lang.description_purchase:
|
||
name += '\n' + product_lang.description_purchase
|
||
lines = lines.filtered(lambda l: l.name == name + '\n' + description_picking)
|
||
if lines:
|
||
return lines[0]
|
||
|
||
return lines and lines[0] or self.env['purchase.order.line']
|
||
|
||
def _get_outgoing_incoming_moves(self):
|
||
outgoing_moves = self.env['stock.move']
|
||
incoming_moves = self.env['stock.move']
|
||
|
||
for move in self.move_ids.filtered(lambda r: r.state != 'cancel' and not r.scrapped and self.product_id == r.product_id):
|
||
if move.location_dest_id.usage == "supplier" and move.to_refund:
|
||
outgoing_moves |= move
|
||
elif move.location_dest_id.usage != "supplier":
|
||
if not move.origin_returned_move_id or (move.origin_returned_move_id and move.to_refund):
|
||
incoming_moves |= move
|
||
|
||
return outgoing_moves, incoming_moves
|
||
|
||
def _update_date_planned(self, updated_date):
|
||
move_to_update = self.move_ids.filtered(lambda m: m.state not in ['done', 'cancel'])
|
||
if not self.move_ids or move_to_update: # Only change the date if there is no move done or none
|
||
super()._update_date_planned(updated_date)
|
||
if move_to_update:
|
||
self._update_move_date_deadline(updated_date)
|
||
|
||
@api.model
|
||
def _update_qty_received_method(self):
|
||
"""Update qty_received_method for old PO before install this module."""
|
||
self.search([])._compute_qty_received_method()
|