[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:
committed by
Pierre Masereel
parent
f47775b039
commit
49c159a906
@@ -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 '',
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'])
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user