diff --git a/account_mass_reconcile/models/advanced_reconciliation.py b/account_mass_reconcile/models/advanced_reconciliation.py
index af0a4459..2e81337d 100644
--- a/account_mass_reconcile/models/advanced_reconciliation.py
+++ b/account_mass_reconcile/models/advanced_reconciliation.py
@@ -110,3 +110,107 @@ class MassReconcileAdvancedRef(models.TransientModel):
move_line["name"].lower().strip(),
),
)
+
+
+class MassReconcileAdvancedName(models.TransientModel):
+
+ _name = "mass.reconcile.advanced.name"
+ _inherit = "mass.reconcile.advanced"
+ _description = "Mass Reconcile Advanced Name"
+
+ @staticmethod
+ def _skip_line(move_line):
+ """
+ When True is returned on some conditions, the credit move line
+ will be skipped for reconciliation. Can be inherited to
+ skip on some conditions. ie: ref or partner_id is empty.
+ """
+ return not (move_line.get("name", "/") != "/" and move_line.get("partner_id"))
+
+ @staticmethod
+ def _matchers(move_line):
+ """
+ Return the values used as matchers to find the opposite lines
+
+ All the matcher keys in the dict must have their equivalent in
+ the `_opposite_matchers`.
+
+ The values of each matcher key will be searched in the
+ one returned by the `_opposite_matchers`
+
+ Must be inherited to implement the matchers for one method
+
+ For instance, it can return:
+ return ('ref', move_line['rec'])
+
+ or
+ return (('partner_id', move_line['partner_id']),
+ ('ref', "prefix_%s" % move_line['rec']))
+
+ All the matchers have to be found in the opposite lines
+ to consider them as "opposite"
+
+ The matchers will be evaluated in the same order as declared
+ vs the the opposite matchers, so you can gain performance by
+ declaring first the partners with the less computation.
+
+ All matchers should match with their opposite to be considered
+ as "matching".
+ So with the previous example, partner_id and ref have to be
+ equals on the opposite line matchers.
+
+ :return: tuple of tuples (key, value) where the keys are
+ the matchers keys
+ (must be the same than `_opposite_matchers` returns,
+ and their values to match in the opposite lines.
+ A matching key can have multiples values.
+ """
+ return (
+ ("partner_id", move_line["partner_id"]),
+ ("name", move_line["name"].lower().strip()),
+ )
+
+ @staticmethod
+ def _opposite_matchers(move_line):
+ """
+ Return the values of the opposite line used as matchers
+ so the line is matched
+
+ Must be inherited to implement the matchers for one method
+ It can be inherited to apply some formatting of fields
+ (strip(), lower() and so on)
+
+ This method is the counterpart of the `_matchers()` method.
+
+ Each matcher has to yield its value respecting the order
+ of the `_matchers()`.
+
+ When a matcher does not correspond, the next matchers won't
+ be evaluated so the ones which need the less computation
+ have to be executed first.
+
+ If the `_matchers()` returns:
+ (('partner_id', move_line['partner_id']),
+ ('ref', move_line['ref']))
+
+ Here, you should yield :
+ yield ('partner_id', move_line['partner_id'])
+ yield ('ref', move_line['ref'])
+
+ Note that a matcher can contain multiple values, as instance,
+ if for a move line, you want to search from its `ref` in the
+ `ref` or `name` fields of the opposite move lines, you have to
+ yield ('partner_id', move_line['partner_id'])
+ yield ('ref', (move_line['ref'], move_line['name'])
+
+ An OR is used between the values for the same key.
+ An AND is used between the differents keys.
+
+ :param dict move_line: values of the move_line
+ :yield: matchers as tuple ('matcher key', value(s))
+ """
+ yield ("partner_id", move_line["partner_id"])
+ yield (
+ "name",
+ (move_line["name"].lower().strip(),),
+ )
diff --git a/account_mass_reconcile/models/base_advanced_reconciliation.py b/account_mass_reconcile/models/base_advanced_reconciliation.py
index 2d8064e7..57e80321 100644
--- a/account_mass_reconcile/models/base_advanced_reconciliation.py
+++ b/account_mass_reconcile/models/base_advanced_reconciliation.py
@@ -205,6 +205,7 @@ class MassReconcileAdvanced(models.AbstractModel):
]
def _action_rec(self):
+ self.flush()
credit_lines = self._query_credit()
debit_lines = self._query_debit()
result = self._rec_auto_lines_advanced(credit_lines, debit_lines)
@@ -222,57 +223,51 @@ class MassReconcileAdvanced(models.AbstractModel):
""" Advanced reconciliation main loop """
# pylint: disable=invalid-commit
reconciled_ids = []
- for rec in self:
- reconcile_groups = []
- ctx = self.env.context.copy()
- ctx["commit_every"] = rec.account_id.company_id.reconciliation_commit_every
- _logger.info("%d credit lines to reconcile", len(credit_lines))
- for idx, credit_line in enumerate(credit_lines, start=1):
- if idx % 50 == 0:
- _logger.info(
- "... %d/%d credit lines inspected ...", idx, len(credit_lines)
+ reconcile_groups = []
+ _logger.info("%d credit lines to reconcile", len(credit_lines))
+ for idx, credit_line in enumerate(credit_lines, start=1):
+ if idx % 50 == 0:
+ _logger.info(
+ "... %d/%d credit lines inspected ...", idx, len(credit_lines)
+ )
+ if self._skip_line(credit_line):
+ continue
+ opposite_lines = self._search_opposites(credit_line, debit_lines)
+ if not opposite_lines:
+ continue
+ opposite_ids = [line["id"] for line in opposite_lines]
+ line_ids = opposite_ids + [credit_line["id"]]
+ for group in reconcile_groups:
+ if any([lid in group for lid in opposite_ids]):
+ _logger.debug(
+ "New lines %s matched with an existing " "group %s",
+ line_ids,
+ group,
)
- if self._skip_line(credit_line):
- continue
- opposite_lines = self._search_opposites(credit_line, debit_lines)
- if not opposite_lines:
- continue
- opposite_ids = [l["id"] for l in opposite_lines]
- line_ids = opposite_ids + [credit_line["id"]]
- for group in reconcile_groups:
- if any([lid in group for lid in opposite_ids]):
- _logger.debug(
- "New lines %s matched with an existing " "group %s",
- line_ids,
- group,
- )
- group.update(line_ids)
- break
- else:
- _logger.debug("New group of lines matched %s", line_ids)
- reconcile_groups.append(set(line_ids))
- lines_by_id = {l["id"]: l for l in credit_lines + debit_lines}
+ group.update(line_ids)
+ break
+ else:
+ _logger.debug("New group of lines matched %s", line_ids)
+ reconcile_groups.append(set(line_ids))
+ lines_by_id = {line["id"]: line for line in credit_lines + debit_lines}
_logger.info("Found %d groups to reconcile", len(reconcile_groups))
- for group_count, reconcile_group_ids in enumerate(
- reconcile_groups, start=1
- ):
- _logger.debug(
- "Reconciling group %d/%d with ids %s",
- group_count,
- len(reconcile_groups),
- reconcile_group_ids,
- )
- group_lines = [lines_by_id[lid] for lid in reconcile_group_ids]
- reconciled, full = self._reconcile_lines(
- group_lines, allow_partial=True
- )
- if reconciled and full:
- reconciled_ids += reconcile_group_ids
+ for group_count, reconcile_group_ids in enumerate(reconcile_groups, start=1):
+ _logger.debug(
+ "Reconciling group %d/%d with ids %s",
+ group_count,
+ len(reconcile_groups),
+ reconcile_group_ids,
+ )
+ group_lines = [lines_by_id[lid] for lid in reconcile_group_ids]
+ reconciled, full = self._reconcile_lines(group_lines, allow_partial=True)
+ if reconciled and full:
+ reconciled_ids += reconcile_group_ids
- if ctx["commit_every"] and group_count % ctx["commit_every"] == 0:
- self.env.cr.commit()
- _logger.info(
- "Commit the reconciliations after %d groups", group_count
- )
+ if (
+ self.env.context.get("commit_every", 0)
+ and group_count % self.env.context["commit_every"] == 0
+ ):
+ self.env.cr.commit()
+ _logger.info("Commit the reconciliations after %d groups", group_count)
_logger.info("Reconciliation is over")
return reconciled_ids
diff --git a/account_mass_reconcile/models/base_reconciliation.py b/account_mass_reconcile/models/base_reconciliation.py
index 9c90017f..079d4fa7 100644
--- a/account_mass_reconcile/models/base_reconciliation.py
+++ b/account_mass_reconcile/models/base_reconciliation.py
@@ -2,7 +2,6 @@
# Copyright 2010 Sébastien Beau
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-from functools import reduce
from operator import itemgetter
from odoo import _, fields, models
@@ -46,6 +45,9 @@ class MassReconcileBase(models.AbstractModel):
"id",
"debit",
"credit",
+ "currency_id",
+ "amount_residual",
+ "amount_residual_currency",
"date",
"ref",
"name",
@@ -69,7 +71,8 @@ class MassReconcileBase(models.AbstractModel):
self.ensure_one()
where = (
"WHERE account_move_line.account_id = %s "
- "AND NOT account_move_line.reconciled"
+ "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
@@ -78,7 +81,7 @@ class MassReconcileBase(models.AbstractModel):
params = [self.account_id.id]
if self.partner_ids:
where += " AND account_move_line.partner_id IN %s"
- params.append(tuple([l.id for l in self.partner_ids]))
+ params.append(tuple([line.id for line in self.partner_ids]))
return where, params
def _get_filter(self):
@@ -95,16 +98,29 @@ class MassReconcileBase(models.AbstractModel):
def _below_writeoff_limit(self, lines, writeoff_limit):
self.ensure_one()
precision = self.env["decimal.precision"].precision_get("Account")
- keys = ("debit", "credit")
- sums = reduce(
- lambda line, memo: {
- key: value + memo[key] for key, value in line.items() if key in keys
- },
- lines,
+
+ 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,
)
- debit, credit = sums["debit"], sums["credit"]
- writeoff_amount = round(debit - credit, precision)
- return bool(writeoff_limit >= abs(writeoff_amount)), debit, credit
def _get_rec_date(self, lines, based_on="end_period_last_credit"):
self.ensure_one()
@@ -113,10 +129,10 @@ class MassReconcileBase(models.AbstractModel):
return max(mlines, key=itemgetter("date"))
def credit(mlines):
- return [l for l in mlines if l["credit"] > 0]
+ return [line for line in mlines if line["credit"] > 0]
def debit(mlines):
- return [l for l in mlines if l["debit"] > 0]
+ return [line for line in mlines if line["debit"] > 0]
if based_on == "newest":
return last_date(lines)["date"]
@@ -128,11 +144,51 @@ class MassReconcileBase(models.AbstractModel):
# 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")
+ write_off_vals = {
+ "name": _("Automatic writeoff"),
+ "amount_currency": same_curr and amount_curr or 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"),
+ "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 l: l.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
+ 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
@@ -143,36 +199,26 @@ class MassReconcileBase(models.AbstractModel):
"""
self.ensure_one()
ml_obj = self.env["account.move.line"]
- below_writeoff, sum_debit, sum_credit = self._below_writeoff_limit(
- lines, self.write_off
- )
+ (
+ 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([l["id"] for l in lines]).with_context(
+ line_rs = ml_obj.browse([line["id"] for line in lines]).with_context(
date_p=rec_date, comment=_("Automatic Write Off")
)
if below_writeoff:
- if sum_credit > sum_debit:
- writeoff_account = self.account_profit_id
- else:
- writeoff_account = self.account_lost_id
- line_rs.reconcile(
- writeoff_acc_id=writeoff_account, writeoff_journal_id=self.journal_id
- )
+ 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:
- # We need to give a writeoff_acc_id
- # in case we have a multi currency lines
- # to reconcile.
- # If amount in currency is equal between
- # lines to reconcile
- # it will do a full reconcile instead of a partial reconcile
- # and make a write-off for exchange
- if sum_credit > sum_debit:
- writeoff_account = self.income_exchange_account_id
- else:
- writeoff_account = self.expense_exchange_account_id
- line_rs.reconcile(
- writeoff_acc_id=writeoff_account, writeoff_journal_id=self.journal_id
- )
+ line_rs.reconcile()
return True, False
return False, False
diff --git a/account_mass_reconcile/models/mass_reconcile.py b/account_mass_reconcile/models/mass_reconcile.py
index a5ec4606..519bcb5e 100644
--- a/account_mass_reconcile/models/mass_reconcile.py
+++ b/account_mass_reconcile/models/mass_reconcile.py
@@ -5,9 +5,10 @@
import logging
from datetime import datetime
+import psycopg2
from psycopg2.extensions import AsIs
-from odoo import _, api, fields, models, sql_db
+from odoo import _, api, exceptions, fields, models, sql_db
from odoo.exceptions import Warning as UserError
_logger = logging.getLogger(__name__)
@@ -40,12 +41,6 @@ class MassReconcileOptions(models.AbstractModel):
default="newest",
)
_filter = fields.Char(string="Filter")
- income_exchange_account_id = fields.Many2one(
- "account.account", string="Gain Exchange Rate Account"
- )
- expense_exchange_account_id = fields.Many2one(
- "account.account", string="Loss Exchange Rate Account"
- )
class AccountMassReconcileMethod(models.Model):
@@ -61,6 +56,7 @@ class AccountMassReconcileMethod(models.Model):
("mass.reconcile.simple.partner", "Simple. Amount and Partner"),
("mass.reconcile.simple.reference", "Simple. Amount and Reference"),
("mass.reconcile.advanced.ref", "Advanced. Partner and Ref."),
+ ("mass.reconcile.advanced.name", "Advanced. Partner and Name."),
]
def _selection_name(self):
@@ -136,13 +132,16 @@ class AccountMassReconcile(models.Model):
"write_off": rec_method.write_off,
"account_lost_id": (rec_method.account_lost_id.id),
"account_profit_id": (rec_method.account_profit_id.id),
- "income_exchange_account_id": (rec_method.income_exchange_account_id.id),
- "expense_exchange_account_id": (rec_method.income_exchange_account_id.id),
"journal_id": (rec_method.journal_id.id),
"date_base_on": rec_method.date_base_on,
"_filter": rec_method._filter,
}
+ def _run_reconcile_method(self, reconcile_method):
+ rec_model = self.env[reconcile_method.name]
+ auto_rec_id = rec_model.create(self._prepare_run_transient(reconcile_method))
+ return auto_rec_id.automatic_reconcile()
+
def run_reconcile(self):
def find_reconcile_ids(fieldname, move_line_ids):
if not move_line_ids:
@@ -163,21 +162,36 @@ class AccountMassReconcile(models.Model):
# does not.
for rec in self:
+ # SELECT FOR UPDATE the mass reconcile row ; this is done in order
+ # to avoid 2 processes on the same mass reconcile method.
+ try:
+ self.env.cr.execute(
+ "SELECT id FROM account_mass_reconcile"
+ " WHERE id = %s"
+ " FOR UPDATE NOWAIT",
+ (rec.id,),
+ )
+ except psycopg2.OperationalError:
+ raise exceptions.UserError(
+ _(
+ "A mass reconcile is already ongoing for this account, "
+ "please try again later."
+ )
+ )
ctx = self.env.context.copy()
ctx["commit_every"] = rec.account.company_id.reconciliation_commit_every
if ctx["commit_every"]:
new_cr = sql_db.db_connect(self.env.cr.dbname).cursor()
+ new_env = api.Environment(new_cr, self.env.uid, ctx)
else:
new_cr = self.env.cr
+ new_env = self.env
try:
all_ml_rec_ids = []
for method in rec.reconcile_method:
- rec_model = self.env[method.name]
- auto_rec_id = rec_model.create(self._prepare_run_transient(method))
-
- ml_rec_ids = auto_rec_id.automatic_reconcile()
+ ml_rec_ids = self.with_env(new_env)._run_reconcile_method(method)
all_ml_rec_ids += ml_rec_ids
diff --git a/account_mass_reconcile/models/simple_reconciliation.py b/account_mass_reconcile/models/simple_reconciliation.py
index f55c4801..7ce6ff1d 100644
--- a/account_mass_reconcile/models/simple_reconciliation.py
+++ b/account_mass_reconcile/models/simple_reconciliation.py
@@ -2,8 +2,12 @@
# Copyright 2010 Sébastien Beau
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+import logging
+
from odoo import models
+_logger = logging.getLogger(__name__)
+
class MassReconcileSimple(models.AbstractModel):
_name = "mass.reconcile.simple"
@@ -40,6 +44,15 @@ class MassReconcileSimple(models.AbstractModel):
if reconciled:
res += [credit_line["id"], debit_line["id"]]
del lines[i]
+ if (
+ self.env.context.get("commit_every", 0)
+ and len(res) % self.env.context["commit_every"] == 0
+ ):
+ # new cursor is already open in cron
+ self.env.cr.commit() # pylint: disable=invalid-commit
+ _logger.info(
+ "Commit the reconciliations after %d groups", len(res)
+ )
break
count += 1
return res
@@ -58,7 +71,7 @@ class MassReconcileSimple(models.AbstractModel):
query = " ".join(
(select, self._from_query(), where, where2, self._simple_order())
)
-
+ self.flush()
self.env.cr.execute(query, params + params2)
lines = self.env.cr.dictfetchall()
return self.rec_auto_lines_simple(lines)
diff --git a/account_mass_reconcile/security/ir.model.access.csv b/account_mass_reconcile/security/ir.model.access.csv
index dc2e3435..786158c8 100644
--- a/account_mass_reconcile/security/ir.model.access.csv
+++ b/account_mass_reconcile/security/ir.model.access.csv
@@ -10,3 +10,5 @@ access_mass_reconcile_history_acc_mgr,mass.reconcile.history,model_mass_reconcil
access_mass_reconcile_simple_name,mass.reconcile.simple.name,model_mass_reconcile_simple_name,account.group_account_user,1,1,1,1
access_mass_reconcile_simple_partner,mass.reconcile.simple.partner,model_mass_reconcile_simple_partner,account.group_account_user,1,1,1,1
access_mass_reconcile_simple_reference,mass.reconcile.simple.reference,model_mass_reconcile_simple_reference,account.group_account_user,1,1,1,1
+access_mass_reconcile_advanced_ref_acc_user,mass.reconcile.advanced.ref,model_mass_reconcile_advanced_ref,account.group_account_user,1,1,1,1
+access_mass_reconcile_advanced_name_acc_user,mass.reconcile.advanced.name,model_mass_reconcile_advanced_name,account.group_account_user,1,1,1,1
diff --git a/account_mass_reconcile/tests/__init__.py b/account_mass_reconcile/tests/__init__.py
index fe1df9fe..89f15015 100644
--- a/account_mass_reconcile/tests/__init__.py
+++ b/account_mass_reconcile/tests/__init__.py
@@ -2,6 +2,5 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_onchange_company
-from . import test_reconcile_history
from . import test_reconcile
from . import test_scenario_reconcile
diff --git a/account_mass_reconcile/tests/test_onchange_company.py b/account_mass_reconcile/tests/test_onchange_company.py
index 9db6eef4..1730bb86 100644
--- a/account_mass_reconcile/tests/test_onchange_company.py
+++ b/account_mass_reconcile/tests/test_onchange_company.py
@@ -1,8 +1,6 @@
# © 2014-2016 Camptocamp SA (Damien Crier)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-from odoo import tools
-from odoo.modules import get_module_resource
from odoo.tests import common
@@ -10,15 +8,6 @@ class TestOnChange(common.SavepointCase):
@classmethod
def setUpClass(cls):
super(TestOnChange, cls).setUpClass()
- tools.convert_file(
- cls.cr,
- "account",
- get_module_resource("account", "test", "account_minimal_test.xml"),
- {},
- "init",
- False,
- "test",
- )
acc_setting = cls.env["res.config.settings"]
cls.acc_setting_obj = acc_setting.create({})
cls.company_obj = cls.env["res.company"]
diff --git a/account_mass_reconcile/tests/test_reconcile.py b/account_mass_reconcile/tests/test_reconcile.py
index 06901e8d..3c176e12 100644
--- a/account_mass_reconcile/tests/test_reconcile.py
+++ b/account_mass_reconcile/tests/test_reconcile.py
@@ -1,29 +1,24 @@
# © 2014-2016 Camptocamp SA (Damien Crier)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-from odoo import exceptions, fields, tools
-from odoo.modules import get_module_resource
-from odoo.tests import common
+import odoo.tests
+from odoo import exceptions, fields
+
+from odoo.addons.account.tests.common import TestAccountReconciliationCommon
-class TestReconcile(common.SavepointCase):
+@odoo.tests.tagged("post_install", "-at_install")
+class TestReconcile(TestAccountReconciliationCommon):
@classmethod
def setUpClass(cls):
super(TestReconcile, cls).setUpClass()
- tools.convert_file(
- cls.cr,
- "account",
- get_module_resource("account", "test", "account_minimal_test.xml"),
- {},
- "init",
- False,
- "test",
- )
cls.rec_history_obj = cls.env["mass.reconcile.history"]
cls.mass_rec_obj = cls.env["account.mass.reconcile"]
cls.mass_rec_method_obj = cls.env["account.mass.reconcile.method"]
+
+ cls.sale_journal = cls.company_data["default_journal_sale"]
cls.mass_rec = cls.mass_rec_obj.create(
- {"name": "AER2", "account": cls.env.ref("account.a_salary_expense").id}
+ {"name": "Sale Account", "account": cls.sale_journal.default_account_id.id}
)
cls.mass_rec_method = cls.mass_rec_method_obj.create(
{
@@ -33,7 +28,7 @@ class TestReconcile(common.SavepointCase):
}
)
cls.mass_rec_no_history = cls.mass_rec_obj.create(
- {"name": "AER3", "account": cls.env.ref("account.a_salary_expense").id}
+ {"name": "AER3", "account": cls.sale_journal.default_account_id.id}
)
cls.rec_history = cls.rec_history_obj.create(
{"mass_reconcile_id": cls.mass_rec.id, "date": fields.Datetime.now()}
@@ -57,4 +52,14 @@ class TestReconcile(common.SavepointCase):
def test_prepare_run_transient(self):
res = self.mass_rec._prepare_run_transient(self.mass_rec_method)
- self.assertEqual(self.ref("account.a_salary_expense"), res.get("account_id", 0))
+ self.assertEqual(
+ self.sale_journal.default_account_id.id, res.get("account_id", 0)
+ )
+
+ def test_open_full_empty(self):
+ res = self.rec_history._open_move_lines()
+ self.assertEqual([("id", "in", [])], res.get("domain", []))
+
+ def test_open_full_empty_from_method(self):
+ res = self.rec_history.open_reconcile()
+ self.assertEqual([("id", "in", [])], res.get("domain", []))
diff --git a/account_mass_reconcile/tests/test_reconcile_history.py b/account_mass_reconcile/tests/test_reconcile_history.py
deleted file mode 100644
index 89ea2647..00000000
--- a/account_mass_reconcile/tests/test_reconcile_history.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# © 2014-2016 Camptocamp SA (Damien Crier)
-# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-
-from odoo import fields, tools
-from odoo.modules import get_module_resource
-from odoo.tests import common
-
-
-class TestReconcileHistory(common.SavepointCase):
- @classmethod
- def setUpClass(cls):
- super(TestReconcileHistory, cls).setUpClass()
- tools.convert_file(
- cls.cr,
- "account",
- get_module_resource("account", "test", "account_minimal_test.xml"),
- {},
- "init",
- False,
- "test",
- )
- cls.rec_history_obj = cls.env["mass.reconcile.history"]
- cls.mass_rec_obj = cls.env["account.mass.reconcile"]
- cls.mass_rec = cls.mass_rec_obj.create(
- {"name": "AER1", "account": cls.env.ref("account.a_expense").id}
- )
- cls.rec_history = cls.rec_history_obj.create(
- {"mass_reconcile_id": cls.mass_rec.id, "date": fields.Datetime.now()}
- )
-
- def test_open_full_empty(self):
- res = self.rec_history._open_move_lines()
- self.assertEqual([("id", "in", [])], res.get("domain", []))
-
- def test_open_full_empty_from_method(self):
- res = self.rec_history.open_reconcile()
- self.assertEqual([("id", "in", [])], res.get("domain", []))
diff --git a/account_mass_reconcile/tests/test_scenario_reconcile.py b/account_mass_reconcile/tests/test_scenario_reconcile.py
index 9ef536f5..09fba673 100644
--- a/account_mass_reconcile/tests/test_scenario_reconcile.py
+++ b/account_mass_reconcile/tests/test_scenario_reconcile.py
@@ -1,24 +1,19 @@
# © 2014-2016 Camptocamp SA (Damien Crier)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-from odoo import fields, tools
-from odoo.modules import get_module_resource
-from odoo.tests import common
+from datetime import timedelta
+
+import odoo.tests
+from odoo import fields
+
+from odoo.addons.account.tests.common import TestAccountReconciliationCommon
-class TestScenarioReconcile(common.SavepointCase):
+@odoo.tests.tagged("post_install", "-at_install")
+class TestScenarioReconcile(TestAccountReconciliationCommon):
@classmethod
def setUpClass(cls):
super(TestScenarioReconcile, cls).setUpClass()
- tools.convert_file(
- cls.cr,
- "account",
- get_module_resource("account", "test", "account_minimal_test.xml"),
- {},
- "init",
- False,
- "test",
- )
cls.rec_history_obj = cls.env["mass.reconcile.history"]
cls.mass_rec_obj = cls.env["account.mass.reconcile"]
cls.invoice_obj = cls.env["account.move"]
@@ -26,13 +21,12 @@ class TestScenarioReconcile(common.SavepointCase):
cls.bk_stmt_line_obj = cls.env["account.bank.statement.line"]
cls.acc_move_line_obj = cls.env["account.move.line"]
cls.mass_rec_method_obj = cls.env["account.mass.reconcile.method"]
- cls.account_fx_income_id = cls.env.ref("account.income_fx_income").id
- cls.account_fx_expense_id = cls.env.ref("account.income_fx_expense").id
cls.acs_model = cls.env["res.config.settings"]
- acs_ids = cls.acs_model.search(
- [("company_id", "=", cls.env.ref("base.main_company").id)]
- )
+ cls.company = cls.company_data["company"]
+ cls.bank_journal = cls.company_data["default_journal_bank"]
+ cls.sale_journal = cls.company_data["default_journal_sale"]
+ acs_ids = cls.acs_model.search([("company_id", "=", cls.company.id)])
values = {"group_multi_currency": True}
@@ -44,185 +38,193 @@ class TestScenarioReconcile(common.SavepointCase):
acs_ids = cls.acs_model.create(default_vals)
def test_scenario_reconcile(self):
- # create invoice
- invoice = self.invoice_obj.with_context(default_type="out_invoice").create(
- {
- "type": "out_invoice",
- "company_id": self.ref("base.main_company"),
- "journal_id": self.ref("account.sales_journal"),
- "partner_id": self.ref("base.res_partner_12"),
- "invoice_line_ids": [
- (
- 0,
- 0,
- {
- "name": "[FURN_7800] Desk Combination",
- "account_id": self.ref("account.a_sale"),
- "price_unit": 1000.0,
- "quantity": 1.0,
- "product_id": self.ref("product.product_product_3"),
- },
- )
- ],
- }
- )
- # validate invoice
- invoice.post()
+ invoice = self.create_invoice()
self.assertEqual("posted", invoice.state)
- # create bank_statement
- statement = self.bk_stmt_obj.create(
+ receivalble_account_id = invoice.partner_id.property_account_receivable_id.id
+ # create payment
+ payment = self.env["account.payment"].create(
{
- "balance_end_real": 0.0,
- "balance_start": 0.0,
- "date": fields.Date.today(),
- "journal_id": self.ref("account.bank_journal"),
- "line_ids": [
- (
- 0,
- 0,
- {
- "amount": 1000.0,
- "partner_id": self.ref("base.res_partner_12"),
- "name": invoice.name,
- "ref": invoice.name,
- },
- )
- ],
+ "partner_type": "customer",
+ "payment_type": "inbound",
+ "partner_id": invoice.partner_id.id,
+ "destination_account_id": receivalble_account_id,
+ "amount": 50.0,
+ "journal_id": self.bank_journal.id,
}
)
-
- # reconcile
- line_id = None
- for l in invoice.line_ids:
- if l.account_id.internal_type == "receivable":
- line_id = l
- break
-
- for statement_line in statement.line_ids:
- statement_line.process_reconciliation(
- [
- {
- "move_line": line_id,
- "credit": 1000.0,
- "debit": 0.0,
- "name": invoice.name,
- }
- ]
- )
-
- # unreconcile journal item created by previous reconciliation
- lines_to_unreconcile = self.acc_move_line_obj.search(
- [("reconciled", "=", True), ("statement_id", "=", statement.id)]
- )
- lines_to_unreconcile.remove_move_reconcile()
+ payment.action_post()
# create the mass reconcile record
mass_rec = self.mass_rec_obj.create(
{
"name": "mass_reconcile_1",
- "account": line_id.account_id.id,
+ "account": invoice.partner_id.property_account_receivable_id.id,
"reconcile_method": [(0, 0, {"name": "mass.reconcile.simple.partner"})],
}
)
# call the automatic reconcilation method
mass_rec.run_reconcile()
- invoice.invalidate_cache()
- self.assertEqual("paid", invoice.invoice_payment_state)
+ self.assertEqual("paid", invoice.payment_state)
def test_scenario_reconcile_currency(self):
# create currency rate
self.env["res.currency.rate"].create(
{
- "name": fields.Date.today().strftime("%Y-%m-%d") + " 00:00:00",
+ "name": fields.Date.today(),
"currency_id": self.ref("base.USD"),
- "rate": 1.5,
+ "rate": 1.25,
}
)
# create invoice
- invoice = self.invoice_obj.with_context(default_type="out_invoice").create(
- {
- "type": "out_invoice",
- "company_id": self.ref("base.main_company"),
- "currency_id": self.ref("base.USD"),
- "journal_id": self.ref("account.sales_journal"),
- "partner_id": self.ref("base.res_partner_12"),
- "invoice_line_ids": [
- (
- 0,
- 0,
- {
- "name": "[FURN_7800] Desk Combination",
- "account_id": self.ref("account.a_sale"),
- "price_unit": 1000.0,
- "quantity": 1.0,
- "product_id": self.ref("product.product_product_3"),
- },
- )
- ],
- }
+ invoice = self._create_invoice(
+ currency_id=self.ref("base.USD"),
+ date_invoice=fields.Date.today(),
+ auto_validate=True,
)
- # validate invoice
- invoice.post()
self.assertEqual("posted", invoice.state)
- # create bank_statement
- statement = self.bk_stmt_obj.create(
+ self.env["res.currency.rate"].create(
{
- "balance_end_real": 0.0,
- "balance_start": 0.0,
- "date": fields.Date.today(),
- "journal_id": self.ref("account.bank_journal_usd"),
+ "name": fields.Date.today() - timedelta(days=3),
"currency_id": self.ref("base.USD"),
- "line_ids": [
- (
- 0,
- 0,
- {
- "amount": 1000.0,
- "amount_currency": 1500.0,
- "partner_id": self.ref("base.res_partner_12"),
- "name": invoice.name,
- "ref": invoice.name,
- },
- )
- ],
+ "rate": 2,
}
)
-
- # reconcile
- line_id = None
- for l in invoice.line_ids:
- if l.account_id.internal_type == "receivable":
- line_id = l
- break
-
- for statement_line in statement.line_ids:
- statement_line.process_reconciliation(
- [
- {
- "move_line": line_id,
- "credit": 1000.0,
- "debit": 0.0,
- "name": invoice.name,
- }
- ]
- )
- # unreconcile journal item created by previous reconciliation
- lines_to_unreconcile = self.acc_move_line_obj.search(
- [("reconciled", "=", True), ("statement_id", "=", statement.id)]
+ receivable_account_id = invoice.partner_id.property_account_receivable_id.id
+ # create payment
+ payment = self.env["account.payment"].create(
+ {
+ "partner_type": "customer",
+ "payment_type": "inbound",
+ "partner_id": invoice.partner_id.id,
+ "destination_account_id": receivable_account_id,
+ "amount": 50.0,
+ "currency_id": self.ref("base.USD"),
+ "journal_id": self.bank_journal.id,
+ "date": fields.Date.today() - timedelta(days=2),
+ }
)
- lines_to_unreconcile.remove_move_reconcile()
+ payment.action_post()
# create the mass reconcile record
mass_rec = self.mass_rec_obj.create(
{
"name": "mass_reconcile_1",
- "account": line_id.account_id.id,
+ "account": invoice.partner_id.property_account_receivable_id.id,
"reconcile_method": [(0, 0, {"name": "mass.reconcile.simple.partner"})],
}
)
# call the automatic reconcilation method
mass_rec.run_reconcile()
- invoice.invalidate_cache()
- self.assertEqual("paid", invoice.invoice_payment_state)
+ self.assertEqual("paid", invoice.payment_state)
+
+ def test_scenario_reconcile_partial(self):
+ invoice1 = self.create_invoice()
+ invoice1.ref = "test ref"
+ # create payment
+ receivable_account_id = invoice1.partner_id.property_account_receivable_id.id
+ payment = self.env["account.payment"].create(
+ {
+ "partner_type": "customer",
+ "payment_type": "inbound",
+ "partner_id": invoice1.partner_id.id,
+ "destination_account_id": receivable_account_id,
+ "amount": 500.0,
+ "journal_id": self.bank_journal.id,
+ "ref": "test ref",
+ }
+ )
+ payment.action_post()
+ line_payment = payment.line_ids.filtered(
+ lambda l: l.account_id.id == receivable_account_id
+ )
+ self.assertEqual(line_payment.reconciled, False)
+ invoice1_line = invoice1.line_ids.filtered(
+ lambda l: l.account_id.id == receivable_account_id
+ )
+ self.assertEqual(invoice1_line.reconciled, False)
+
+ # Create the mass reconcile record
+ reconcile_method_vals = {
+ "name": "mass.reconcile.advanced.ref",
+ "write_off": 0.1,
+ }
+ mass_rec = self.mass_rec_obj.create(
+ {
+ "name": "mass_reconcile_1",
+ "account": receivable_account_id,
+ "reconcile_method": [(0, 0, reconcile_method_vals)],
+ }
+ )
+ mass_rec.run_reconcile()
+
+ self.assertEqual(line_payment.amount_residual, -450.0)
+ self.assertEqual(invoice1_line.reconciled, True)
+ invoice2 = self._create_invoice(invoice_amount=500, auto_validate=True)
+ invoice2.ref = "test ref"
+ invoice2_line = invoice2.line_ids.filtered(
+ lambda l: l.account_id.id == receivable_account_id
+ )
+ mass_rec.run_reconcile()
+ self.assertEqual(line_payment.reconciled, True)
+ self.assertEqual(invoice2_line.reconciled, False)
+
+ self.assertEqual(invoice2_line.amount_residual, 50.0)
+
+ def test_reconcile_with_writeoff(self):
+ invoice = self.create_invoice()
+
+ receivable_account_id = invoice.partner_id.property_account_receivable_id.id
+ # create payment
+ payment = self.env["account.payment"].create(
+ {
+ "partner_type": "customer",
+ "payment_type": "inbound",
+ "partner_id": invoice.partner_id.id,
+ "destination_account_id": receivable_account_id,
+ "amount": 50.1,
+ "journal_id": self.bank_journal.id,
+ }
+ )
+ payment.action_post()
+
+ # create the mass reconcile record
+ mass_rec = self.mass_rec_obj.create(
+ {
+ "name": "mass_reconcile_1",
+ "account": invoice.partner_id.property_account_receivable_id.id,
+ "reconcile_method": [
+ (
+ 0,
+ 0,
+ {
+ "name": "mass.reconcile.simple.partner",
+ "account_lost_id": self.company_data[
+ "default_account_expense"
+ ].id,
+ "account_profit_id": self.company_data[
+ "default_account_revenue"
+ ].id,
+ "journal_id": self.company_data["default_journal_misc"].id,
+ "write_off": 0.05,
+ },
+ )
+ ],
+ }
+ )
+ # call the automatic reconcilation method
+ mass_rec.run_reconcile()
+ self.assertEqual("not_paid", invoice.payment_state)
+ mass_rec.reconcile_method.write_off = 0.11
+ mass_rec.run_reconcile()
+ self.assertEqual("paid", invoice.payment_state)
+ full_reconcile = invoice.line_ids.mapped("full_reconcile_id")
+ writeoff_line = full_reconcile.reconciled_line_ids.filtered(
+ lambda l: l.debit == 0.1
+ )
+ self.assertEqual(len(writeoff_line), 1)
+ self.assertEqual(
+ writeoff_line.move_id.journal_id.id,
+ self.company_data["default_journal_misc"].id,
+ )
diff --git a/account_mass_reconcile/views/mass_reconcile.xml b/account_mass_reconcile/views/mass_reconcile.xml
index ce59569a..7a39e161 100644
--- a/account_mass_reconcile/views/mass_reconcile.xml
+++ b/account_mass_reconcile/views/mass_reconcile.xml
@@ -173,14 +173,6 @@ The lines should have the same partner, and the credit entry ref. is matched wit
name="account_profit_id"
attrs="{'required':[('write_off','>',0)]}"
/>
-
-