[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'.
This commit is contained in:
Srushti Patel
2017-04-25 11:08:14 +02:00
committed by Pierre Masereel
parent f47775b039
commit 49c159a906
9 changed files with 130 additions and 57 deletions
+5 -4
View File
@@ -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 '',
+4 -2
View File
@@ -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)
+84 -13
View File
@@ -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')
+1 -1
View File
@@ -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
-25
View File
@@ -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')
+1 -1
View File
@@ -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'])
-11
View File
@@ -23,16 +23,5 @@
</field>
</record>
<record id="view_stock_return_picking_form_inherit_sale_stock" model="ir.ui.view">
<field name="name">stock.return.picking.sale.stock.form</field>
<field name="inherit_id" ref="stock.view_stock_return_picking_form"/>
<field name="model">stock.return.picking</field>
<field name="arch" type="xml">
<field name="quantity" position="after">
<field name="to_refund_so"/>
</field>
</field>
</record>
</data>
</odoo>
+24
View File
@@ -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')
@@ -38,5 +38,16 @@
</field>
</record>
<record id="view_stock_return_picking_form_inherit_stock_account" model="ir.ui.view">
<field name="name">stock.return.picking.stock.account.form</field>
<field name="inherit_id" ref="stock.view_stock_return_picking_form"/>
<field name="model">stock.return.picking</field>
<field name="arch" type="xml">
<field name="quantity" position="after">
<field name="to_refund"/>
</field>
</field>
</record>
</data>
</odoo>