diff --git a/addons/sale_stock/models/stock.py b/addons/sale_stock/models/stock.py index 9fd4787db8a..9aba0b8203a 100644 --- a/addons/sale_stock/models/stock.py +++ b/addons/sale_stock/models/stock.py @@ -72,6 +72,38 @@ class StockPicking(models.Model): sale_id = fields.Many2one(related="group_id.sale_id", string="Sales Order", store=True, readonly=False) + def _action_done(self): + res = super()._action_done() + sale_order_lines_vals = [] + for move in self.move_lines: + sale_order = move.picking_id.sale_id + # Creates new SO line only when pickings linked to a sale order and + # for moves with qty. done and not already linked to a SO line. + if not sale_order or move.sale_line_id or not move.quantity_done: + continue + product = move.product_id + so_line_vals = { + 'move_ids': [(4, move.id, 0)], + 'name': product.display_name, + 'order_id': sale_order.id, + 'product_id': product.id, + 'product_uom_qty': 0, + 'qty_delivered': move.quantity_done, + } + if product.invoice_policy == 'delivery': + # Check if there is already a SO line for this product to get + # back its unit price (in case it was manually updated). + so_line = sale_order.order_line.filtered(lambda sol: sol.product_id == product) + if so_line: + so_line_vals['price_unit'] = so_line[0].price_unit + elif product.invoice_policy == 'order': + # No unit price if the product is invoiced on the ordered qty. + so_line_vals['price_unit'] = 0 + sale_order_lines_vals.append(so_line_vals) + + if sale_order_lines_vals: + self.env['sale.order.line'].create(sale_order_lines_vals) + return res def _log_less_quantities_than_expected(self, moves): """ Log an activity on sale order that are linked to moves. The diff --git a/addons/sale_stock/tests/test_sale_stock.py b/addons/sale_stock/tests/test_sale_stock.py index bc4f96c083b..a29407bb7d0 100644 --- a/addons/sale_stock/tests/test_sale_stock.py +++ b/addons/sale_stock/tests/test_sale_stock.py @@ -11,21 +11,22 @@ from odoo.tests import Form, tagged @tagged('post_install', '-at_install') class TestSaleStock(TestSaleCommon, ValuationReconciliationTestCommon): - def _get_new_sale_order(self, amount=10.0): + def _get_new_sale_order(self, amount=10.0, product=False): """ Creates and returns a sale order with one default order line. :param float amount: quantity of product for the order line (10 by default) """ + product = product or self.company_data['product_delivery_no'] sale_order_vals = { 'partner_id': self.partner_a.id, 'partner_invoice_id': self.partner_a.id, 'partner_shipping_id': self.partner_a.id, 'order_line': [(0, 0, { - 'name': self.company_data['product_delivery_no'].name, - 'product_id': self.company_data['product_delivery_no'].id, + 'name': product.name, + 'product_id': product.id, 'product_uom_qty': amount, - 'product_uom': self.company_data['product_delivery_no'].uom_id.id, - 'price_unit': self.company_data['product_delivery_no'].list_price})], + 'product_uom': product.uom_id.id, + 'price_unit': product.list_price})], 'pricelist_id': self.company_data['default_pricelist'].id, } sale_order = self.env['sale.order'].create(sale_order_vals) @@ -770,6 +771,88 @@ class TestSaleStock(TestSaleCommon, ValuationReconciliationTestCommon): # Checks the delivery amount (must still be 10). self.assertEqual(sale_order.order_line.qty_delivered, 10) + def test_13_delivered_qty(self): + """ Creates a sale order, valids it and adds a new move line in the delivery for a + product with an invoicing policy on 'order', then checks a new SO line was created. + After that, creates a second sale order and does the same thing but with a product + with and invoicing policy on 'ordered'. + """ + product_inv_on_delivered = self.company_data['product_delivery_no'] + # Configure a product with invoicing policy on order. + product_inv_on_order = self.env['product.product'].create({ + 'name': 'Shenaniffluffy', + 'type': 'consu', + 'invoice_policy': 'order', + 'list_price': 55.0, + }) + # Creates a sale order for 3 products invoiced on qty. delivered. + sale_order = self._get_new_sale_order(amount=3) + # Confirms the sale order, then increases the delivered qty., adds a new + # line and valids the delivery. + sale_order.action_confirm() + self.assertTrue(sale_order.picking_ids) + self.assertEqual(len(sale_order.order_line), 1) + self.assertEqual(sale_order.order_line.qty_delivered, 0) + picking = sale_order.picking_ids + + picking_form = Form(picking) + with picking_form.move_line_ids_without_package.edit(0) as move: + move.qty_done = 5 + with picking_form.move_line_ids_without_package.new() as new_move: + new_move.product_id = product_inv_on_order + new_move.qty_done = 5 + picking = picking_form.save() + picking.button_validate() + + # Check a new sale order line was correctly created. + self.assertEqual(len(sale_order.order_line), 2) + so_line_1 = sale_order.order_line[0] + so_line_2 = sale_order.order_line[1] + self.assertEqual(so_line_1.product_id.id, product_inv_on_delivered.id) + self.assertEqual(so_line_1.product_uom_qty, 3) + self.assertEqual(so_line_1.qty_delivered, 5) + self.assertEqual(so_line_1.price_unit, 70.0) + self.assertEqual(so_line_2.product_id.id, product_inv_on_order.id) + self.assertEqual(so_line_2.product_uom_qty, 0) + self.assertEqual(so_line_2.qty_delivered, 5) + self.assertEqual( + so_line_2.price_unit, 0, + "Shouldn't get the product price as the invoice policy is on qty. ordered") + + # Creates a second sale order for 3 product invoiced on qty. ordered. + sale_order = self._get_new_sale_order(product=product_inv_on_order, amount=3) + # Confirms the sale order, then increases the delivered qty., adds a new + # line and valids the delivery. + sale_order.action_confirm() + self.assertTrue(sale_order.picking_ids) + self.assertEqual(len(sale_order.order_line), 1) + self.assertEqual(sale_order.order_line.qty_delivered, 0) + picking = sale_order.picking_ids + + picking_form = Form(picking) + with picking_form.move_line_ids_without_package.edit(0) as move: + move.qty_done = 5 + with picking_form.move_line_ids_without_package.new() as new_move: + new_move.product_id = product_inv_on_delivered + new_move.qty_done = 5 + picking = picking_form.save() + picking.button_validate() + + # Check a new sale order line was correctly created. + self.assertEqual(len(sale_order.order_line), 2) + so_line_1 = sale_order.order_line[0] + so_line_2 = sale_order.order_line[1] + self.assertEqual(so_line_1.product_id.id, product_inv_on_order.id) + self.assertEqual(so_line_1.product_uom_qty, 3) + self.assertEqual(so_line_1.qty_delivered, 5) + self.assertEqual(so_line_1.price_unit, 55.0) + self.assertEqual(so_line_2.product_id.id, product_inv_on_delivered.id) + self.assertEqual(so_line_2.product_uom_qty, 0) + self.assertEqual(so_line_2.qty_delivered, 5) + self.assertEqual( + so_line_2.price_unit, 70.0, + "Should get the product price as the invoice policy is on qty. delivered") + def test_08_sale_return_qty_and_cancel(self): """ Test a SO with a product on delivery with a 5 quantity.