diff --git a/addons/sale/models/account_move_line.py b/addons/sale/models/account_move_line.py index 7acbe50b6a0b4be2023d445f58be9b5fe2c92fe6..6d52d2df7695597273b3235d738ec94856619dec 100644 --- a/addons/sale/models/account_move_line.py +++ b/addons/sale/models/account_move_line.py @@ -47,16 +47,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): @@ -92,8 +89,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` diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 811e6a37b84a408cb491b87d235e888ac9c430d2..8f44d5a47df9948fa5ad9f7c72961967956cab0e 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -344,7 +344,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) # compute for manual lines @@ -365,20 +365,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 diff --git a/addons/sale/tests/test_reinvoice.py b/addons/sale/tests/test_reinvoice.py index 53841f9abcc055614bb5a01e4626f599e00d6f4e..be414b81e8c4d74bb004708246e22572c7344cb6 100644 --- a/addons/sale/tests/test_reinvoice.py +++ b/addons/sale/tests/test_reinvoice.py @@ -34,17 +34,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): """ Test vendor bill at cost for product based on ordered and delivered quantities. """ # create SO line and confirm SO (with only one line) @@ -312,54 +301,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 - 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', - }) - self._create_sol(self.sale_order, product_1) - self._create_sol(self.sale_order, product_2) - 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.line_ids.new() as line_form: - line_form.product_id = product_1 - line_form.quantity = 20.0 - line_form.analytic_account_id = self.analytic_account - with bill_form.line_ids.new() as line_form: - line_form.product_id = product_2 - line_form.quantity = 20.0 - line_form.analytic_account_id = self.analytic_account - 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}])