From 49c159a9060af25b49669d3b18079a95fac6229b Mon Sep 17 00:00:00 2001 From: Srushti Patel Date: Fri, 30 Sep 2016 11:30:32 +0530 Subject: [PATCH] [IMP] purchase, sale_stock, stock_account: to refund purchase When we have purchase orders and we return some products to the supplier, we now have the possibilty to set those moves as 'to refund' and so the quantity received is decreased. Because the feature 'to refund' already exists on the sale orders, we will generalize it in the module 'stock_account' which is a dependency of both 'sale_stock' and 'purchase' which are the both module that use the feature 'to refund'. --- addons/purchase/models/purchase.py | 9 +- addons/purchase/models/stock.py | 6 +- addons/purchase/tests/test_purchase_order.py | 97 ++++++++++++++++--- addons/sale_stock/models/sale_order.py | 2 +- addons/sale_stock/models/stock.py | 25 ----- addons/sale_stock/tests/test_sale_stock.py | 2 +- addons/sale_stock/views/stock_views.xml | 11 --- addons/stock_account/models/stock.py | 24 +++++ .../views/stock_account_views.xml | 11 +++ 9 files changed, 130 insertions(+), 57 deletions(-) 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 + + + + + + +