Skip to content
Snippets Groups Projects
Commit 5258e2ce authored by Yolann Sabaux's avatar Yolann Sabaux
Browse files

[FIX] sale: refund of bill re-invoiced


Steps to reproduce:
- create an expense product, re-invoice: "at cost"
- create a sale order with the product and set an analytic account
- create a purchase order with the expense product and the same analytic account
- create the bill
-> a new line on the SO is created with the same "Quantity Delivered" as the "Quantity Invoiced" for the purchase order
- for the bill, create a Credit Note of x unit
-> the purchase order has the Quantity Invoiced diminished of x

Issue:
- the Sale Order has not taken into account the new quantity after refund
- Since the line is an "analytic" one, the quantity delivered cannot be changed even if we wreate a credit note for the invoice

Solution:
During the processus of creation of new analytic lines, instead of creating new so lines we adapt the analytic line

closes odoo/odoo#120370

X-original-commit: 024e9c6c
Signed-off-by: default avatarWilliam André (wan) <wan@odoo.com>
parent b5bbb9b6
Branches
Tags
No related merge requests found
......@@ -48,13 +48,16 @@ class AccountMoveLine(models.Model):
def _sale_can_be_reinvoice(self):
""" determine if the generated analytic line should be reinvoiced or not.
For Vendor Bill flow, if the product has a 'erinvoice policy' and is a cost, then we will find the SO on which reinvoice the AAL
For Vendor Bill flow, if the product has a 'reinvoice policy' and is a cost, then we will find the SO on which reinvoice the AAL
if it is refund, we will update the quantity of the SO line
"""
self.ensure_one()
if self.sale_line_ids:
return False
uom_precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
return float_compare(self.credit or 0.0, self.debit or 0.0, precision_digits=uom_precision_digits) != 1 and self.product_id.expense_policy not in [False, 'no']
is_refund = self.move_id.move_type in ('out_refund', 'in_refund')
return self.product_id.expense_policy not in [False, 'no'] and (
(self.currency_id.compare_amounts(self.balance, 0.0) == -1 and is_refund)
or (self.currency_id.compare_amounts(self.balance, 0.0) == 1 and not is_refund))
def _sale_create_reinvoice_sale_line(self):
......@@ -90,7 +93,8 @@ class AccountMoveLine(models.Model):
# find the existing sale.line or keep its creation values to process this in batch
sale_line = None
if move_line.product_id.expense_policy == 'sales_price' and move_line.product_id.invoice_policy == 'delivery': # for those case only, we can try to reuse one
if (move_line.product_id.expense_policy == 'sales_price' and move_line.product_id.invoice_policy == 'delivery') \
or move_line.move_id.move_type in ('out_refund', 'in_refund'): # for those case only, we can try to reuse one
map_entry_key = (sale_order.id, move_line.product_id.id, price) # cache entry to limit the call to search
sale_line = existing_sale_line_cache.get(map_entry_key)
if sale_line: # already search, so reuse it. sale_line can be sale.order.line record or index of a "to create values" in `sale_line_values_to_create`
......
......@@ -691,7 +691,7 @@ class SaleOrderLine(models.Model):
"""
# compute for analytic lines
lines_by_analytic = self.filtered(lambda sol: sol.qty_delivered_method == 'analytic')
mapping = lines_by_analytic._get_delivered_quantity_by_analytic([('amount', '<=', 0.0)])
mapping = lines_by_analytic._get_delivered_quantity_by_analytic([])
for so_line in lines_by_analytic:
so_line.qty_delivered = mapping.get(so_line.id or so_line._origin.id, 0.0)
......@@ -722,29 +722,20 @@ class SaleOrderLine(models.Model):
# group analytic lines by product uom and so line
domain = expression.AND([[('so_line', 'in', self.ids)], additional_domain])
data = self.env['account.analytic.line'].read_group(
domain,
['so_line', 'unit_amount', 'product_uom_id'], ['product_uom_id', 'so_line'], lazy=False
)
# convert uom and sum all unit_amount of analytic lines to get the delivered qty of SO lines
# browse so lines and product uoms here to make them share the same prefetch
lines = self.browse([item['so_line'][0] for item in data])
lines_map = {line.id: line for line in lines}
product_uom_ids = [item['product_uom_id'][0] for item in data if item['product_uom_id']]
product_uom_map = {uom.id: uom for uom in self.env['uom.uom'].browse(product_uom_ids)}
for item in data:
if not item['product_uom_id']:
analytic_lines = self.env['account.analytic.line'].search(domain)
for line in analytic_lines:
if not line.product_uom_id:
continue
so_line_id = item['so_line'][0]
so_line = lines_map[so_line_id]
result.setdefault(so_line_id, 0.0)
uom = product_uom_map.get(item['product_uom_id'][0])
if so_line.product_uom.category_id == uom.category_id:
qty = uom._compute_quantity(item['unit_amount'], so_line.product_uom, rounding_method='HALF-UP')
result.setdefault(line.so_line.id, 0.0)
if line.so_line.product_uom.category_id == line.product_uom_id.category_id:
qty = line.product_uom_id._compute_quantity(line.unit_amount, line.so_line.product_uom, rounding_method='HALF-UP')
else:
qty = item['unit_amount']
result[so_line_id] += qty
qty = line.unit_amount
# if greater than 0 -> refund
sign = line.amount and -line.amount / abs(line.amount) or 1
result[line.so_line.id] += sign * qty
return result
......
......@@ -40,6 +40,17 @@ class TestReInvoice(TestSaleCommon):
mail_create_nolog=True,
)
def _create_sol(self, sale_order, product):
return self.env['sale.order.line'].create({
'name': product.name,
'product_id': product.id,
'product_uom_qty': 1,
'qty_delivered': 0,
'product_uom': product.uom_id.id,
'price_unit': product.list_price,
'order_id': sale_order.id,
})
def test_at_cost(self):
# Required for `analytic_account_id` to be visible in the view
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
......@@ -278,3 +289,57 @@ class TestReInvoice(TestSaleCommon):
self.assertFalse(so_line4, "No re-invoicing should have created a new sale line with product #2")
self.assertEqual(so_line1.qty_delivered, 1, "No re-invoicing should have impacted exising SO line 1")
self.assertEqual(so_line2.qty_delivered, 1, "No re-invoicing should have impacted exising SO line 2")
def test_refund_delivered_reinvoiced(self):
"""
Tests that when we refund a re-invoiced expense, the Quantity Delivered on the Sale Order Line is updated
- (1) We create a Sale Order
- (2) We create a bill to be re-invoiced
- (3) We create a partial credit note
-> The sale order lines created in (2) should be updated during (3) with the correct delivered quantity.
"""
# create the setup
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
product_1 = self.company_data['product_order_cost']
product_2 = self.env['product.product'].create({
'name': 'Great Product',
'standard_price': 50.0,
'list_price': 100.0,
'type': 'consu',
'uom_id': self.env.ref('uom.product_uom_unit').id,
'uom_po_id': self.env.ref('uom.product_uom_unit').id,
'invoice_policy': 'order',
'expense_policy': 'cost',
})
sol_1 = self._create_sol(self.sale_order, product_1)
sol_1.analytic_distribution = {self.analytic_account.id: 100}
sol_2 = self._create_sol(self.sale_order, product_2)
sol_2.analytic_distribution = {self.analytic_account.id: 100}
self.sale_order.action_confirm()
# create the bill to be re-invoiced
bill_form = Form(self.AccountMove.with_context(default_move_type='in_invoice'))
bill_form.partner_id = self.partner_b
with bill_form.invoice_line_ids.new() as line_form:
line_form.product_id = product_1
line_form.quantity = 20.0
line_form.analytic_distribution = {self.analytic_account.id: 100}
with bill_form.invoice_line_ids.new() as line_form:
line_form.product_id = product_2
line_form.quantity = 20.0
line_form.analytic_distribution = {self.analytic_account.id: 100}
bill = bill_form.save()
bill.action_post()
self.assertRecordValues(self.sale_order.order_line[-2:], [{'qty_delivered': 20}, {'qty_delivered': 20}])
# create partial credit note
rbill_form = Form(bill._reverse_moves([{'invoice_date': '2023-03-31'}]))
with rbill_form.invoice_line_ids.edit(0) as line_form:
line_form.quantity = 10
with rbill_form.invoice_line_ids.edit(1) as line_form:
line_form.quantity = 5
rbill = rbill_form.save()
rbill.action_post()
self.assertRecordValues(self.sale_order.order_line[-2:], [{'qty_delivered': 10}, {'qty_delivered': 15}])
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment