Skip to content

Commit

Permalink
[ADD] sale_purchase_stock: link between PO<->SO in case of MTO
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ryv-odoo committed Jan 23, 2020
1 parent 46386f3 commit 5a1645a
Show file tree
Hide file tree
Showing 17 changed files with 190 additions and 71 deletions.
3 changes: 2 additions & 1 deletion addons/sale_purchase/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
'data': [
'data/mail_data.xml',
'views/product_views.xml',
'views/sale_views.xml',
'views/sale_order_views.xml',
'views/purchase_order_views.xml',
],
'demo': [
],
Expand Down
35 changes: 34 additions & 1 deletion addons/sale_purchase/models/purchase_order.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
38 changes: 27 additions & 11 deletions addons/sale_purchase/models/sale_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,15 @@
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()
Expand All @@ -37,11 +35,29 @@ def action_cancel(self):
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.
Expand Down
6 changes: 3 additions & 3 deletions addons/sale_purchase/tests/test_access_rights.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,15 @@ def test_access_saleperson(self):

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
Expand Down
18 changes: 18 additions & 0 deletions addons/sale_purchase/views/purchase_order_views.xml
Original file line number Diff line number Diff line change
@@ -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>
18 changes: 18 additions & 0 deletions addons/sale_purchase/views/sale_order_views.xml
Original file line number Diff line number Diff line change
@@ -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 changes: 0 additions & 23 deletions addons/sale_purchase/views/sale_views.xml

This file was deleted.

4 changes: 4 additions & 0 deletions addons/sale_purchase_stock/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import models
18 changes: 18 additions & 0 deletions addons/sale_purchase_stock/__manifest__.py
Original file line number Diff line number Diff line change
@@ -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,
}
5 changes: 5 additions & 0 deletions addons/sale_purchase_stock/models/__init__.py
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions addons/sale_purchase_stock/models/purchase_order.py
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions addons/sale_purchase_stock/models/sale_order.py
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions addons/stock/models/stock_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion addons/stock_dropshipping/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
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,
Expand Down
17 changes: 1 addition & 16 deletions addons/stock_dropshipping/models/purchase.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@


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)
Expand All @@ -24,18 +24,3 @@ def _prepare_purchase_order_line_from_procurement(self, product_id, product_qty,
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)
28 changes: 13 additions & 15 deletions addons/stock_dropshipping/models/sale.py
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -31,3 +18,14 @@ def _compute_is_mto(self):
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 changes: 15 additions & 0 deletions addons/stock_dropshipping/models/stock.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down

0 comments on commit 5a1645a

Please sign in to comment.