[REV] sale: refund of bill re-invoiced

This reverts commit 33d95edd4832c4bd68bb2da2c4585e7b26db8a3c.

Following of the next steps https://www.odoo.com/web#id=3338017&cids=1&model=project.task&view_type=form

Initial opw-2992106

closes odoo/odoo#122998

Forward-port-of: #122431
X-original-commit: 9b50f49edaf1bee5cab0f42f37f92f4752654789
Signed-off-by: William André (wan) <wan@odoo.com>
Signed-off-by: Yolann Sabaux (yosa) <yosa@odoo.com>
This commit is contained in:
Yolann Sabaux
2023-05-31 07:13:56 +02:00
parent fa66af2c47
commit 249064ff8b
3 changed files with 26 additions and 86 deletions
+4 -8
View File
@@ -48,16 +48,13 @@ 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 '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
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
"""
self.ensure_one()
if self.sale_line_ids:
return False
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))
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']
def _sale_create_reinvoice_sale_line(self):
@@ -93,8 +90,7 @@ 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') \
or move_line.move_id.move_type in ('out_refund', 'in_refund'): # 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': # 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`
+22 -13
View File
@@ -674,7 +674,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([])
mapping = lines_by_analytic._get_delivered_quantity_by_analytic([('amount', '<=', 0.0)])
for so_line in lines_by_analytic:
so_line.qty_delivered = mapping.get(so_line.id or so_line._origin.id, 0.0)
@@ -705,20 +705,29 @@ class SaleOrderLine(models.Model):
# group analytic lines by product uom and so line
domain = expression.AND([[('so_line', 'in', self.ids)], additional_domain])
analytic_lines = self.env['account.analytic.line'].search(domain)
for line in analytic_lines:
if not line.product_uom_id:
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']:
continue
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')
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')
else:
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
qty = item['unit_amount']
result[so_line_id] += qty
return result
-65
View File
@@ -40,17 +40,6 @@ 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')
@@ -289,57 +278,3 @@ 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}])