Files
account-financial-tools/purchase_unreconciled/models/purchase_order.py
2024-10-16 11:33:49 +05:30

277 lines
12 KiB
Python

# Copyright 2019-21 ForgeFlow S.L..
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import _, api, exceptions, fields, models
from odoo.osv import expression
from odoo.tools import float_is_zero
class PurchaseOrder(models.Model):
_inherit = "purchase.order"
unreconciled = fields.Boolean(
compute="_compute_unreconciled",
search="_search_unreconciled",
help="Indicates that a Purchase Order has related Journal items not "
"reconciled.\nNote that if it is false it can be either that "
"everything is reconciled or that the related accounts do not "
"allow reconciliation",
)
amount_unreconciled = fields.Float(compute="_compute_unreconciled")
def _get_account_domain(self):
included_accounts = (
(
self.env["product.category"]
.with_company(self.company_id.id)
.search([("property_valuation", "=", "real_time")])
)
.mapped("property_stock_account_input_categ_id")
.ids
)
return [("account_id", "in", included_accounts)]
@api.model
def _get_purchase_unreconciled_base_domain(self):
unreconciled_domain = [
("account_id.reconcile", "=", True),
("move_id.state", "=", "posted"),
("company_id", "in", self.env.companies.ids),
# same condition than Odoo Unreconciled filter
("amount_residual", "!=", 0.0),
("balance", "!=", 0.0),
]
return unreconciled_domain
def _compute_unreconciled(self):
acc_item = self.env["account.move.line"]
for rec in self:
domain = rec.with_company(
rec.company_id
)._get_purchase_unreconciled_base_domain()
domain_account = rec._get_account_domain()
unreconciled_domain = expression.AND([domain, domain_account])
unreconciled_domain = expression.AND(
[unreconciled_domain, [("purchase_order_id", "=", rec.id)]]
)
unreconciled_items = acc_item.search(unreconciled_domain)
rec.unreconciled = len(unreconciled_items) > 0
rec.amount_unreconciled = sum(unreconciled_items.mapped("amount_residual"))
def _search_unreconciled(self, operator, value):
if operator not in ("=", "!=") or not isinstance(value, bool):
raise ValueError(_("Unsupported search operator"))
acc_item = self.env["account.move.line"]
domain = self._get_purchase_unreconciled_base_domain()
domain = expression.AND([domain, [("purchase_order_id", "!=", False)]])
domain_account = self._get_account_domain()
domain = expression.AND([domain_account, domain])
acc_items = acc_item.search(domain)
unreconciled_pos_ids = acc_items.mapped("purchase_order_id").ids
if value:
return [("id", "in", unreconciled_pos_ids)]
else:
return [("id", "not in", unreconciled_pos_ids)]
def action_view_unreconciled(self):
self.ensure_one()
acc_item = self.env["account.move.line"]
domain = self.with_company(
self.company_id.id
)._get_purchase_unreconciled_base_domain()
domain_account = self._get_account_domain()
unreconciled_domain = expression.AND([domain, domain_account])
unreconciled_domain = expression.AND(
[unreconciled_domain, [("purchase_order_id", "=", self.id)]]
)
unreconciled_domain.remove(("amount_residual", "!=", 0.0))
unreconciled_domain.remove("&")
unreconciled_items = acc_item.search(unreconciled_domain)
action = self.env.ref("account.action_account_moves_all")
action_dict = action.read()[0]
action_dict["domain"] = [("id", "in", unreconciled_items.ids)]
return action_dict
def action_reconcile(self):
if (
not self.company_id.purchase_reconcile_account_id
or not self.company_id.purchase_reconcile_journal_id
):
raise exceptions.ValidationError(
_(
"The write-off account and journal for purchases is missing. An "
"accountant must fill that information"
)
)
self.ensure_one()
res = {}
domain = self._get_purchase_unreconciled_base_domain()
domain_account = self._get_account_domain()
unreconciled_domain = expression.AND([domain, domain_account])
unreconciled_domain = expression.AND(
[domain, [("purchase_order_id", "=", self.id)]]
)
unreconciled_domain = expression.AND(
[unreconciled_domain, [("company_id", "=", self.company_id.id)]]
)
writeoff_to_reconcile = self.env["account.move.line"]
all_writeoffs = self.env["account.move.line"]
reconciling_groups = self.env["account.move.line"].read_group(
domain=unreconciled_domain,
fields=["account_id", "product_id", "purchase_line_id"],
groupby=["account_id", "product_id", "purchase_line_id"],
lazy=False,
)
unreconciled_items = self.env["account.move.line"].search(unreconciled_domain)
for group in reconciling_groups:
account_id = group["account_id"][0]
product_id = group["product_id"][0] if group["product_id"] else False
purchase_line_id = (
group["purchase_line_id"][0] if group["purchase_line_id"] else False
)
unreconciled_items_group = unreconciled_items.filtered(
lambda line, account_id=account_id, product_id=product_id: (
line.account_id.id == account_id
and line.product_id.id == product_id
)
)
# Check which type of force reconciling we are doing:
# - Force reconciling amount_residual
# - Force reconciling amount_residual_currency
amount_residual_currency_reconcile = any(
unreconciled_items_group.filtered(
lambda item_group,
account_id=account_id: item_group.amount_residual_currency != 0.0
and item_group.account_id.id == account_id
)
)
if amount_residual_currency_reconcile:
residual_field = "amount_residual_currency"
else:
residual_field = "amount_residual"
if float_is_zero(
sum(unreconciled_items_group.mapped(residual_field)),
precision_rounding=self.company_id.currency_id.rounding,
):
moves_to_reconcile = unreconciled_items_group
else:
writeoff_vals = self._get_purchase_writeoff_vals(
unreconciled_items_group, purchase_line_id, product_id
)
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
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 move: not move.reconciled
).reconcile()
reconciled_ids = unreconciled_items | all_writeoffs
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.button_done()
return res
def _get_purchase_writeoff_vals(self, amls, purchase_line_id, product_id):
writeoff_date = self.env.context.get("writeoff_date", False)
aml_date = max(amls.mapped("move_id.date"))
res = {
"account_id": self.company_id.purchase_reconcile_account_id.id,
"journal_id": self.company_id.purchase_reconcile_journal_id.id,
"purchase_order_id": self.id,
"purchase_line_id": purchase_line_id or False,
"product_id": product_id,
"currency_id": self.currency_id.id or self.env.company.currency_id.id,
"date": aml_date,
}
# hook for custom date:
if writeoff_date:
res.update({"date": writeoff_date})
return res
def reconcile_criteria(self):
"""Gets the criteria where POs are locked or not, by default uses the company
configuration"""
self.ensure_one()
return self.unreconciled and self.company_id.purchase_lock_auto_reconcile
def button_done(self):
for rec in self:
criteria = rec.reconcile_criteria()
if criteria:
if rec.unreconciled:
exception_msg = rec.unreconciled_exception_msg()
if exception_msg:
res = rec.purchase_unreconciled_exception(exception_msg)
return res
else:
rec.action_reconcile()
return super(PurchaseOrder, rec).button_done()
else:
return super(PurchaseOrder, rec).button_done()
else:
return super(PurchaseOrder, rec).button_done()
def purchase_unreconciled_exception(self, exception_msg=None):
"""This mean to be run when the SO cannot be reconciled because it is over
tolerance"""
self.ensure_one()
if exception_msg:
return (
self.env["purchase.unreconciled.exceeded.wiz"]
.create(
{
"exception_msg": exception_msg,
"purchase_id": self.id,
"origin_reference": "{},{}".format("purchase.order", self.id),
"continue_method": "action_reconcile",
}
)
.action_show()
)
def unreconciled_exception_msg(self):
self.ensure_one()
exception_msg = ""
amount_total = self.amount_total
if self.currency_id and self.company_id.currency_id != self.currency_id:
amount_total = self.currency_id._convert(
amount_total,
self.company_id.currency_id,
self.company_id,
fields.Date.today(),
)
if (
self.company_id.purchase_reconcile_tolerance
and amount_total
and abs(self.amount_unreconciled / amount_total)
>= self.company_id.purchase_reconcile_tolerance / 100.0
):
params = {
"amount_unreconciled": self.amount_unreconciled,
"amount_allowed": self.amount_total
* self.company_id.purchase_reconcile_tolerance
/ 100.0,
}
exception_msg = (
_(
"Finance Warning: \nUnreconciled amount is too high. Total "
"unreconciled amount: %(amount_unreconciled)s Maximum unreconciled"
" amount accepted: %(amount_allowed)s "
)
% params
)
return exception_msg