Skip to content

Commit

Permalink
[IMP] sale_unreconciled: reconcile by groups
Browse files Browse the repository at this point in the history
  • Loading branch information
AaronHForgeFlow committed Feb 21, 2023
1 parent 83a7e4b commit a5a89b4
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 50 deletions.
2 changes: 2 additions & 0 deletions sale_unreconciled/models/account_move_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
126 changes: 76 additions & 50 deletions sale_unreconciled/models/sale_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,68 +110,94 @@ 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",
"view_mode": "tree,form",
"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
Expand Down
30 changes: 30 additions & 0 deletions sale_unreconciled/tests/test_sale_unreconciled.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit a5a89b4

Please sign in to comment.