From 61300f6817cabf1f3f57f96c4aa30b049eeeffe4 Mon Sep 17 00:00:00 2001 From: svs-odoo Date: Wed, 15 Jul 2020 08:31:52 +0000 Subject: [PATCH] [IMP] sale_stock: extend replenishment report Adds Sale Order as consuming document for the report. Also, make the SO line `warehouse_id` field related to SO warehouse. task-2058495 --- addons/sale_stock/__manifest__.py | 1 + addons/sale_stock/models/sale_order.py | 6 +- addons/sale_stock/models/stock.py | 4 ++ addons/sale_stock/report/__init__.py | 1 + .../report/report_stock_forecasted.py | 32 +++++++++++ .../report/report_stock_forecasted.xml | 12 ++++ addons/sale_stock/tests/__init__.py | 1 + .../tests/test_sale_stock_report.py | 57 +++++++++++++++++++ 8 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 addons/sale_stock/report/report_stock_forecasted.py create mode 100644 addons/sale_stock/report/report_stock_forecasted.xml create mode 100644 addons/sale_stock/tests/test_sale_stock_report.py diff --git a/addons/sale_stock/__manifest__.py b/addons/sale_stock/__manifest__.py index 0992ac081e0..638753a08d0 100644 --- a/addons/sale_stock/__manifest__.py +++ b/addons/sale_stock/__manifest__.py @@ -30,6 +30,7 @@ Preferences 'views/stock_production_lot_views.xml', 'views/report_invoice.xml', 'views/res_users_views.xml', + 'report/report_stock_forecasted.xml', 'report/sale_order_report_templates.xml', 'report/stock_report_deliveryslip.xml', 'data/sale_stock_data.xml', diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index fde99c982c2..aa4a7dc2879 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -263,7 +263,7 @@ class SaleOrderLine(models.Model): scheduled_date = fields.Datetime(compute='_compute_qty_at_date') free_qty_today = fields.Float(compute='_compute_qty_at_date') qty_available_today = fields.Float(compute='_compute_qty_at_date') - warehouse_id = fields.Many2one('stock.warehouse', compute='_compute_qty_at_date') + warehouse_id = fields.Many2one(related='order_id.warehouse_id') qty_to_deliver = fields.Float(compute='_compute_qty_to_deliver') is_mto = fields.Boolean(compute='_compute_is_mto') display_qty_widget = fields.Boolean(compute='_compute_qty_to_deliver') @@ -278,7 +278,7 @@ class SaleOrderLine(models.Model): else: line.display_qty_widget = False - @api.depends('product_id', 'customer_lead', 'product_uom_qty', 'product_uom', 'order_id.warehouse_id', 'order_id.commitment_date') + @api.depends('product_id', 'customer_lead', 'product_uom_qty', 'product_uom', 'order_id.commitment_date') def _compute_qty_at_date(self): """ Compute the quantity forecasted of product at delivery date. There are two cases: @@ -292,7 +292,6 @@ class SaleOrderLine(models.Model): for line in self: if not line.display_qty_widget: continue - line.warehouse_id = line.order_id.warehouse_id if line.order_id.commitment_date: date = line.order_id.commitment_date else: @@ -327,7 +326,6 @@ class SaleOrderLine(models.Model): remaining.scheduled_date = False remaining.free_qty_today = False remaining.qty_available_today = False - remaining.warehouse_id = False @api.depends('product_id', 'route_id', 'order_id.warehouse_id', 'product_id.route_ids') def _compute_is_mto(self): diff --git a/addons/sale_stock/models/stock.py b/addons/sale_stock/models/stock.py index 9b679944502..e1d36c38f7d 100644 --- a/addons/sale_stock/models/stock.py +++ b/addons/sale_stock/models/stock.py @@ -36,6 +36,10 @@ class StockMove(models.Model): #rslt += invoices.mapped('reverse_entry_ids') return rslt + def _get_source_document(self): + res = super()._get_source_document() + return self.sale_line_id.order_id or res + def _assign_picking_post_process(self, new=False): super(StockMove, self)._assign_picking_post_process(new=new) if new: diff --git a/addons/sale_stock/report/__init__.py b/addons/sale_stock/report/__init__.py index 660a07d84e2..4109cd49093 100644 --- a/addons/sale_stock/report/__init__.py +++ b/addons/sale_stock/report/__init__.py @@ -2,4 +2,5 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import sale_report +from . import report_stock_forecasted from . import report_stock_rule diff --git a/addons/sale_stock/report/report_stock_forecasted.py b/addons/sale_stock/report/report_stock_forecasted.py new file mode 100644 index 00000000000..be66eed9e3a --- /dev/null +++ b/addons/sale_stock/report/report_stock_forecasted.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class ReplenishmentReport(models.AbstractModel): + _inherit = 'report.stock.report_product_product_replenishment' + + def _compute_draft_quantity_count(self, product_template_ids, product_variant_ids, wh_location_ids): + res = super()._compute_draft_quantity_count(product_template_ids, product_variant_ids, wh_location_ids) + domain = self._product_sale_domain(product_template_ids, product_variant_ids) + so_lines = self.env['sale.order.line'].search(domain) + out_sum = 0 + if so_lines: + product_uom = so_lines[0].product_id.uom_id + quantities = so_lines.mapped(lambda line: line.product_uom._compute_quantity(line.product_uom_qty, product_uom)) + out_sum = sum(quantities) + res['draft_sale_qty'] = out_sum + res['qty']['out'] += out_sum + return res + + def _product_sale_domain(self, product_template_ids, product_variant_ids): + domain = [('state', 'in', ['draft', 'sent'])] + if product_template_ids: + domain += [('product_template_id', 'in', product_template_ids)] + elif product_variant_ids: + domain += [('product_id', 'in', product_variant_ids)] + warehouse_id = self.env.context.get('warehouse', False) + if warehouse_id: + domain += [('warehouse_id', '=', warehouse_id)] + return domain diff --git a/addons/sale_stock/report/report_stock_forecasted.xml b/addons/sale_stock/report/report_stock_forecasted.xml new file mode 100644 index 00000000000..9f43b615fc0 --- /dev/null +++ b/addons/sale_stock/report/report_stock_forecasted.xml @@ -0,0 +1,12 @@ + + + + diff --git a/addons/sale_stock/tests/__init__.py b/addons/sale_stock/tests/__init__.py index c9c90165757..f4f97c3a04a 100644 --- a/addons/sale_stock/tests/__init__.py +++ b/addons/sale_stock/tests/__init__.py @@ -5,5 +5,6 @@ from . import test_anglo_saxon_valuation from . import test_anglo_saxon_valuation_reconciliation from . import test_sale_stock from . import test_sale_stock_lead_time +from . import test_sale_stock_report from . import test_sale_order_dates from . import test_sale_stock_multicompany diff --git a/addons/sale_stock/tests/test_sale_stock_report.py b/addons/sale_stock/tests/test_sale_stock_report.py new file mode 100644 index 00000000000..a132a844dcd --- /dev/null +++ b/addons/sale_stock/tests/test_sale_stock_report.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime, timedelta + +from odoo.tests.common import Form +from odoo.addons.stock.tests.test_report import TestReportsCommon + + +class TestSaleStockReports(TestReportsCommon): + def test_report_forecast_1_sale_order_replenishment(self): + """ Create and confirm two sale orders: one for the next week and one + for tomorrow. Then check in the report it's the most urgent who is + linked to the qty. on stock. + """ + today = datetime.today() + # Put some quantity in stock. + quant_vals = { + 'product_id': self.product.id, + 'product_uom_id': self.product.uom_id.id, + 'location_id': self.stock_location.id, + 'quantity': 5, + 'reserved_quantity': 0, + } + self.env['stock.quant'].create(quant_vals) + # Create a first SO for the next week. + so_form = Form(self.env['sale.order']) + so_form.partner_id = self.partner + # so_form.validity_date = today + timedelta(days=7) + with so_form.order_line.new() as so_line: + so_line.product_id = self.product + so_line.product_uom_qty = 5 + so_1 = so_form.save() + so_1.action_confirm() + so_1.picking_ids.scheduled_date = today + timedelta(days=7) + + # Create a second SO for tomorrow. + so_form = Form(self.env['sale.order']) + so_form.partner_id = self.partner + # so_form.validity_date = today + timedelta(days=1) + with so_form.order_line.new() as so_line: + so_line.product_id = self.product + so_line.product_uom_qty = 5 + so_2 = so_form.save() + so_2.action_confirm() + so_2.picking_ids.scheduled_date = today + timedelta(days=1) + + report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids) + self.assertEqual(len(lines), 2) + line_1 = lines[0] + line_2 = lines[1] + self.assertEqual(line_1['quantity'], 5) + self.assertTrue(line_1['replenishment_filled']) + self.assertEqual(line_1['document_out'].id, so_2.id) + self.assertEqual(line_2['quantity'], 5) + self.assertEqual(line_2['replenishment_filled'], False) + self.assertEqual(line_2['document_out'].id, so_1.id)