account-reconcile/account_reconcile_rule/models/account_reconcile_rule.py

166 lines
6.2 KiB
Python

# Author: Guewen Baconnier
# Copyright 2014 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import models, fields, api
from odoo.addons import decimal_precision as dp
class AccountReconcileRule(models.Model):
_name = 'account.reconcile.rule'
_description = 'Rules for reconciliation'
_order = 'sequence ASC, id ASC'
name = fields.Char()
rule_type = fields.Selection(
selection=[('rounding', 'Roundings'),
('currency', 'Currencies')],
string='Type',
default='rounding',
required=True,
)
reconcile_model_ids = fields.Many2many(
comodel_name='account.reconcile.model',
string='Reconciliation models',
)
amount_min = fields.Float(
string='Min. Amount',
digits=dp.get_precision('Account'),
)
amount_max = fields.Float(
string='Max. Amount',
digits=dp.get_precision('Account'),
)
currency_ids = fields.Many2many(
comodel_name='res.currency',
string='Currencies',
help="For 'Currencies' rules, you can choose for which currencies "
"the rule will be applicable.",
)
sequence = fields.Integer(
default=20,
help="If several rules match, the first one is used.",
)
@staticmethod
def _between_with_bounds(low, value, high, currency):
"""Equivalent to a three way comparison: ``min <= value <= high``
The comparisons are done with the currency to use the correct
precision.
"""
if currency.compare_amounts(value, low) == -1:
return False
if currency.compare_amounts(value, high) == 1:
return False
return True
@api.multi
def _balance_in_range(self, balance, currency):
return self._between_with_bounds(self.amount_min, balance,
self.amount_max, currency)
@api.model
def _is_multicurrency(self, statement_line):
currency = statement_line.currency_for_rules()
company_currency = statement_line.company_id.currency_id
return currency != company_currency
@api.multi
def _is_valid_balance(self, statement_line, balance):
if self._is_multicurrency(statement_line):
return False
currency = statement_line.currency_for_rules()
return self._balance_in_range(balance, currency)
@api.multi
def _is_valid_multicurrency(self, statement_line, move_lines, balance):
"""Check if the multi-currency rule can be applied.
The rule is applied if and only if:
* The currency is not company's one
* The currency of the statement line and all the lines is the same
* The balance of the amount currencies is 0
* The balance is between the bounds configured on the rule
"""
if not self._is_multicurrency(statement_line):
return False
currency = statement_line.currency_for_rules()
if currency not in self.currency_ids:
return False
amount_currency = statement_line.amount_currency
for move_line in move_lines:
if move_line.currency_id != statement_line.currency_id:
# use case not supported, no rule found
return False
amount_currency -= move_line.amount_currency
# amount in currency is the same, so the balance is
# a difference due to currency rates
if statement_line.currency_id.is_zero(amount_currency):
return self._balance_in_range(balance, currency)
return False
@api.multi
def is_valid(self, statement_line, move_lines, balance):
"""Check if a rule applies to a group of statement_line + move lines.
This is the public method where the rule is evaluated whatever
its type is. When a rule returns True, it means that it is a
candidate for the current reconciliation. The rule with the lowest
number in the ``sequence`` field is chosen.
:param statement_line: the line to reconcile
:param move_lines: the selected move lines for reconciliation
:param balance: the balance between the statement_line and the
move_lines. It could be computed here but it is
computed before to avoid to compute it for each
rule when called on multiple rules.
"""
self.ensure_one()
if self.rule_type == 'rounding':
return self._is_valid_balance(statement_line, balance)
elif self.rule_type == 'currency':
return self._is_valid_multicurrency(statement_line,
move_lines,
balance)
@api.model
def find_first_rule(self, statement_line, move_lines):
"""Find rules to apply to given statement line and move lines.
:param statement_line: the line to reconcile
:param move_lines: the selected move lines for reconciliation
"""
balance = statement_line.amount
for move_line in move_lines:
balance += move_line.credit - move_line.debit
currency = statement_line.currency_for_rules()
if currency.is_zero(balance):
return self.browse()
rules = self.search([])
# return the first applicable rule
for rule in rules:
if rule.is_valid(statement_line, move_lines, balance):
return rule
return self.browse()
@api.model
@api.returns('account.reconcile.model')
def models_for_reconciliation(self, statement_line_id, move_line_ids):
"""Find the reconcile models for the for given statement and move lines.
Look for the first reconciliation rule to apply and return its
reconciliation models.
Called from the javascript reconciliation view.
"""
line_obj = self.env['account.bank.statement.line']
move_line_obj = self.env['account.move.line']
statement_line = line_obj.browse(statement_line_id)
move_lines = move_line_obj.browse(move_line_ids)
rules = self.find_first_rule(statement_line, move_lines)
return rules.reconcile_model_ids