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
|
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
class SaleOrder(models.Model):
_inherit = 'sale.order'
mrp_production_count = fields.Integer(
"Count of MO generated",
compute='_compute_mrp_production_count',
groups='mrp.group_mrp_user')
@api.depends('procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids')
def _compute_mrp_production_count(self):
for sale in self:
sale.mrp_production_count = len(sale.procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids)
def action_view_mrp_production(self):
self.ensure_one()
mrp_production_ids = self.procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids.ids
action = {
'res_model': 'mrp.production',
'type': 'ir.actions.act_window',
}
if len(mrp_production_ids) == 1:
action.update({
'view_mode': 'form',
'res_id': mrp_production_ids[0],
})
else:
action.update({
'name': _("Manufacturing Orders Generated by %s", self.name),
'domain': [('id', 'in', mrp_production_ids)],
'view_mode': 'tree,form',
})
return action
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
@api.depends('product_uom_qty', 'qty_delivered', 'product_id', 'state')
def _compute_qty_to_deliver(self):
"""The inventory widget should now be visible in more cases if the product is consumable."""
super(SaleOrderLine, self)._compute_qty_to_deliver()
for line in self:
# Hide the widget for kits since forecast doesn't support them.
boms = self.env['mrp.bom']
if line.state == 'sale':
boms = line.move_ids.mapped('bom_line_id.bom_id')
elif line.state in ['draft', 'sent'] and line.product_id:
boms = boms._bom_find(product=line.product_id, company_id=line.company_id.id, bom_type='phantom')
relevant_bom = boms.filtered(lambda b: b.type == 'phantom' and
(b.product_id == line.product_id or
(b.product_tmpl_id == line.product_id.product_tmpl_id and not b.product_id)))
if relevant_bom:
line.display_qty_widget = False
continue
if line.state == 'draft' and line.product_type == 'consu':
components = line.product_id.get_components()
if components and components != [line.product_id.id]:
line.display_qty_widget = True
def _compute_qty_delivered(self):
super(SaleOrderLine, self)._compute_qty_delivered()
for order_line in self:
if order_line.qty_delivered_method == 'stock_move':
boms = order_line.move_ids.filtered(lambda m: m.state != 'cancel').mapped('bom_line_id.bom_id')
dropship = False
if not boms and any(m._is_dropshipped() for m in order_line.move_ids):
boms = boms._bom_find(product=order_line.product_id, company_id=order_line.company_id.id, bom_type='phantom')
dropship = True
# We fetch the BoMs of type kits linked to the order_line,
# the we keep only the one related to the finished produst.
# This bom shoud be the only one since bom_line_id was written on the moves
relevant_bom = boms.filtered(lambda b: b.type == 'phantom' and
(b.product_id == order_line.product_id or
(b.product_tmpl_id == order_line.product_id.product_tmpl_id and not b.product_id)))
if relevant_bom:
# In case of dropship, we use a 'all or nothing' policy since 'bom_line_id' was
# not written on a move coming from a PO.
# FIXME: if the components of a kit have different suppliers, multiple PO
# are generated. If one PO is confirmed and all the others are in draft, receiving
# the products for this PO will set the qty_delivered. We might need to check the
# state of all PO as well... but sale_mrp doesn't depend on purchase.
if dropship:
moves = order_line.move_ids.filtered(lambda m: m.state != 'cancel')
if moves and all(m.state == 'done' for m in moves):
order_line.qty_delivered = order_line.product_uom_qty
else:
order_line.qty_delivered = 0.0
continue
moves = order_line.move_ids.filtered(lambda m: m.state == 'done' and not m.scrapped)
filters = {
'incoming_moves': lambda m: m.location_dest_id.usage == 'customer' and (not m.origin_returned_move_id or (m.origin_returned_move_id and m.to_refund)),
'outgoing_moves': lambda m: m.location_dest_id.usage != 'customer' and m.to_refund
}
order_qty = order_line.product_uom._compute_quantity(order_line.product_uom_qty, relevant_bom.product_uom_id)
qty_delivered = moves._compute_kit_quantities(order_line.product_id, order_qty, relevant_bom, filters)
order_line.qty_delivered = relevant_bom.product_uom_id._compute_quantity(qty_delivered, order_line.product_uom)
# If no relevant BOM is found, fall back on the all-or-nothing policy. This happens
# when the product sold is made only of kits. In this case, the BOM of the stock moves
# do not correspond to the product sold => no relevant BOM.
elif boms:
if all(m.state == 'done' for m in order_line.move_ids):
order_line.qty_delivered = order_line.product_uom_qty
else:
order_line.qty_delivered = 0.0
def _get_bom_component_qty(self, bom):
bom_quantity = self.product_uom._compute_quantity(1, bom.product_uom_id)
boms, lines = bom.explode(self.product_id, bom_quantity)
components = {}
for line, line_data in lines:
product = line.product_id.id
uom = line.product_uom_id
qty = line_data['qty']
if components.get(product, False):
if uom.id != components[product]['uom']:
from_uom = uom
to_uom = self.env['uom.uom'].browse(components[product]['uom'])
qty = from_uom._compute_quantity(qty, to_uom)
components[product]['qty'] += qty
else:
# To be in the uom reference of the product
to_uom = self.env['product.product'].browse(product).uom_id
if uom.id != to_uom.id:
from_uom = uom
qty = from_uom._compute_quantity(qty, to_uom)
components[product] = {'qty': qty, 'uom': to_uom.id}
return components
def _get_qty_procurement(self, previous_product_uom_qty=False):
self.ensure_one()
# Specific case when we change the qty on a SO for a kit product.
# We don't try to be too smart and keep a simple approach: we compare the quantity before
# and after update, and return the difference. We don't take into account what was already
# sent, or any other exceptional case.
bom = self.env['mrp.bom']._bom_find(product=self.product_id, bom_type='phantom')
if bom and previous_product_uom_qty:
return previous_product_uom_qty and previous_product_uom_qty.get(self.id, 0.0)
return super(SaleOrderLine, self)._get_qty_procurement(previous_product_uom_qty=previous_product_uom_qty)
|