1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
|
# -*- 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, float_round
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', compute_sudo=False)
@api.depends('order_line.move_ids.picking_id')
def _compute_picking(self):
for order in self:
pickings = order.order_line.mapped('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.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)
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
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 + 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
price_unit_prec = self.env['decimal.precision'].precision_get('Product Price')
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, product=line.product_id, partner=line.order_id.partner_id
)['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:
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)
description_picking = product._get_description(self.order_id.picking_type_id)
if self.product_description_variants:
description_picking += "\n" + self.product_description_variants
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': description_picking,
'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,
}
@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()
|