[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
This commit is contained in:
Rémy Voet (ryv)
2020-01-23 14:59:57 +00:00
parent 46386f3868
commit 5a1645a8f8
17 changed files with 190 additions and 71 deletions
+2 -1
View File
@@ -18,7 +18,8 @@ by external providers and will automatically generate purchase orders directed t
'data': [
'data/mail_data.xml',
'views/product_views.xml',
'views/sale_views.xml',
'views/sale_order_views.xml',
'views/purchase_order_views.xml',
],
'demo': [
],
+34 -1
View File
@@ -1,17 +1,50 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo import api, fields, models, _
class PurchaseOrder(models.Model):
_inherit = "purchase.order"
sale_order_count = fields.Integer(
"Number of Source Sale",
compute='_compute_sale_order_count',
groups='sales_team.group_sale_salesman')
@api.depends('order_line.sale_order_id')
def _compute_sale_order_count(self):
for purchase in self:
purchase.sale_order_count = len(purchase._get_sale_orders())
def action_view_sale_orders(self):
self.ensure_one()
sale_order_ids = self._get_sale_orders().ids
action = {
'res_model': 'sale.order',
'type': 'ir.actions.act_window',
}
if len(sale_order_ids) == 1:
action.update({
'view_mode': 'form',
'res_id': sale_order_ids[0],
})
else:
action.update({
'name': _('Sources Sale Orders %s' % self.name),
'domain': [('id', 'in', sale_order_ids)],
'view_mode': 'tree,form',
})
return action
def button_cancel(self):
result = super(PurchaseOrder, self).button_cancel()
self.sudo()._activity_cancel_on_sale()
return result
def _get_sale_orders(self):
return self.order_line.sale_order_id
def _activity_cancel_on_sale(self):
""" If some PO are cancelled, we need to put an activity on their origin SO (only the open ones). Since a PO can have
been modified by several SO, when cancelling one PO, many next activities can be schedulded on different SO.
+27 -11
View File
@@ -11,17 +11,15 @@ from odoo.tools import float_compare
class SaleOrder(models.Model):
_inherit = 'sale.order'
purchase_order_count = fields.Integer("Number of Purchase Order", compute='_compute_purchase_order_count', groups='purchase.group_purchase_user')
purchase_order_count = fields.Integer(
"Number of Purchase Order Generated",
compute='_compute_purchase_order_count',
groups='purchase.group_purchase_user')
@api.depends('order_line.purchase_line_ids')
@api.depends('order_line.purchase_line_ids.order_id')
def _compute_purchase_order_count(self):
purchase_line_data = self.env['purchase.order.line'].read_group(
[('sale_order_id', 'in', self.ids)],
['sale_order_id', 'purchase_order_count:count_distinct(order_id)'], ['sale_order_id']
)
purchase_count_map = {item['sale_order_id'][0]: item['purchase_order_count'] for item in purchase_line_data}
for order in self:
order.purchase_order_count = purchase_count_map.get(order.id, 0)
order.purchase_order_count = len(self._get_purchase_orders())
def _action_confirm(self):
result = super(SaleOrder, self)._action_confirm()
@@ -37,11 +35,29 @@ class SaleOrder(models.Model):
self.sudo()._activity_cancel_on_purchase()
return result
def action_view_purchase(self):
action = self.env.ref('purchase.purchase_rfq').read()[0]
action['domain'] = [('id', 'in', self.mapped('order_line.purchase_line_ids.order_id').ids)]
def action_view_purchase_orders(self):
self.ensure_one()
purchase_order_ids = self._get_purchase_orders().ids
action = {
'res_model': 'purchase.order',
'type': 'ir.actions.act_window',
}
if len(purchase_order_ids) == 1:
action.update({
'view_mode': 'form',
'res_id': purchase_order_ids[0],
})
else:
action.update({
'name': _("Purchase Order generated from %s" % self.name),
'domain': [('id', 'in', purchase_order_ids)],
'view_mode': 'tree,form',
})
return action
def _get_purchase_orders(self):
return self.order_line.purchase_line_ids.order_id
def _activity_cancel_on_purchase(self):
""" If some SO are cancelled, we need to put an activity on their generated purchase. If sale lines of
different sale orders impact different purchase, we only want one activity to be attached.
@@ -54,15 +54,15 @@ class TestAccessRights(TestCommonSalePurchaseNoChart):
self.assertTrue(sale_order.name, "Saleperson can read its own SO")
action = sale_order.sudo().action_view_purchase()
action = sale_order.sudo().action_view_purchase_orders()
# try to access PO as sale person
with self.assertRaises(AccessError):
purchase_orders = self.env['purchase.order'].with_user(self.user_salesperson).search(action['domain'])
purchase_orders = self.env['purchase.order'].with_user(self.user_salesperson).browse(action['res_id'])
purchase_orders.read()
# try to access PO as purchase person
purchase_orders = self.env['purchase.order'].with_user(self.user_purchaseperson).search(action['domain'])
purchase_orders = self.env['purchase.order'].with_user(self.user_purchaseperson).browse(action['res_id'])
purchase_orders.read()
# try to access the PO lines from the SO, as sale person
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<record id="purchase_order_inherited_form_sale" model="ir.ui.view">
<field name="name">purchase.order.inherited.form.sale</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<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)]}">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value"><field name="sale_order_count"/></span>
<span class="o_stat_text">Sales</span>
</div>
</button>
</xpath>
</field>
</record>
</data></odoo>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<record id="sale_order_inherited_form_purchase" model="ir.ui.view">
<field name="name">sale.order.inherited.form.purchase</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<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)]}">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value"><field name="purchase_order_count"/></span>
<span class="o_stat_text">Purchases</span>
</div>
</button>
</xpath>
</field>
</record>
</data></odoo>
-23
View File
@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sale_order_form_view_inherit" model="ir.ui.view">
<field name="name">sale.order.form.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_view_invoice']" position="before">
<button type="object"
name="action_view_purchase"
class="oe_stat_button"
icon="fa-shopping-cart"
attrs="{'invisible': [('purchase_order_count', '=', 0)]}"
groups="purchase.group_purchase_user">
<field name="purchase_order_count" widget="statinfo" string="Purchase Orders"/>
</button>
</xpath>
</field>
<field name="active" eval="False"/>
</record>
</odoo>
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'MTO Sale <-> Purchase',
'version': '1.0',
'category': 'Hidden',
'summary': 'SO/PO relation in case of MTO',
'description': """
Add relation information between Sale Orders and Purchase Orders if Make to Order (MTO) is activated on one sold product.
""",
'depends': ['sale_stock', 'purchase_stock', 'sale_purchase'],
'data': [],
'demo': [],
'qweb': [],
'installable': True,
'auto_install': True,
}
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import purchase_order
from . import sale_order
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class PurchaseOrder(models.Model):
_inherit = 'purchase.order'
@api.depends('order_line.move_dest_ids.group_id.sale_id')
def _compute_sale_order_count(self):
super(PurchaseOrder, self)._compute_sale_order_count()
def _get_sale_orders(self):
return super(PurchaseOrder, self)._get_sale_orders() | self.order_line.move_dest_ids.group_id.sale_id
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class SaleOrder(models.Model):
_inherit = 'sale.order'
@api.depends('procurement_group_id.stock_move_ids.created_purchase_line_id.order_id')
def _compute_purchase_order_count(self):
super(SaleOrder, self)._compute_purchase_order_count()
def _get_purchase_orders(self):
return super(SaleOrder, self)._get_purchase_orders() | self.procurement_group_id.stock_move_ids.created_purchase_line_id.order_id
+1
View File
@@ -366,6 +366,7 @@ class ProcurementGroup(models.Model):
('direct', 'Partial'),
('one', 'All at once')], string='Delivery Type', default='direct',
required=True)
stock_move_ids = fields.One2many('stock.move', 'group_id', string="Related Stock Moves")
@api.model
def run(self, procurements, raise_user_error=True):
+1 -1
View File
@@ -20,7 +20,7 @@ going through the retailer's warehouse. In this case no
internal transfer document is needed.
""",
'depends': ['sale_purchase', 'sale_stock', 'purchase_stock'],
'depends': ['sale_purchase_stock'],
'data': ['data/stock_data.xml', 'views/sale_order_views.xml'],
'installable': True,
'auto_install': False,
+1 -16
View File
@@ -5,7 +5,7 @@ from odoo import api, models
class PurchaseOrderLine(models.Model):
_inherit = "purchase.order.line"
_inherit = 'purchase.order.line'
def _prepare_stock_moves(self, picking):
res = super(PurchaseOrderLine, self)._prepare_stock_moves(picking)
@@ -24,18 +24,3 @@ class PurchaseOrderLine(models.Model):
res = super()._prepare_purchase_order_line_from_procurement(product_id, product_qty, product_uom, company_id, values, po)
res['sale_line_id'] = values.get('sale_line_id', False)
return res
class StockRule(models.Model):
_inherit = 'stock.rule'
@api.model
def _get_procurements_to_merge_groupby(self, procurement):
""" Do not group purchase order line if they are linked to different
sale order line. The purpose is to compute the delivered quantities.
"""
return procurement.values.get('sale_line_id'), super(StockRule, self)._get_procurements_to_merge_groupby(procurement)
@api.model
def _get_procurements_to_merge_sorted(self, procurement):
return procurement.values.get('sale_line_id'), super(StockRule, self)._get_procurements_to_merge_sorted(procurement)
+13 -15
View File
@@ -1,24 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models, fields
from odoo import models
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
purchase_line_ids = fields.One2many('purchase.order.line', 'sale_line_id')
def _get_qty_procurement(self, previous_product_uom_qty):
# People without purchase rights should be able to do this operation
purchase_lines_sudo = self.sudo().purchase_line_ids
if purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'):
qty = 0.0
for po_line in purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'):
qty += po_line.product_uom._compute_quantity(po_line.product_qty, self.product_uom, rounding_method='HALF-UP')
return qty
else:
return super(SaleOrderLine, self)._get_qty_procurement(previous_product_uom_qty=previous_product_uom_qty)
_inherit = 'sale.order.line'
def _compute_is_mto(self):
super(SaleOrderLine, self)._compute_is_mto()
@@ -31,3 +18,14 @@ class SaleOrderLine(models.Model):
pull_rule.picking_type_id.sudo().default_location_dest_id.usage == 'customer':
line.is_mto = True
break
def _get_qty_procurement(self, previous_product_uom_qty):
# People without purchase rights should be able to do this operation
purchase_lines_sudo = self.sudo().purchase_line_ids
if purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'):
qty = 0.0
for po_line in purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'):
qty += po_line.product_uom._compute_quantity(po_line.product_qty, self.product_uom, rounding_method='HALF-UP')
return qty
else:
return super(SaleOrderLine, self)._get_qty_procurement(previous_product_uom_qty=previous_product_uom_qty)
+15
View File
@@ -4,6 +4,21 @@
from odoo import api, models
class StockRule(models.Model):
_inherit = 'stock.rule'
@api.model
def _get_procurements_to_merge_groupby(self, procurement):
""" Do not group purchase order line if they are linked to different
sale order line. The purpose is to compute the delivered quantities.
"""
return procurement.values.get('sale_line_id'), super(StockRule, self)._get_procurements_to_merge_groupby(procurement)
@api.model
def _get_procurements_to_merge_sorted(self, procurement):
return procurement.values.get('sale_line_id'), super(StockRule, self)._get_procurements_to_merge_sorted(procurement)
class ProcurementGroup(models.Model):
_inherit = "procurement.group"