diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 4d1ad503c31..c93d04b8abf 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -542,10 +542,11 @@ class PurchaseOrderLine(models.Model): total = 0.0 for move in line.move_ids: if move.state == 'done': - if move.product_uom != line.product_uom: - total += move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom) + if move.location_dest_id.usage == "supplier": + if move.to_refund: + total -= move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom) else: - total += move.product_uom_qty + total += move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom) line.qty_received = total @api.model @@ -639,7 +640,7 @@ class PurchaseOrderLine(models.Model): return res qty = 0.0 price_unit = self._get_stock_move_price_unit() - for move in self.move_ids.filtered(lambda x: x.state != 'cancel'): + for move in self.move_ids.filtered(lambda x: x.state != 'cancel' and not x.location_dest_id.usage == "supplier"): qty += move.product_qty template = { 'name': self.name or '', diff --git a/addons/purchase/models/stock.py b/addons/purchase/models/stock.py index e3e66104591..3bfc0877f74 100644 --- a/addons/purchase/models/stock.py +++ b/addons/purchase/models/stock.py @@ -60,8 +60,10 @@ class StockMove(models.Model): def copy(self, default=None): self.ensure_one() default = default or {} - if not default.get('split_from'): - #we don't want to propagate the link to the purchase order line except in case of move split + # we don't want to propagate the link to the purchase order line on the move copied, + # except when it's a split or a returned move + + if not default.get('split_from') and not default.get('origin_returned_move_id'): default['purchase_line_id'] = False return super(StockMove, self).copy(default) diff --git a/addons/purchase/tests/test_purchase_order.py b/addons/purchase/tests/test_purchase_order.py index a7b83a0e237..42c67a62b63 100644 --- a/addons/purchase/tests/test_purchase_order.py +++ b/addons/purchase/tests/test_purchase_order.py @@ -16,23 +16,12 @@ class TestPurchaseOrder(AccountingTestCase): self.PurchaseOrderLine = self.env['purchase.order.line'] self.AccountInvoice = self.env['account.invoice'] self.AccountInvoiceLine = self.env['account.invoice.line'] - - def test_00_purchase_order_flow(self): self.partner_id = self.env.ref('base.res_partner_1') self.product_id_1 = self.env.ref('product.product_product_8') self.product_id_2 = self.env.ref('product.product_product_11') (self.product_id_1 | self.product_id_2).write({'purchase_method': 'purchase'}) - # Ensure product_id_2 doesn't have res_partner_1 as supplier - if self.partner_id in self.product_id_2.seller_ids.mapped('name'): - id_to_remove = self.product_id_2.seller_ids.filtered(lambda r: r.name == self.partner_id).ids[0] if self.product_id_2.seller_ids.filtered(lambda r: r.name == self.partner_id) else False - if id_to_remove: - self.product_id_2.write({ - 'seller_ids': [(2, id_to_remove, False)], - }) - self.assertFalse(self.product_id_2.seller_ids.filtered(lambda r: r.name == self.partner_id), 'Purchase: the partner should not be in the list of the product suppliers') - - po_vals = { + self.po_vals = { 'partner_id': self.partner_id.id, 'order_line': [ (0, 0, { @@ -52,7 +41,20 @@ class TestPurchaseOrder(AccountingTestCase): 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT), })], } - self.po = self.PurchaseOrder.create(po_vals) + + + def test_00_purchase_order_flow(self): + + # Ensure product_id_2 doesn't have res_partner_1 as supplier + if self.partner_id in self.product_id_2.seller_ids.mapped('name'): + id_to_remove = self.product_id_2.seller_ids.filtered(lambda r: r.name == self.partner_id).ids[0] if self.product_id_2.seller_ids.filtered(lambda r: r.name == self.partner_id) else False + if id_to_remove: + self.product_id_2.write({ + 'seller_ids': [(2, id_to_remove, False)], + }) + self.assertFalse(self.product_id_2.seller_ids.filtered(lambda r: r.name == self.partner_id), 'Purchase: the partner should not be in the list of the product suppliers') + + self.po = self.PurchaseOrder.create(self.po_vals) self.assertTrue(self.po, 'Purchase: no purchase order created') self.assertEqual(self.po.invoice_status, 'no', 'Purchase: PO invoice_status should be "Not purchased"') self.assertEqual(self.po.order_line.mapped('qty_received'), [0.0, 0.0], 'Purchase: no product should be received"') @@ -85,3 +87,72 @@ class TestPurchaseOrder(AccountingTestCase): }) self.invoice.purchase_order_change() self.assertEqual(self.po.order_line.mapped('qty_invoiced'), [5.0, 5.0], 'Purchase: all products should be invoiced"') + + def test_02_po_return(self): + """ + Test a PO with a product on Incoming shipment. Validate the PO, then do a return + of the picking with Refund. + """ + # Draft purchase order created + self.po = self.env['purchase.order'].create(self.po_vals) + self.assertTrue(self.po, 'Purchase: no purchase order created') + self.assertEqual(self.po.order_line.mapped('qty_received'), [0.0, 0.0], 'Purchase: no product should be received"') + self.assertEqual(self.po.order_line.mapped('qty_invoiced'), [0.0, 0.0], 'Purchase: no product should be invoiced"') + + self.po.button_confirm() + self.assertEqual(self.po.state, 'purchase', 'Purchase: PO state should be "Purchase"') + self.assertEqual(self.po.invoice_status, 'to invoice', 'Purchase: PO invoice_status should be "Waiting Invoices"') + + # Confirm the purchase order + self.po.button_confirm() + self.assertEqual(self.po.state, 'purchase', 'Purchase: PO state should be "Purchase') + self.assertEqual(self.po.picking_count, 1, 'Purchase: one picking should be created"') + self.picking = self.po.picking_ids[0] + self.picking.force_assign() + self.picking.pack_operation_product_ids.write({'qty_done': 5.0}) + self.picking.do_new_transfer() + self.assertEqual(self.po.order_line.mapped('qty_received'), [5.0, 5.0], 'Purchase: all products should be received"') + + #After Receiving all products create vendor bill. + self.invoice = self.AccountInvoice.create({ + 'partner_id': self.partner_id.id, + 'purchase_id': self.po.id, + 'account_id': self.partner_id.property_account_payable_id.id, + 'type': 'in_invoice', + }) + self.invoice.purchase_order_change() + self.invoice.invoice_validate() + self.assertEqual(self.po.order_line.mapped('qty_invoiced'), [5.0, 5.0], 'Purchase: all products should be invoiced"') + + # Check quantity received + received_qty = sum(pol.qty_received for pol in self.po.order_line) + self.assertEqual(received_qty, 10.0, 'Purchase: Received quantity should be 10.0 instead of %s after validating incomming shipment' % received_qty) + + # Create return picking + StockReturnPicking = self.env['stock.return.picking'] + pick = self.po.picking_ids + default_data = StockReturnPicking.with_context(active_ids=pick.ids, active_id=pick.ids[0]).default_get(['move_dest_exists', 'original_location_id', 'product_return_moves', 'parent_location_id', 'location_id']) + return_wiz = StockReturnPicking.with_context(active_ids=pick.ids, active_id=pick.ids[0]).create(default_data) + return_wiz.product_return_moves.write({'quantity': 2.0, 'to_refund': True}) # Return only 2 + res = return_wiz.create_returns() + return_pick = self.env['stock.picking'].browse(res['res_id']) + + # Validate picking + return_pick.force_assign() + return_pick.pack_operation_product_ids.write({'qty_done': 2}) + return_pick.do_new_transfer() + + # Check Received quantity + self.assertEqual(self.po.order_line[0].qty_received, 3.0, 'Purchase: delivered quantity should be 3.0 instead of "%s" after picking return' % self.po.order_line[0].qty_received) + #Create vendor bill for refund qty + self.invoice = self.AccountInvoice.create({ + 'partner_id': self.partner_id.id, + 'purchase_id': self.po.id, + 'account_id': self.partner_id.property_account_payable_id.id, + 'type': 'in_refund', + }) + self.invoice.purchase_order_change() + self.invoice.invoice_line_ids[0].quantity = 2.0 + self.invoice.invoice_line_ids[1].quantity = 2.0 + self.invoice.invoice_validate() + self.assertEqual(self.po.order_line.mapped('qty_invoiced'), [3.0, 3.0], 'Purchase: Billed quantity should be 3.0') diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index 313d7e1afaa..9fcf8471fe7 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -192,7 +192,7 @@ class SaleOrderLine(models.Model): if move.location_dest_id.usage == "customer": if not move.origin_returned_move_id: qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom) - elif move.location_dest_id.usage == "internal" and move.to_refund_so: + elif move.location_dest_id.usage == "internal" and move.to_refund: qty -= move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom) return qty diff --git a/addons/sale_stock/models/stock.py b/addons/sale_stock/models/stock.py index 41d1c3b0d13..2b571f5df0a 100644 --- a/addons/sale_stock/models/stock.py +++ b/addons/sale_stock/models/stock.py @@ -13,10 +13,6 @@ class StockLocationRoute(models.Model): class StockMove(models.Model): _inherit = "stock.move" - to_refund_so = fields.Boolean( - "To Refund in SO", default=False, - help='Trigger a decrease of the delivered quantity in the associated Sales Order') - @api.multi def action_done(self): result = super(StockMove, self).action_done() @@ -66,24 +62,3 @@ class StockPicking(models.Model): values={'self': backorder, 'origin': order}, subtype_id=self.env.ref('mail.mt_note').id) return res - - -class StockReturnPicking(models.TransientModel): - _inherit = "stock.return.picking" - - @api.multi - def _create_returns(self): - new_picking_id, pick_type_id = super(StockReturnPicking, self)._create_returns() - new_picking = self.env['stock.picking'].browse([new_picking_id]) - for move in new_picking.move_lines: - return_picking_line = self.product_return_moves.filtered(lambda r: r.move_id == move.origin_returned_move_id) - if return_picking_line and return_picking_line.to_refund_so: - move.to_refund_so = True - - return new_picking_id, pick_type_id - - -class StockReturnPickingLine(models.TransientModel): - _inherit = "stock.return.picking.line" - - to_refund_so = fields.Boolean(string="To Refund", help='Trigger a decrease of the delivered quantity in the associated Sales Order') diff --git a/addons/sale_stock/tests/test_sale_stock.py b/addons/sale_stock/tests/test_sale_stock.py index ab39a0d2129..6b71bb07975 100644 --- a/addons/sale_stock/tests/test_sale_stock.py +++ b/addons/sale_stock/tests/test_sale_stock.py @@ -161,7 +161,7 @@ class TestSaleStock(TestSale): default_data = StockReturnPicking.with_context(active_ids=pick.ids, active_id=pick.ids[0]).default_get(['move_dest_exists', 'original_location_id', 'product_return_moves', 'parent_location_id', 'location_id']) return_wiz = StockReturnPicking.with_context(active_ids=pick.ids, active_id=pick.ids[0]).create(default_data) return_wiz.product_return_moves.quantity = 2.0 # Return only 2 - return_wiz.product_return_moves.to_refund_so = True # Refund these 2 + return_wiz.product_return_moves.to_refund = True # Refund these 2 res = return_wiz.create_returns() return_pick = self.env['stock.picking'].browse(res['res_id']) diff --git a/addons/sale_stock/views/stock_views.xml b/addons/sale_stock/views/stock_views.xml index 2d6828b88d4..4dd032e1519 100644 --- a/addons/sale_stock/views/stock_views.xml +++ b/addons/sale_stock/views/stock_views.xml @@ -23,16 +23,5 @@ - - stock.return.picking.sale.stock.form - - stock.return.picking - - - - - - - diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index 9fec58c20ab..822057e5d69 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -176,6 +176,9 @@ class StockQuant(models.Model): class StockMove(models.Model): _inherit = "stock.move" + to_refund = fields.Boolean(string="To Refund", + help='Trigger a decrease of the delivered/received quantity in the associated Sale Order/Purchase Order') + @api.multi def action_done(self): self.product_price_update_before_done() @@ -346,3 +349,24 @@ class StockMove(models.Model): } res.append((0, 0, price_diff_line)) return res + + +class StockReturnPicking(models.TransientModel): + _inherit = "stock.return.picking" + + @api.multi + def _create_returns(self): + new_picking_id, pick_type_id = super(StockReturnPicking, self)._create_returns() + new_picking = self.env['stock.picking'].browse([new_picking_id]) + for move in new_picking.move_lines: + return_picking_line = self.product_return_moves.filtered(lambda r: r.move_id == move.origin_returned_move_id) + if return_picking_line and return_picking_line.to_refund: + move.to_refund = True + + return new_picking_id, pick_type_id + + +class StockReturnPickingLine(models.TransientModel): + _inherit = "stock.return.picking.line" + + to_refund = fields.Boolean(string="To Refund", help='Trigger a decrease of the delivered/received quantity in the associated Sale Order/Purchase Order') diff --git a/addons/stock_account/views/stock_account_views.xml b/addons/stock_account/views/stock_account_views.xml index bd817ab8bd3..688646ba89d 100644 --- a/addons/stock_account/views/stock_account_views.xml +++ b/addons/stock_account/views/stock_account_views.xml @@ -38,5 +38,16 @@ + + stock.return.picking.stock.account.form + + stock.return.picking + + + + + + +