From b139cc1c164308028610a7c903a06dc0e44a62eb Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Sat, 31 Oct 2020 08:48:59 -0700 Subject: [PATCH] [MIG] sale_sourced_by_line: to Odoo 13.0 --- sale_sourced_by_line/__manifest__.py | 2 +- sale_sourced_by_line/models/sale.py | 62 +++++++++++++++++- .../tests/test_sale_sources.py | 64 +++++++++++++++---- 3 files changed, 113 insertions(+), 15 deletions(-) diff --git a/sale_sourced_by_line/__manifest__.py b/sale_sourced_by_line/__manifest__.py index 5caeeef6..ba372277 100644 --- a/sale_sourced_by_line/__manifest__.py +++ b/sale_sourced_by_line/__manifest__.py @@ -1,7 +1,7 @@ { 'name': 'Sale Sourced by Line', 'summary': 'Multiple warehouse source locations for Sale order', - 'version': '12.0.1.0.0', + 'version': '13.0.1.0.0', 'author': "Hibou Corp.,Odoo Community Association (OCA)", 'category': 'Warehouse', 'license': 'AGPL-3', diff --git a/sale_sourced_by_line/models/sale.py b/sale_sourced_by_line/models/sale.py index e9376a4f..29c903ad 100644 --- a/sale_sourced_by_line/models/sale.py +++ b/sale_sourced_by_line/models/sale.py @@ -1,3 +1,4 @@ +from collections import defaultdict from odoo import api, fields, models @@ -11,10 +12,12 @@ class SaleOrder(models.Model): class SaleOrderLine(models.Model): _inherit = 'sale.order.line' - warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse') + # In 13, this field exists, but isn't stored and is computed during + # computation for available inventory (set to order's warehouse) + warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse', + compute=None, store=True) date_planned = fields.Datetime('Planned Date') - @api.multi def _prepare_procurement_values(self, group_id=False): vals = super(SaleOrderLine, self)._prepare_procurement_values(group_id=group_id) if self.warehouse_id: @@ -24,3 +27,58 @@ class SaleOrderLine(models.Model): elif self.order_id.date_planned: vals.update({'date_planned': self.order_id.date_planned}) return vals + + # Needs modifications to not actually set a warehouse on the line as it is now stored + @api.depends('product_id', 'customer_lead', 'product_uom_qty', 'product_uom', 'order_id.warehouse_id', 'order_id.commitment_date') + def _compute_qty_at_date(self): + """ Compute the quantity forecasted of product at delivery date. There are + two cases: + 1. The quotation has a commitment_date, we take it as delivery date + 2. The quotation hasn't commitment_date, we compute the estimated delivery + date based on lead time""" + qty_processed_per_product = defaultdict(lambda: 0) + grouped_lines = defaultdict(lambda: self.env['sale.order.line']) + # We first loop over the SO lines to group them by warehouse and schedule + # date in order to batch the read of the quantities computed field. + for line in self: + if not (line.product_id and line.display_qty_widget): + continue + # use warehouse from line or order + warehouse = line.warehouse_id or line.order_id.warehouse_id + # line.warehouse_id = line.order_id.warehouse_id + if line.order_id.commitment_date: + date = line.order_id.commitment_date + else: + date = line._expected_date() + grouped_lines[(warehouse, date)] |= line + + treated = self.browse() + for (warehouse, scheduled_date), lines in grouped_lines.items(): + product_qties = lines.mapped('product_id').with_context(to_date=scheduled_date, warehouse=warehouse).read([ + 'qty_available', + 'free_qty', + 'virtual_available', + ]) + qties_per_product = { + product['id']: (product['qty_available'], product['free_qty'], product['virtual_available']) + for product in product_qties + } + for line in lines: + line.scheduled_date = scheduled_date + qty_available_today, free_qty_today, virtual_available_at_date = qties_per_product[line.product_id.id] + line.qty_available_today = qty_available_today - qty_processed_per_product[line.product_id.id] + line.free_qty_today = free_qty_today - qty_processed_per_product[line.product_id.id] + line.virtual_available_at_date = virtual_available_at_date - qty_processed_per_product[line.product_id.id] + if line.product_uom and line.product_id.uom_id and line.product_uom != line.product_id.uom_id: + line.qty_available_today = line.product_id.uom_id._compute_quantity(line.qty_available_today, line.product_uom) + line.free_qty_today = line.product_id.uom_id._compute_quantity(line.free_qty_today, line.product_uom) + line.virtual_available_at_date = line.product_id.uom_id._compute_quantity(line.virtual_available_at_date, line.product_uom) + qty_processed_per_product[line.product_id.id] += line.product_uom_qty + treated |= lines + remaining = (self - treated) + remaining.virtual_available_at_date = False + remaining.scheduled_date = False + remaining.free_qty_today = False + remaining.qty_available_today = False + # don't unset warehouse as it may be set by hand + # remaining.warehouse_id = False diff --git a/sale_sourced_by_line/tests/test_sale_sources.py b/sale_sourced_by_line/tests/test_sale_sources.py index 2038d24a..76f91c47 100644 --- a/sale_sourced_by_line/tests/test_sale_sources.py +++ b/sale_sourced_by_line/tests/test_sale_sources.py @@ -3,21 +3,61 @@ from odoo.tests import common class TestSaleSources(common.TransactionCase): - def test_plan_two_warehouses(self): - partner = self.env.ref('base.res_partner_2') - product_1 = self.env.ref('product.product_product_24_product_template') - product_2 = self.env.ref('product.product_product_16_product_template') - wh_1 = self.env.ref('stock.stock_warehouse_shop0') - wh_2 = self.env.ref('stock.warehouse0') + def setUp(self): + super(TestSaleSources, self).setUp() + self.partner = self.env.ref('base.res_partner_2') + self.product_1 = self.env['product.product'].create({ + 'type': 'consu', + 'name': 'Test Product 1', + }) + self.product_2 = self.env['product.product'].create({ + 'type': 'consu', + 'name': 'Test Product 2', + }) + self.wh_1 = self.env.ref('stock.warehouse0') + self.wh_2 = self.env['stock.warehouse'].create({ + 'name': 'Test WH2', + 'code': 'TWH2', + }) + + def test_plan_one_warehouse(self): so = self.env['sale.order'].create({ - 'warehouse_id': wh_1.id, - 'partner_id': partner.id, + 'warehouse_id': self.wh_1.id, + 'partner_id': self.partner.id, 'date_planned': '2018-01-01', - 'order_line': [(0, 0, {'product_id': product_1.product_variant_id.id}), - (0, 0, {'product_id': product_2.product_variant_id.id, 'date_planned': '2018-02-01', 'warehouse_id': wh_2.id})] + 'order_line': [(0, 0, { + 'product_id': self.product_1.id, + 'product_uom_qty': 1.0, + 'product_uom': self.product_1.uom_id.id, + 'price_unit': 10.0, + }), + (0, 0, { + 'product_id': self.product_2.id, + 'product_uom_qty': 1.0, + 'product_uom': self.product_2.uom_id.id, + 'price_unit': 10.0, + })] }) so.action_confirm() self.assertTrue(so.state in ('sale', 'done')) + self.assertEqual(len(so.picking_ids), 1) + self.assertEqual(len(so.picking_ids.filtered(lambda p: p.picking_type_id.warehouse_id == self.wh_1)), 1) + self.assertEqual(len(so.picking_ids.filtered(lambda p: p.picking_type_id.warehouse_id == self.wh_2)), 0) + + + def test_plan_two_warehouses(self): + so = self.env['sale.order'].create({ + 'warehouse_id': self.wh_1.id, + 'partner_id': self.partner.id, + 'date_planned': '2018-01-01', + 'order_line': [(0, 0, {'product_id': self.product_1.id}), + (0, 0, {'product_id': self.product_2.id, + 'date_planned': '2018-02-01', 'warehouse_id': self.wh_2.id})] + }) + # in 13 default computation, this would result in a failure + self.assertTrue(so.order_line.filtered(lambda l: l.warehouse_id)) + so.action_confirm() + self.assertTrue(so.state in ('sale', 'done')) self.assertEqual(len(so.picking_ids), 2) - self.assertEqual(len(so.picking_ids.filtered(lambda p: p.picking_type_id.warehouse_id == wh_1)), 1) - self.assertEqual(len(so.picking_ids.filtered(lambda p: p.picking_type_id.warehouse_id == wh_2)), 1) + self.assertEqual(len(so.picking_ids.filtered(lambda p: p.picking_type_id.warehouse_id == self.wh_1)), 1) + self.assertEqual(len(so.picking_ids.filtered(lambda p: p.picking_type_id.warehouse_id == self.wh_2)), 1)