[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:
@@ -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': [
|
||||
],
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user