# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from markupsafe import Markup from dateutil.relativedelta import relativedelta from odoo import api, fields, models, SUPERUSER_ID, _ from odoo.tools.float_utils import float_compare, float_is_zero, float_round from odoo.exceptions import UserError from odoo.addons.purchase.models.purchase import PurchaseOrder as Purchase from odoo.tools.misc import OrderedSet 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.") incoterm_location = fields.Char(string='Incoterm Location', states={'done': [('readonly', True)]}) incoming_picking_count = fields.Integer("Incoming Shipment count", compute='_compute_incoming_picking_count') picking_ids = fields.Many2many('stock.picking', compute='_compute_picking_ids', string='Receptions', copy=False, store=True) dest_address_id = fields.Many2one('res.partner', compute='_compute_dest_address_id', store=True, readonly=False) 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("Arrival", 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', compute_sudo=False) receipt_status = fields.Selection([ ('pending', 'Not Received'), ('partial', 'Partially Received'), ('full', 'Fully Received'), ], string='Receipt Status', compute='_compute_receipt_status', store=True) @api.depends('order_line.move_ids.picking_id') def _compute_picking_ids(self): for order in self: order.picking_ids = order.order_line.move_ids.picking_id @api.depends('picking_ids') def _compute_incoming_picking_count(self): for order in self: order.incoming_picking_count = len(order.picking_ids) @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 != 'supplier' 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.depends('picking_ids', 'picking_ids.state') def _compute_receipt_status(self): for order in self: if not order.picking_ids or all(p.state == 'cancel' for p in order.picking_ids): order.receipt_status = False elif all(p.state in ['done', 'cancel'] for p in order.picking_ids): order.receipt_status = 'full' elif any(p.state == 'done' for p in order.picking_ids): order.receipt_status = 'partial' else: order.receipt_status = 'pending' @api.depends('picking_type_id') def _compute_dest_address_id(self): self.filtered(lambda po: po.picking_type_id.default_location_dest_id.usage != 'customer').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): order_lines_ids = OrderedSet() pickings_to_cancel_ids = OrderedSet() 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'): order_lines_ids.update(order.order_line.ids) pickings_to_cancel_ids.update(order.picking_ids.filtered(lambda r: r.state != 'cancel').ids) order_lines = self.env['purchase.order.line'].browse(order_lines_ids) moves_to_cancel_ids = OrderedSet() moves_to_recompute_ids = OrderedSet() for order_line in order_lines: moves_to_cancel_ids.update(order_line.move_ids.ids) if order_line.move_dest_ids: move_dest_ids = order_line.move_dest_ids.filtered(lambda move: move.state != 'done' and not move.scrapped) if order_line.propagate_cancel: moves_to_cancel_ids.update(move_dest_ids.ids) else: moves_to_recompute_ids.update(move_dest_ids.ids) if moves_to_cancel_ids: moves_to_cancel = self.env['stock.move'].browse(moves_to_cancel_ids) moves_to_cancel._action_cancel() if moves_to_recompute_ids: moves_to_recompute = self.env['stock.move'].browse(moves_to_recompute_ids) moves_to_recompute.write({'procure_method': 'make_to_stock'}) moves_to_recompute._recompute_state() if pickings_to_cancel_ids: pikings_to_cancel = self.env['stock.picking'].browse(pickings_to_cancel_ids) pikings_to_cancel.action_cancel() if order_lines: order_lines.write({'move_dest_ids': [(5, 0, 0)]}) return super(PurchaseOrder, self).button_cancel() def action_view_picking(self): return self._get_action_view_picking(self.picking_ids) def _get_action_view_picking(self, pickings): """ This function returns an action that display existing picking orders of given purchase order ids. When only one found, show the picking immediately. """ self.ensure_one() 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} # choose the view_mode accordingly if not pickings or len(pickings) > 1: result['domain'] = [('id', 'in', pickings.ids)] elif len(pickings) == 1: res = self.env.ref('stock.view_picking_form', False) form_view = [(res and res.id or False, 'form')] result['views'] = form_view + [(state, view) for state, view in result.get('views', []) if view != 'form'] result['res_id'] = pickings.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_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['ir.qweb']._render('purchase_stock.exception_on_po', values) documents = self.env['stock.picking']._log_activity_get_documents(purchase_order_lines_quantities, 'move_ids', 'DOWN', _keys_in_groupby) filtered_documents = {} for (parent, responsible), rendering_context in documents.items(): if parent._name == 'stock.picking': if parent.state in ['cancel', 'done']: 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.filtered(lambda po: po.state in ('purchase', 'done')): 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) pickings = picking 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() # Get following pickings (created by push rules) to confirm them as well. forward_pickings = self.env['stock.picking']._get_impacted_pickings(moves) (pickings | forward_pickings).action_confirm() 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: message = _("Those dates couldn’t be modified accordingly on the receipt %s which had already been validated.", validated_picking[0].name) elif not self.picking_ids: message = _("Corresponding receipt not found.") else: message = _("Those dates have been updated accordingly on the receipt %s.", self.picking_ids[0].name) activity.note += Markup('

{}

').format(message) 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('

') note_lines.pop() activity.note = Markup('

').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' def _ondelete_stock_moves(self): modified_fields = ['qty_received_manual', 'qty_received_method'] self.flush_recordset(fnames=['qty_received', *modified_fields]) self.invalidate_recordset(fnames=modified_fields, flush=False) query = f''' UPDATE {self._table} SET qty_received_manual = qty_received, qty_received_method = 'manual' WHERE id IN %(ids)s ''' self.env.cr.execute(query, {'ids': self._ids or (None,)}) self.modified(modified_fields) qty_received_method = fields.Selection(selection_add=[('stock_moves', 'Stock Moves')], ondelete={'stock_moves': _ondelete_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', copy=False, index='btree_not_null') 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) forecasted_issue = fields.Boolean(compute='_compute_forecasted_issue') 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' def _get_po_line_moves(self): self.ensure_one() moves = self.move_ids.filtered(lambda m: m.product_id == self.product_id) if self._context.get('accrual_entry_date'): moves = moves.filtered(lambda r: fields.Date.context_today(r, r.date) <= self._context['accrual_entry_date']) return moves def _get_po_line_invoice_lines_su(self): #TODO remove in master: un-used return self.sudo().invoice_lines @api.depends('move_ids.state', 'move_ids.product_uom_qty', 'move_ids.product_uom') def _compute_qty_received(self): from_stock_lines = self.filtered(lambda order_line: order_line.qty_received_method == 'stock_moves') super(PurchaseOrderLine, self - from_stock_lines)._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._get_po_line_moves(): if move.state == 'done': if move._is_purchase_return(): if move.to_refund: total -= move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom, rounding_method='HALF-UP') 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.origin_returned_move_id and move.origin_returned_move_id._is_purchase_return() and not move.to_refund: pass else: total += move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom, rounding_method='HALF-UP') line._track_qty_received(total) line.qty_received = total @api.depends('product_uom_qty', 'date_planned') def _compute_forecasted_issue(self): for line in self: warehouse = line.order_id.picking_type_id.warehouse_id line.forecasted_issue = False if line.product_id: virtual_available = line.product_id.with_context(warehouse=warehouse.id, to_date=line.date_planned).virtual_available if line.state == 'draft': virtual_available += line.product_uom_qty if virtual_available < 0: line.forecasted_issue = True @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): if values.get('date_planned'): new_date = fields.Datetime.to_datetime(values['date_planned']) self.filtered(lambda l: not l.display_type)._update_move_date_deadline(new_date) lines = self.filtered(lambda l: l.order_id.state == 'purchase' and not l.display_type) if 'product_packaging_id' in values: self.move_ids.filtered( lambda m: m.state not in ['cancel', 'done'] ).product_packaging_id = values['product_packaging_id'] previous_product_uom_qty = {line.id: line.product_uom_qty for line in lines} previous_product_qty = {line.id: line.product_qty for line in lines} result = super(PurchaseOrderLine, self).write(values) if 'price_unit' in values: for line in lines: # Avoid updating kit components' stock.move moves = line.move_ids.filtered(lambda s: s.state not in ('cancel', 'done') and s.product_id == line.product_id) moves.write({'price_unit': line._get_stock_move_price_unit()}) if 'product_qty' in values: lines = lines.filtered(lambda l: float_compare(previous_product_qty[l.id], l.product_qty, precision_rounding=l.product_uom.rounding) != 0) lines.with_context(previous_product_qty=previous_product_uom_qty)._create_or_update_picking() return result def action_product_forecast_report(self): self.ensure_one() action = self.product_id.action_product_forecast_report() action['context'] = { 'active_id': self.product_id.id, 'active_model': 'product.product', 'move_to_match_ids': self.move_ids.filtered(lambda m: m.product_id == self.product_id).ids, 'purchase_line_to_match_id': self.id, } warehouse = self.order_id.picking_type_id.warehouse_id if warehouse: action['context']['warehouse'] = warehouse.id return action def unlink(self): self.move_ids._action_cancel() ppg_cancel_lines = self.filtered(lambda line: line.propagate_cancel) ppg_cancel_lines.move_dest_ids._action_cancel() not_ppg_cancel_lines = self.filtered(lambda line: not line.propagate_cancel) not_ppg_cancel_lines.move_dest_ids.write({'procure_method': 'make_to_stock'}) not_ppg_cancel_lines.move_dest_ids._recompute_state() return super().unlink() # -------------------------------------------------- # 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 def _create_or_update_picking(self): for line in self: if line.product_id and line.product_id.type in ('product', 'consu'): rounding = line.product_uom.rounding # Prevent decreasing below received quantity if float_compare(line.product_qty, line.qty_received, precision_rounding=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, precision_rounding=rounding) < 0 and line.invoice_lines: # 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 # Give priority to the pickings related to the line line_pickings = line.move_ids.picking_id.filtered(lambda p: p.state not in ('done', 'cancel') and p.location_dest_id.usage in ('internal', 'transit', 'customer')) if line_pickings: picking = line_pickings[0] else: 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: if not line.product_qty > line.qty_received: continue 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() order = self.order_id price_unit = self._convert_to_tax_base_line_dict()['price_unit'] price_unit_prec = self.env['decimal.precision'].precision_get('Product Price') if self.taxes_id: qty = self.product_qty or 1 price_unit = self.taxes_id.with_context(round=False).compute_all( price_unit, currency=self.order_id.currency_id, quantity=qty, product=self.product_id, partner=self.order_id.partner_id )['total_void'] price_unit = price_unit / qty if self.product_uom.id != self.product_id.uom_id.id: price_unit *= self.product_uom.factor / self.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 float_round(price_unit, precision_digits=price_unit_prec) 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 price_unit = self._get_stock_move_price_unit() qty = self._get_qty_procurement() move_dests = self.move_dest_ids or self.move_ids.move_dest_ids move_dests = move_dests.filtered(lambda m: m.state != 'cancel' and not m._is_purchase_return()) 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 not float_is_zero(qty_to_push, precision_rounding=self.product_uom.rounding): 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 _get_qty_procurement(self): self.ensure_one() qty = 0.0 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') return qty def _check_orderpoint_picking_type(self): warehouse_loc = self.order_id.picking_type_id.warehouse_id.view_location_id dest_loc = self.move_dest_ids.location_id or self.orderpoint_id.location_id if warehouse_loc and dest_loc and dest_loc.warehouse_id and not warehouse_loc.parent_path in dest_loc[0].parent_path: raise UserError(_('For the product %s, the warehouse of the operation type (%s) is inconsistent with the location (%s) of the reordering rule (%s). Change the operation type or cancel the request for quotation.', self.product_id.display_name, self.order_id.picking_type_id.display_name, self.orderpoint_id.location_id.display_name, self.orderpoint_id.display_name)) def _prepare_stock_move_vals(self, picking, price_unit, product_uom_qty, product_uom): self.ensure_one() self._check_orderpoint_picking_type() 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.product_id.display_name or '')[:2000], 'product_id': self.product_id.id, 'date': date_planned, 'date_deadline': date_planned, '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 (picking.location_dest_id.parent_path in self.orderpoint_id.location_id.parent_path)) 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, 'warehouse_id': self.order_id.picking_type_id.warehouse_id.id, 'product_uom_qty': product_uom_qty, 'product_uom': product_uom.id, 'product_packaging_id': self.product_packaging_id.id, 'sequence': self.sequence, } @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['date_planned'] = values.get('date_planned') 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 (l.orderpoint_id == values['orderpoint_id'] if values['orderpoint_id'] and not values['move_dest_ids'] else 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._is_purchase_return() 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(['!', ('state', 'in', ['purchase', 'done'])])._compute_qty_received_method()