241 lines
8.4 KiB
Python
241 lines
8.4 KiB
Python
# Copyright 2012-2016 Camptocamp SA
|
|
# Copyright 2010 Sébastien Beau
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
|
|
from operator import itemgetter
|
|
|
|
from odoo import _, fields, models
|
|
from odoo.tools.safe_eval import safe_eval
|
|
|
|
|
|
class MassReconcileBase(models.AbstractModel):
|
|
"""Abstract Model for reconciliation methods"""
|
|
|
|
_name = "mass.reconcile.base"
|
|
_inherit = "mass.reconcile.options"
|
|
_description = "Mass Reconcile Base"
|
|
|
|
account_id = fields.Many2one("account.account", string="Account", required=True)
|
|
partner_ids = fields.Many2many(
|
|
comodel_name="res.partner", string="Restrict on partners"
|
|
)
|
|
# other fields are inherited from mass.reconcile.options
|
|
|
|
def automatic_reconcile(self):
|
|
"""Reconciliation method called from the view.
|
|
|
|
:return: list of reconciled ids
|
|
"""
|
|
self.ensure_one()
|
|
return self._action_rec()
|
|
|
|
def _action_rec(self):
|
|
"""Must be inherited to implement the reconciliation
|
|
|
|
:return: list of reconciled ids
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@staticmethod
|
|
def _base_columns():
|
|
"""Mandatory columns for move lines queries
|
|
An extra column aliased as ``key`` should be defined
|
|
in each query."""
|
|
aml_cols = (
|
|
"id",
|
|
"debit",
|
|
"credit",
|
|
"currency_id",
|
|
"amount_residual",
|
|
"amount_residual_currency",
|
|
"date",
|
|
"ref",
|
|
"name",
|
|
"partner_id",
|
|
"account_id",
|
|
"reconciled",
|
|
"move_id",
|
|
)
|
|
return [f"account_move_line.{col}" for col in aml_cols]
|
|
|
|
def _selection_columns(self):
|
|
return self._base_columns()
|
|
|
|
def _select_query(self, *args, **kwargs):
|
|
return "SELECT %s" % ", ".join(self._selection_columns())
|
|
|
|
def _from_query(self, *args, **kwargs):
|
|
return "FROM account_move_line "
|
|
|
|
def _where_query(self, *args, **kwargs):
|
|
self.ensure_one()
|
|
where = (
|
|
"WHERE account_move_line.account_id = %s "
|
|
"AND NOT account_move_line.reconciled "
|
|
"AND parent_state = 'posted'"
|
|
)
|
|
# it would be great to use dict for params
|
|
# but as we use _where_calc in _get_filter
|
|
# which returns a list, we have to
|
|
# accommodate with that
|
|
params = [self.account_id.id]
|
|
if self.partner_ids:
|
|
where += " AND account_move_line.partner_id IN %s"
|
|
params.append(tuple(line.id for line in self.partner_ids))
|
|
return where, params
|
|
|
|
def _get_filter(self):
|
|
self.ensure_one()
|
|
ml_obj = self.env["account.move.line"]
|
|
where = ""
|
|
params = []
|
|
if self._filter:
|
|
dummy, where, params = ml_obj._where_calc(safe_eval(self._filter)).get_sql()
|
|
if where:
|
|
where = " AND %s" % where
|
|
return where, params
|
|
|
|
def _below_writeoff_limit(self, lines, writeoff_limit):
|
|
self.ensure_one()
|
|
precision = self.env["decimal.precision"].precision_get("Account")
|
|
|
|
writeoff_amount = round(
|
|
sum(line["amount_residual"] for line in lines), precision
|
|
)
|
|
writeoff_amount_curr = round(
|
|
sum(line["amount_residual_currency"] for line in lines), precision
|
|
)
|
|
|
|
first_currency = lines[0]["currency_id"]
|
|
if all([line["currency_id"] == first_currency for line in lines]):
|
|
ref_amount = writeoff_amount_curr
|
|
same_curr = True
|
|
# TODO if currency != company currency compute writeoff_limit in currency
|
|
else:
|
|
ref_amount = writeoff_amount
|
|
same_curr = False
|
|
|
|
return (
|
|
bool(writeoff_limit >= abs(ref_amount)),
|
|
writeoff_amount,
|
|
writeoff_amount_curr,
|
|
same_curr,
|
|
)
|
|
|
|
def _get_rec_date(self, lines, based_on="end_period_last_credit"):
|
|
self.ensure_one()
|
|
|
|
def last_date(mlines):
|
|
return max(mlines, key=itemgetter("date"))
|
|
|
|
def oldest_date(mlines):
|
|
return min(mlines, key=itemgetter("date"))
|
|
|
|
def credit(mlines):
|
|
return [line for line in mlines if line["credit"] > 0]
|
|
|
|
def debit(mlines):
|
|
return [line for line in mlines if line["debit"] > 0]
|
|
|
|
if based_on == "newest":
|
|
return last_date(lines)["date"]
|
|
elif based_on == "oldest":
|
|
return oldest_date(lines)["date"]
|
|
elif based_on == "newest_credit":
|
|
return last_date(credit(lines))["date"]
|
|
elif based_on == "newest_debit":
|
|
return last_date(debit(lines))["date"]
|
|
# reconcilation date will be today
|
|
# when date is None
|
|
return None
|
|
|
|
def create_write_off(self, lines, amount, amount_curr, same_curr):
|
|
self.ensure_one()
|
|
if amount < 0:
|
|
account = self.account_profit_id
|
|
else:
|
|
account = self.account_lost_id
|
|
currency = same_curr and lines[0].currency_id or lines[0].company_id.currency_id
|
|
journal = self.journal_id
|
|
partners = lines.mapped("partner_id")
|
|
|
|
# Adjust amount_currency to match the sign of the company's residual (amount)
|
|
if same_curr:
|
|
if amount != 0:
|
|
sign = 1 if amount > 0 else -1
|
|
adjusted_amount_curr = abs(amount_curr) * sign
|
|
else:
|
|
adjusted_amount_curr = 0.0
|
|
else:
|
|
adjusted_amount_curr = amount
|
|
|
|
write_off_vals = {
|
|
"name": _("Automatic writeoff"),
|
|
"amount_currency": adjusted_amount_curr if same_curr else amount,
|
|
"debit": amount > 0.0 and amount or 0.0,
|
|
"credit": amount < 0.0 and -amount or 0.0,
|
|
"partner_id": len(partners) == 1 and partners.id or False,
|
|
"account_id": account.id,
|
|
"journal_id": journal.id,
|
|
"currency_id": currency.id,
|
|
}
|
|
counterpart_account = lines.mapped("account_id")
|
|
counter_part = write_off_vals.copy()
|
|
counter_part["debit"] = write_off_vals["credit"]
|
|
counter_part["credit"] = write_off_vals["debit"]
|
|
counter_part["amount_currency"] = -write_off_vals["amount_currency"]
|
|
counter_part["account_id"] = (counterpart_account.id,)
|
|
|
|
move = self.env["account.move"].create(
|
|
{
|
|
"date": lines.env.context.get("date_p") or fields.Date.today(),
|
|
"journal_id": journal.id,
|
|
"currency_id": currency.id,
|
|
"line_ids": [(0, 0, write_off_vals), (0, 0, counter_part)],
|
|
}
|
|
)
|
|
move.action_post()
|
|
return move.line_ids.filtered(
|
|
lambda line: line.account_id.id == counterpart_account.id
|
|
)
|
|
|
|
def _reconcile_lines(self, lines, allow_partial=False):
|
|
"""Try to reconcile given lines
|
|
|
|
:param list lines: list of dict of move lines, they must at least
|
|
contain values for : id, debit, credit, amount_residual and
|
|
amount_residual_currency
|
|
:param boolean allow_partial: if True, partial reconciliation will be
|
|
created, otherwise only Full
|
|
reconciliation will be created
|
|
:return: tuple of boolean values, first item is wether the items
|
|
have been reconciled or not,
|
|
the second is wether the reconciliation is full (True)
|
|
or partial (False)
|
|
"""
|
|
self.ensure_one()
|
|
ml_obj = self.env["account.move.line"]
|
|
(
|
|
below_writeoff,
|
|
amount_writeoff,
|
|
amount_writeoff_curr,
|
|
same_curr,
|
|
) = self._below_writeoff_limit(lines, self.write_off)
|
|
rec_date = self._get_rec_date(lines, self.date_base_on)
|
|
line_rs = ml_obj.browse([line["id"] for line in lines]).with_context(
|
|
date_p=rec_date, comment=_("Automatic Write Off")
|
|
)
|
|
if below_writeoff:
|
|
balance = amount_writeoff_curr if same_curr else amount_writeoff
|
|
if abs(balance) != 0.0:
|
|
writeoff_line = self.create_write_off(
|
|
line_rs, amount_writeoff, amount_writeoff_curr, same_curr
|
|
)
|
|
line_rs |= writeoff_line
|
|
line_rs.reconcile()
|
|
return True, True
|
|
elif allow_partial:
|
|
line_rs.reconcile()
|
|
return True, False
|
|
return False, False
|