Skip to content

Commit 5a1645a

Browse files
committed
[ADD] sale_purchase_stock: link between PO<->SO in case of MTO
In order to improve the navigation between of SO and PO in the case of MTO, add a direct link between PO<->SO. When the SO is confirmed (with storable product(s) with MTO + buy), PO is/are generated, in this case, add a stat button on each model form to avoid to manually search the related SO with the source field (name). task-1913392
1 parent 46386f3 commit 5a1645a

17 files changed

+190
-71
lines changed

addons/sale_purchase/__manifest__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
'data': [
1919
'data/mail_data.xml',
2020
'views/product_views.xml',
21-
'views/sale_views.xml',
21+
'views/sale_order_views.xml',
22+
'views/purchase_order_views.xml',
2223
],
2324
'demo': [
2425
],

addons/sale_purchase/models/purchase_order.py

+34-1
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,50 @@
11
# -*- coding: utf-8 -*-
22
# Part of Odoo. See LICENSE file for full copyright and licensing details.
33

4-
from odoo import api, fields, models
4+
from odoo import api, fields, models, _
55

66

77
class PurchaseOrder(models.Model):
88
_inherit = "purchase.order"
99

10+
sale_order_count = fields.Integer(
11+
"Number of Source Sale",
12+
compute='_compute_sale_order_count',
13+
groups='sales_team.group_sale_salesman')
14+
15+
@api.depends('order_line.sale_order_id')
16+
def _compute_sale_order_count(self):
17+
for purchase in self:
18+
purchase.sale_order_count = len(purchase._get_sale_orders())
19+
20+
def action_view_sale_orders(self):
21+
self.ensure_one()
22+
sale_order_ids = self._get_sale_orders().ids
23+
action = {
24+
'res_model': 'sale.order',
25+
'type': 'ir.actions.act_window',
26+
}
27+
if len(sale_order_ids) == 1:
28+
action.update({
29+
'view_mode': 'form',
30+
'res_id': sale_order_ids[0],
31+
})
32+
else:
33+
action.update({
34+
'name': _('Sources Sale Orders %s' % self.name),
35+
'domain': [('id', 'in', sale_order_ids)],
36+
'view_mode': 'tree,form',
37+
})
38+
return action
39+
1040
def button_cancel(self):
1141
result = super(PurchaseOrder, self).button_cancel()
1242
self.sudo()._activity_cancel_on_sale()
1343
return result
1444

45+
def _get_sale_orders(self):
46+
return self.order_line.sale_order_id
47+
1548
def _activity_cancel_on_sale(self):
1649
""" If some PO are cancelled, we need to put an activity on their origin SO (only the open ones). Since a PO can have
1750
been modified by several SO, when cancelling one PO, many next activities can be schedulded on different SO.

addons/sale_purchase/models/sale_order.py

+27-11
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,15 @@
1111
class SaleOrder(models.Model):
1212
_inherit = 'sale.order'
1313

14-
purchase_order_count = fields.Integer("Number of Purchase Order", compute='_compute_purchase_order_count', groups='purchase.group_purchase_user')
14+
purchase_order_count = fields.Integer(
15+
"Number of Purchase Order Generated",
16+
compute='_compute_purchase_order_count',
17+
groups='purchase.group_purchase_user')
1518

16-
@api.depends('order_line.purchase_line_ids')
19+
@api.depends('order_line.purchase_line_ids.order_id')
1720
def _compute_purchase_order_count(self):
18-
purchase_line_data = self.env['purchase.order.line'].read_group(
19-
[('sale_order_id', 'in', self.ids)],
20-
['sale_order_id', 'purchase_order_count:count_distinct(order_id)'], ['sale_order_id']
21-
)
22-
purchase_count_map = {item['sale_order_id'][0]: item['purchase_order_count'] for item in purchase_line_data}
2321
for order in self:
24-
order.purchase_order_count = purchase_count_map.get(order.id, 0)
22+
order.purchase_order_count = len(self._get_purchase_orders())
2523

2624
def _action_confirm(self):
2725
result = super(SaleOrder, self)._action_confirm()
@@ -37,11 +35,29 @@ def action_cancel(self):
3735
self.sudo()._activity_cancel_on_purchase()
3836
return result
3937

40-
def action_view_purchase(self):
41-
action = self.env.ref('purchase.purchase_rfq').read()[0]
42-
action['domain'] = [('id', 'in', self.mapped('order_line.purchase_line_ids.order_id').ids)]
38+
def action_view_purchase_orders(self):
39+
self.ensure_one()
40+
purchase_order_ids = self._get_purchase_orders().ids
41+
action = {
42+
'res_model': 'purchase.order',
43+
'type': 'ir.actions.act_window',
44+
}
45+
if len(purchase_order_ids) == 1:
46+
action.update({
47+
'view_mode': 'form',
48+
'res_id': purchase_order_ids[0],
49+
})
50+
else:
51+
action.update({
52+
'name': _("Purchase Order generated from %s" % self.name),
53+
'domain': [('id', 'in', purchase_order_ids)],
54+
'view_mode': 'tree,form',
55+
})
4356
return action
4457

58+
def _get_purchase_orders(self):
59+
return self.order_line.purchase_line_ids.order_id
60+
4561
def _activity_cancel_on_purchase(self):
4662
""" If some SO are cancelled, we need to put an activity on their generated purchase. If sale lines of
4763
different sale orders impact different purchase, we only want one activity to be attached.

addons/sale_purchase/tests/test_access_rights.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,15 @@ def test_access_saleperson(self):
5454

5555
self.assertTrue(sale_order.name, "Saleperson can read its own SO")
5656

57-
action = sale_order.sudo().action_view_purchase()
57+
action = sale_order.sudo().action_view_purchase_orders()
5858

5959
# try to access PO as sale person
6060
with self.assertRaises(AccessError):
61-
purchase_orders = self.env['purchase.order'].with_user(self.user_salesperson).search(action['domain'])
61+
purchase_orders = self.env['purchase.order'].with_user(self.user_salesperson).browse(action['res_id'])
6262
purchase_orders.read()
6363

6464
# try to access PO as purchase person
65-
purchase_orders = self.env['purchase.order'].with_user(self.user_purchaseperson).search(action['domain'])
65+
purchase_orders = self.env['purchase.order'].with_user(self.user_purchaseperson).browse(action['res_id'])
6666
purchase_orders.read()
6767

6868
# try to access the PO lines from the SO, as sale person
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo><data>
3+
<record id="purchase_order_inherited_form_sale" model="ir.ui.view">
4+
<field name="name">purchase.order.inherited.form.sale</field>
5+
<field name="model">purchase.order</field>
6+
<field name="inherit_id" ref="purchase.purchase_order_form"/>
7+
<field name="arch" type="xml">
8+
<xpath expr="//div[@name='button_box']" position="inside">
9+
<button class="oe_stat_button" name="action_view_sale_orders" type="object" icon="fa-dollar" groups='sales_team.group_sale_salesman' attrs="{'invisible': [('sale_order_count', '=', 0)]}">
10+
<div class="o_field_widget o_stat_info">
11+
<span class="o_stat_value"><field name="sale_order_count"/></span>
12+
<span class="o_stat_text">Sales</span>
13+
</div>
14+
</button>
15+
</xpath>
16+
</field>
17+
</record>
18+
</data></odoo>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo><data>
3+
<record id="sale_order_inherited_form_purchase" model="ir.ui.view">
4+
<field name="name">sale.order.inherited.form.purchase</field>
5+
<field name="model">sale.order</field>
6+
<field name="inherit_id" ref="sale.view_order_form"/>
7+
<field name="arch" type="xml">
8+
<xpath expr="//div[@name='button_box']" position="inside">
9+
<button class="oe_stat_button" name="action_view_purchase_orders" type="object" icon="fa-credit-card" groups='purchase.group_purchase_user' attrs="{'invisible': [('purchase_order_count', '=', 0)]}">
10+
<div class="o_field_widget o_stat_info">
11+
<span class="o_stat_value"><field name="purchase_order_count"/></span>
12+
<span class="o_stat_text">Purchases</span>
13+
</div>
14+
</button>
15+
</xpath>
16+
</field>
17+
</record>
18+
</data></odoo>

addons/sale_purchase/views/sale_views.xml

-23
This file was deleted.
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# -*- coding: utf-8 -*-
2+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
3+
4+
from . import models
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# -*- coding: utf-8 -*-
2+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
3+
4+
{
5+
'name': 'MTO Sale <-> Purchase',
6+
'version': '1.0',
7+
'category': 'Hidden',
8+
'summary': 'SO/PO relation in case of MTO',
9+
'description': """
10+
Add relation information between Sale Orders and Purchase Orders if Make to Order (MTO) is activated on one sold product.
11+
""",
12+
'depends': ['sale_stock', 'purchase_stock', 'sale_purchase'],
13+
'data': [],
14+
'demo': [],
15+
'qweb': [],
16+
'installable': True,
17+
'auto_install': True,
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# -*- coding: utf-8 -*-
2+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
3+
4+
from . import purchase_order
5+
from . import sale_order
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# -*- coding: utf-8 -*-
2+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
3+
4+
from odoo import api, models
5+
6+
7+
class PurchaseOrder(models.Model):
8+
_inherit = 'purchase.order'
9+
10+
@api.depends('order_line.move_dest_ids.group_id.sale_id')
11+
def _compute_sale_order_count(self):
12+
super(PurchaseOrder, self)._compute_sale_order_count()
13+
14+
def _get_sale_orders(self):
15+
return super(PurchaseOrder, self)._get_sale_orders() | self.order_line.move_dest_ids.group_id.sale_id
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# -*- coding: utf-8 -*-
2+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
3+
4+
from odoo import api, models
5+
6+
7+
class SaleOrder(models.Model):
8+
_inherit = 'sale.order'
9+
10+
@api.depends('procurement_group_id.stock_move_ids.created_purchase_line_id.order_id')
11+
def _compute_purchase_order_count(self):
12+
super(SaleOrder, self)._compute_purchase_order_count()
13+
14+
def _get_purchase_orders(self):
15+
return super(SaleOrder, self)._get_purchase_orders() | self.procurement_group_id.stock_move_ids.created_purchase_line_id.order_id

addons/stock/models/stock_rule.py

+1
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ class ProcurementGroup(models.Model):
366366
('direct', 'Partial'),
367367
('one', 'All at once')], string='Delivery Type', default='direct',
368368
required=True)
369+
stock_move_ids = fields.One2many('stock.move', 'group_id', string="Related Stock Moves")
369370

370371
@api.model
371372
def run(self, procurements, raise_user_error=True):

addons/stock_dropshipping/__manifest__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
internal transfer document is needed.
2121
2222
""",
23-
'depends': ['sale_purchase', 'sale_stock', 'purchase_stock'],
23+
'depends': ['sale_purchase_stock'],
2424
'data': ['data/stock_data.xml', 'views/sale_order_views.xml'],
2525
'installable': True,
2626
'auto_install': False,

addons/stock_dropshipping/models/purchase.py

+1-16
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66

77
class PurchaseOrderLine(models.Model):
8-
_inherit = "purchase.order.line"
8+
_inherit = 'purchase.order.line'
99

1010
def _prepare_stock_moves(self, picking):
1111
res = super(PurchaseOrderLine, self)._prepare_stock_moves(picking)
@@ -24,18 +24,3 @@ def _prepare_purchase_order_line_from_procurement(self, product_id, product_qty,
2424
res = super()._prepare_purchase_order_line_from_procurement(product_id, product_qty, product_uom, company_id, values, po)
2525
res['sale_line_id'] = values.get('sale_line_id', False)
2626
return res
27-
28-
29-
class StockRule(models.Model):
30-
_inherit = 'stock.rule'
31-
32-
@api.model
33-
def _get_procurements_to_merge_groupby(self, procurement):
34-
""" Do not group purchase order line if they are linked to different
35-
sale order line. The purpose is to compute the delivered quantities.
36-
"""
37-
return procurement.values.get('sale_line_id'), super(StockRule, self)._get_procurements_to_merge_groupby(procurement)
38-
39-
@api.model
40-
def _get_procurements_to_merge_sorted(self, procurement):
41-
return procurement.values.get('sale_line_id'), super(StockRule, self)._get_procurements_to_merge_sorted(procurement)
+13-15
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,11 @@
11
# -*- coding: utf-8 -*-
22
# Part of Odoo. See LICENSE file for full copyright and licensing details.
33

4-
from odoo import api, models, fields
4+
from odoo import models
55

66

77
class SaleOrderLine(models.Model):
8-
_inherit = "sale.order.line"
9-
10-
purchase_line_ids = fields.One2many('purchase.order.line', 'sale_line_id')
11-
12-
def _get_qty_procurement(self, previous_product_uom_qty):
13-
# People without purchase rights should be able to do this operation
14-
purchase_lines_sudo = self.sudo().purchase_line_ids
15-
if purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'):
16-
qty = 0.0
17-
for po_line in purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'):
18-
qty += po_line.product_uom._compute_quantity(po_line.product_qty, self.product_uom, rounding_method='HALF-UP')
19-
return qty
20-
else:
21-
return super(SaleOrderLine, self)._get_qty_procurement(previous_product_uom_qty=previous_product_uom_qty)
8+
_inherit = 'sale.order.line'
229

2310
def _compute_is_mto(self):
2411
super(SaleOrderLine, self)._compute_is_mto()
@@ -31,3 +18,14 @@ def _compute_is_mto(self):
3118
pull_rule.picking_type_id.sudo().default_location_dest_id.usage == 'customer':
3219
line.is_mto = True
3320
break
21+
22+
def _get_qty_procurement(self, previous_product_uom_qty):
23+
# People without purchase rights should be able to do this operation
24+
purchase_lines_sudo = self.sudo().purchase_line_ids
25+
if purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'):
26+
qty = 0.0
27+
for po_line in purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'):
28+
qty += po_line.product_uom._compute_quantity(po_line.product_qty, self.product_uom, rounding_method='HALF-UP')
29+
return qty
30+
else:
31+
return super(SaleOrderLine, self)._get_qty_procurement(previous_product_uom_qty=previous_product_uom_qty)

addons/stock_dropshipping/models/stock.py

+15
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@
44
from odoo import api, models
55

66

7+
class StockRule(models.Model):
8+
_inherit = 'stock.rule'
9+
10+
@api.model
11+
def _get_procurements_to_merge_groupby(self, procurement):
12+
""" Do not group purchase order line if they are linked to different
13+
sale order line. The purpose is to compute the delivered quantities.
14+
"""
15+
return procurement.values.get('sale_line_id'), super(StockRule, self)._get_procurements_to_merge_groupby(procurement)
16+
17+
@api.model
18+
def _get_procurements_to_merge_sorted(self, procurement):
19+
return procurement.values.get('sale_line_id'), super(StockRule, self)._get_procurements_to_merge_sorted(procurement)
20+
21+
722
class ProcurementGroup(models.Model):
823
_inherit = "procurement.group"
924

0 commit comments

Comments
 (0)