From a5a89b41e19ad4d205833bf80c27a5c6d65b8228 Mon Sep 17 00:00:00 2001 From: AaronHForgeFlow Date: Tue, 21 Feb 2023 09:37:23 +0100 Subject: [PATCH] [IMP] sale_unreconciled: reconcile by groups --- sale_unreconciled/models/account_move_line.py | 2 + sale_unreconciled/models/sale_order.py | 126 +++++++++++------- .../tests/test_sale_unreconciled.py | 30 +++++ 3 files changed, 108 insertions(+), 50 deletions(-) diff --git a/sale_unreconciled/models/account_move_line.py b/sale_unreconciled/models/account_move_line.py index c447f64bf8cb..7c3582bb48a8 100644 --- a/sale_unreconciled/models/account_move_line.py +++ b/sale_unreconciled/models/account_move_line.py @@ -52,6 +52,8 @@ def _create_so_writeoff(self, writeoff_vals): "sale_order_id": writeoff_vals["sale_order_id"], "journal_id": writeoff_vals["journal_id"], "currency_id": writeoff_vals["currency_id"], + "product_id": writeoff_vals["product_id"], + "sale_line_id": writeoff_vals["sale_line_id"], } counterpart_account = self.mapped("account_id") if len(counterpart_account) != 1: diff --git a/sale_unreconciled/models/sale_order.py b/sale_unreconciled/models/sale_order.py index e3ae5a0961cd..4297640dc2c2 100644 --- a/sale_unreconciled/models/sale_order.py +++ b/sale_unreconciled/models/sale_order.py @@ -110,61 +110,57 @@ def action_reconcile(self): unreconciled_items = acc_item.search(unreconciled_domain) writeoff_to_reconcile = self.env["account.move.line"] all_writeoffs = self.env["account.move.line"] - for account in unreconciled_items.mapped("account_id"): - acc_unrec_items = unreconciled_items.filtered( - lambda ml: ml.account_id == account - ) - for currency in acc_unrec_items.mapped("currency_id"): - unreconciled_items_currency = acc_unrec_items.filtered( - lambda l: l.currency_id == currency - ) - # nothing to reconcile - # if journal items are zero zero then we force a matching number - if all( - not x.amount_residual and not x.amount_residual_currency - for x in unreconciled_items_currency - ): - self.env["account.full.reconcile"].create( - { - "reconciled_line_ids": [(6, 0, unreconciled_items.ids)], - } - ) - continue - all_aml_share_same_currency = all( - [ - x.currency_id == self[0].currency_id - for x in unreconciled_items_currency - ] + reconciling_groups = self.env["account.move.line"].read_group( + domain=unreconciled_domain, + fields=["account_id", "currency_id", "product_id", "sale_line_id"], + groupby=["account_id", "currency_id", "product_id", "sale_line_id"], + lazy=False, + ) + moves_to_reconcile = self.env["account.move.line"] + products_considered = {} + main_product = self.env["product.product"] + for group in reconciling_groups: + account_id = group["account_id"][0] + currency_id = group["currency_id"][0] if group["currency_id"] else False + products_considered[currency_id] = self.env["product.product"] + product_id = group["product_id"][0] if group["product_id"] else False + sale_line_id = group["sale_line_id"][0] if group["sale_line_id"] else False + if product_id and product_id in products_considered[currency_id].ids: + # avoid duplicate write-off for kits + continue + if sale_line_id and product_id: + products, main_product = self.get_products(sale_line_id, product_id) + else: + products = self.env["product.product"] + products_considered[currency_id] |= products + unreconciled_items_group = unreconciled_items.filtered( + lambda l: ( + l.account_id.id == account_id + and l.product_id.id in products.ids + and l.currency_id.id == currency_id ) - writeoff_vals = { - "account_id": self.company_id.sale_reconcile_account_id.id, - "journal_id": self.company_id.sale_reconcile_journal_id.id, - "sale_order_id": self.id, - "currency_id": currency.id, - } - if not all_aml_share_same_currency: - writeoff_vals["amount_currency"] = False - writeoff_to_reconcile |= ( - unreconciled_items_currency._create_so_writeoff(writeoff_vals) + ) + if main_product: + # If kit, use the product of the kit + product_id = main_product.id + writeoff_vals = self._get_sale_writeoff_vals( + sale_line_id, currency_id, product_id + ) + if unreconciled_items_group: + writeoff_to_reconcile = unreconciled_items_group._create_writeoff( + [writeoff_vals] ) all_writeoffs |= writeoff_to_reconcile # add writeoff line to reconcile algorithm and finish the reconciliation - remaining_moves = unreconciled_items_currency | writeoff_to_reconcile - # Check if reconciliation is total or needs an exchange rate entry to be created - if remaining_moves: - remaining_moves.filtered( - lambda l: l.balance != 0.0 - ).remove_move_reconcile() - remaining_moves.filtered(lambda l: l.balance != 0.0).reconcile() - # There are some journal items that are zero balance that shows - # as unreconciled, we just attached the full reconcile just created - full_reconcile_id = remaining_moves.mapped("full_reconcile_id") - full_reconcile_id = full_reconcile_id and full_reconcile_id[0] - remaining_moves.filtered( - lambda l: l.balance == 0.0 and not l.full_reconcile_id - ).write({"full_reconcile_id": full_reconcile_id}) + moves_to_reconcile = unreconciled_items_group | writeoff_to_reconcile + # Check if reconciliation is total or needs an exchange rate entry to be + # created + if moves_to_reconcile: + moves_to_reconcile.filtered( + lambda l: l.amount_residual != 0.0 + ).reconcile() reconciled_ids = unreconciled_items | all_writeoffs - return { + res = { "name": _("Reconciled journal items"), "type": "ir.actions.act_window", "view_type": "form", @@ -172,6 +168,36 @@ def action_reconcile(self): "res_model": "account.move.line", "domain": [("id", "in", reconciled_ids.ids)], } + if self.env.context.get("bypass_unreconciled", False): + # When calling the method from the wizard, lock after reconciling + self.action_done() + return res + + def get_products(self, sale_line_id, product_id): + # if kit return the kit and components, otherwise just the product + sale_line = self.env["sale.order.line"].browse(sale_line_id) + boms = ( + sale_line.move_ids.filtered(lambda m: m.state != "cancel") + .mapped("bom_line_id.bom_id") + .filtered(lambda b: b.type == "phantom") + ) + products = self.env["product.product"].browse(product_id) + if boms: + bom = boms[:1] + boms, lines = bom.explode(sale_line.product_id, sale_line.product_uom_qty) + for line, _line_data in lines: + products |= line.product_id + return products, sale_line.product_id + + def _get_sale_writeoff_vals(self, sale_line_id, currency_id, product_id): + return { + "account_id": self.company_id.sale_reconcile_account_id.id, + "journal_id": self.company_id.sale_reconcile_journal_id.id, + "sale_order_id": self.id, + "sale_line_id": sale_line_id or False, + "currency_id": currency_id, + "product_id": product_id, + } def reconcile_criteria(self): """Gets the criteria where SOs are locked or not, by default uses the company diff --git a/sale_unreconciled/tests/test_sale_unreconciled.py b/sale_unreconciled/tests/test_sale_unreconciled.py index ed49e9a49023..06c987d02a8c 100644 --- a/sale_unreconciled/tests/test_sale_unreconciled.py +++ b/sale_unreconciled/tests/test_sale_unreconciled.py @@ -491,3 +491,33 @@ def test_07_multicompany(self): [("sale_order_id", "=", so.id), ("move_id", "!=", invoice.id)] ) self.assertEqual(so.company_id, ji.mapped("company_id")) + + def test_08_reconcile_by_product(self): + """ + Create a write-off by product + """ + so = self._create_sale( + [(self.product_to_reconcile, 1), (self.product_to_reconcile2, 1)] + ) + so.company_id.sale_reconcile_account_id = self.writeoff_acc + so.with_context(force_confirm_sale_order=True).action_confirm() + self._do_picking(so.picking_ids, fields.Datetime.now()) + # Do not create invoices to force discrepancy + so.action_done() + # we check all the journals are balanced by product + ji_s1 = self.env["account.move.line"].search( + [ + ("sale_order_id", "=", so.id), + ("product_id", "=", self.product_to_reconcile.id), + ("account_id", "=", self.account_gdni.id), + ] + ) + ji_s2 = self.env["account.move.line"].search( + [ + ("sale_order_id", "=", so.id), + ("product_id", "=", self.product_to_reconcile2.id), + ("account_id", "=", self.account_gdni.id), + ] + ) + self.assertEqual(sum(ji_s1.mapped("balance")), 0.0) + self.assertEqual(sum(ji_s2.mapped("balance")), 0.0)