From 5a1645a8f8f3560eb778da90b6160b322ce0722e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Voet=20=28ryv=29?= Date: Tue, 7 Jan 2020 15:41:15 +0000 Subject: [PATCH] [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 --- addons/sale_purchase/__manifest__.py | 3 +- addons/sale_purchase/models/purchase_order.py | 35 ++++++++++++++++- addons/sale_purchase/models/sale_order.py | 38 +++++++++++++------ .../sale_purchase/tests/test_access_rights.py | 6 +-- .../views/purchase_order_views.xml | 18 +++++++++ .../sale_purchase/views/sale_order_views.xml | 18 +++++++++ addons/sale_purchase/views/sale_views.xml | 23 ----------- addons/sale_purchase_stock/__init__.py | 4 ++ addons/sale_purchase_stock/__manifest__.py | 18 +++++++++ addons/sale_purchase_stock/models/__init__.py | 5 +++ .../models/purchase_order.py | 15 ++++++++ .../sale_purchase_stock/models/sale_order.py | 15 ++++++++ addons/stock/models/stock_rule.py | 1 + addons/stock_dropshipping/__manifest__.py | 2 +- addons/stock_dropshipping/models/purchase.py | 17 +-------- addons/stock_dropshipping/models/sale.py | 28 +++++++------- addons/stock_dropshipping/models/stock.py | 15 ++++++++ 17 files changed, 190 insertions(+), 71 deletions(-) create mode 100644 addons/sale_purchase/views/purchase_order_views.xml create mode 100644 addons/sale_purchase/views/sale_order_views.xml delete mode 100644 addons/sale_purchase/views/sale_views.xml create mode 100644 addons/sale_purchase_stock/__init__.py create mode 100644 addons/sale_purchase_stock/__manifest__.py create mode 100644 addons/sale_purchase_stock/models/__init__.py create mode 100644 addons/sale_purchase_stock/models/purchase_order.py create mode 100644 addons/sale_purchase_stock/models/sale_order.py diff --git a/addons/sale_purchase/__manifest__.py b/addons/sale_purchase/__manifest__.py index 7fcf2bb81e5..675fc332e76 100644 --- a/addons/sale_purchase/__manifest__.py +++ b/addons/sale_purchase/__manifest__.py @@ -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': [ ], diff --git a/addons/sale_purchase/models/purchase_order.py b/addons/sale_purchase/models/purchase_order.py index 5d4550ed724..8684912c7fc 100644 --- a/addons/sale_purchase/models/purchase_order.py +++ b/addons/sale_purchase/models/purchase_order.py @@ -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. diff --git a/addons/sale_purchase/models/sale_order.py b/addons/sale_purchase/models/sale_order.py index 1f61eb6f827..9eb6ef55849 100644 --- a/addons/sale_purchase/models/sale_order.py +++ b/addons/sale_purchase/models/sale_order.py @@ -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. diff --git a/addons/sale_purchase/tests/test_access_rights.py b/addons/sale_purchase/tests/test_access_rights.py index d21337fb7a3..3fcb7c95a28 100644 --- a/addons/sale_purchase/tests/test_access_rights.py +++ b/addons/sale_purchase/tests/test_access_rights.py @@ -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 diff --git a/addons/sale_purchase/views/purchase_order_views.xml b/addons/sale_purchase/views/purchase_order_views.xml new file mode 100644 index 00000000000..69d66832d97 --- /dev/null +++ b/addons/sale_purchase/views/purchase_order_views.xml @@ -0,0 +1,18 @@ + + + + purchase.order.inherited.form.sale + purchase.order + + + + + + + + diff --git a/addons/sale_purchase/views/sale_order_views.xml b/addons/sale_purchase/views/sale_order_views.xml new file mode 100644 index 00000000000..9722bb49325 --- /dev/null +++ b/addons/sale_purchase/views/sale_order_views.xml @@ -0,0 +1,18 @@ + + + + sale.order.inherited.form.purchase + sale.order + + + + + + + + diff --git a/addons/sale_purchase/views/sale_views.xml b/addons/sale_purchase/views/sale_views.xml deleted file mode 100644 index 0a38b02a54c..00000000000 --- a/addons/sale_purchase/views/sale_views.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - sale.order.form.inherit - sale.order - - - - - - - - - - diff --git a/addons/sale_purchase_stock/__init__.py b/addons/sale_purchase_stock/__init__.py new file mode 100644 index 00000000000..dc5e6b693d1 --- /dev/null +++ b/addons/sale_purchase_stock/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/addons/sale_purchase_stock/__manifest__.py b/addons/sale_purchase_stock/__manifest__.py new file mode 100644 index 00000000000..77a2672de9a --- /dev/null +++ b/addons/sale_purchase_stock/__manifest__.py @@ -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, +} diff --git a/addons/sale_purchase_stock/models/__init__.py b/addons/sale_purchase_stock/models/__init__.py new file mode 100644 index 00000000000..9ed264227ce --- /dev/null +++ b/addons/sale_purchase_stock/models/__init__.py @@ -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 diff --git a/addons/sale_purchase_stock/models/purchase_order.py b/addons/sale_purchase_stock/models/purchase_order.py new file mode 100644 index 00000000000..59738a97d85 --- /dev/null +++ b/addons/sale_purchase_stock/models/purchase_order.py @@ -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 diff --git a/addons/sale_purchase_stock/models/sale_order.py b/addons/sale_purchase_stock/models/sale_order.py new file mode 100644 index 00000000000..0a8522e9892 --- /dev/null +++ b/addons/sale_purchase_stock/models/sale_order.py @@ -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 diff --git a/addons/stock/models/stock_rule.py b/addons/stock/models/stock_rule.py index 99433257da1..8768173a7bb 100644 --- a/addons/stock/models/stock_rule.py +++ b/addons/stock/models/stock_rule.py @@ -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): diff --git a/addons/stock_dropshipping/__manifest__.py b/addons/stock_dropshipping/__manifest__.py index a9b49bcf25d..1d8780f3f12 100644 --- a/addons/stock_dropshipping/__manifest__.py +++ b/addons/stock_dropshipping/__manifest__.py @@ -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, diff --git a/addons/stock_dropshipping/models/purchase.py b/addons/stock_dropshipping/models/purchase.py index 58dda421edc..faf50652e63 100644 --- a/addons/stock_dropshipping/models/purchase.py +++ b/addons/stock_dropshipping/models/purchase.py @@ -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) diff --git a/addons/stock_dropshipping/models/sale.py b/addons/stock_dropshipping/models/sale.py index 53c471e19f4..520e7b53b37 100644 --- a/addons/stock_dropshipping/models/sale.py +++ b/addons/stock_dropshipping/models/sale.py @@ -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) diff --git a/addons/stock_dropshipping/models/stock.py b/addons/stock_dropshipping/models/stock.py index e18004815b8..c44fe2ccbd3 100644 --- a/addons/stock_dropshipping/models/stock.py +++ b/addons/stock_dropshipping/models/stock.py @@ -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"