Manage write off, some refactore to manage better currency, partial reconciliation, and add some tests

Also add an advanced reconciliation method based on name. Could be very useful to actually match account move line based
on payment_reference field on invoices, as it is passed to account move line on name field
pull/517/head
Florian da Costa 2021-04-01 15:04:39 +02:00 committed by sonhd91
parent b95dd38e50
commit 47108f791c
15 changed files with 507 additions and 374 deletions

View File

@ -14,13 +14,13 @@ Account Mass Reconcile
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--reconcile-lightgray.png?logo=github
:target: https://github.com/OCA/account-reconcile/tree/13.0/account_mass_reconcile
:target: https://github.com/OCA/account-reconcile/tree/14.0/account_mass_reconcile
:alt: OCA/account-reconcile
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/account-reconcile-13-0/account-reconcile-13-0-account_mass_reconcile
:target: https://translation.odoo-community.org/projects/account-reconcile-14-0/account-reconcile-14-0-account_mass_reconcile
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/98/13.0
:target: https://runbot.odoo-community.org/runbot/98/14.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
@ -62,7 +62,7 @@ Bug Tracker
Bugs are tracked on `GitHub Issues <https://github.com/OCA/account-reconcile/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/account-reconcile/issues/new?body=module:%20account_mass_reconcile%0Aversion:%2013.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
`feedback <https://github.com/OCA/account-reconcile/issues/new?body=module:%20account_mass_reconcile%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
@ -111,6 +111,6 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/account-reconcile <https://github.com/OCA/account-reconcile/tree/13.0/account_mass_reconcile>`_ project on GitHub.
This module is part of the `OCA/account-reconcile <https://github.com/OCA/account-reconcile/tree/14.0/account_mass_reconcile>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@ -4,7 +4,7 @@
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 13.0\n"
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
@ -13,6 +13,14 @@ msgstr ""
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: account_mass_reconcile
#: code:addons/account_mass_reconcile/models/mass_reconcile.py:0
#, python-format
msgid ""
"A mass reconcile is already ongoing for this account, please try again "
"later."
msgstr ""
#. module: account_mass_reconcile
#: code:addons/account_mass_reconcile/models/base_advanced_reconciliation.py:0
#, python-format
@ -33,6 +41,7 @@ msgstr ""
#. module: account_mass_reconcile
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile__account
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced__account_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_name__account_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_ref__account_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_base__account_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple__account_id
@ -45,6 +54,7 @@ msgstr ""
#. module: account_mass_reconcile
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile_method__account_lost_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced__account_lost_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_name__account_lost_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_ref__account_lost_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_base__account_lost_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_options__account_lost_id
@ -63,6 +73,7 @@ msgstr ""
#. module: account_mass_reconcile
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile_method__account_profit_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced__account_profit_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_name__account_profit_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_ref__account_profit_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_base__account_profit_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_options__account_profit_id
@ -113,6 +124,12 @@ msgstr ""
msgid "Automatic Write Off"
msgstr ""
#. module: account_mass_reconcile
#: code:addons/account_mass_reconcile/models/base_reconciliation.py:0
#, python-format
msgid "Automatic writeoff"
msgstr ""
#. module: account_mass_reconcile
#: model_terms:ir.actions.act_window,help:account_mass_reconcile.action_account_mass_reconcile
msgid "Click to add a reconciliation profile."
@ -148,6 +165,7 @@ msgstr ""
#. module: account_mass_reconcile
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile__create_uid
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile_method__create_uid
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_name__create_uid
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_ref__create_uid
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_history__create_uid
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple_name__create_uid
@ -159,6 +177,7 @@ msgstr ""
#. module: account_mass_reconcile
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile__create_date
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile_method__create_date
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_name__create_date
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_ref__create_date
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_history__create_date
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple_name__create_date
@ -170,6 +189,7 @@ msgstr ""
#. module: account_mass_reconcile
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile_method__date_base_on
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced__date_base_on
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_name__date_base_on
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_ref__date_base_on
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_base__date_base_on
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_options__date_base_on
@ -184,6 +204,7 @@ msgstr ""
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile__display_name
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile_method__display_name
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced__display_name
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_name__display_name
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_ref__display_name
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_base__display_name
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_history__display_name
@ -192,6 +213,8 @@ msgstr ""
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple_name__display_name
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple_partner__display_name
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple_reference__display_name
#: model:ir.model.fields,field_description:account_mass_reconcile.field_res_company__display_name
#: model:ir.model.fields,field_description:account_mass_reconcile.field_res_config_settings__display_name
msgid "Display Name"
msgstr ""
@ -211,6 +234,7 @@ msgstr ""
#. module: account_mass_reconcile
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile_method___filter
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced___filter
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_name___filter
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_ref___filter
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_base___filter
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_options___filter
@ -241,19 +265,6 @@ msgstr ""
msgid "Full Reconciliations"
msgstr ""
#. module: account_mass_reconcile
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile_method__income_exchange_account_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced__income_exchange_account_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_ref__income_exchange_account_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_base__income_exchange_account_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_options__income_exchange_account_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple__income_exchange_account_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple_name__income_exchange_account_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple_partner__income_exchange_account_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple_reference__income_exchange_account_id
msgid "Gain Exchange Rate Account"
msgstr ""
#. module: account_mass_reconcile
#: model_terms:ir.ui.view,arch_db:account_mass_reconcile.account_mass_reconcile_form
#: model_terms:ir.ui.view,arch_db:account_mass_reconcile.mass_reconcile_history_form
@ -277,11 +288,6 @@ msgstr ""
msgid "History"
msgstr ""
#. module: account_mass_reconcile
#: model:ir.actions.act_window,name:account_mass_reconcile.act_mass_reconcile_to_history
msgid "History Details"
msgstr ""
#. module: account_mass_reconcile
#: model:ir.model.fields,field_description:account_mass_reconcile.field_res_company__reconciliation_commit_every
#: model:ir.model.fields,field_description:account_mass_reconcile.field_res_config_settings__reconciliation_commit_every
@ -299,6 +305,7 @@ msgstr ""
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile__id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile_method__id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced__id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_name__id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_ref__id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_base__id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_history__id
@ -307,6 +314,8 @@ msgstr ""
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple_name__id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple_partner__id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple_reference__id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_res_company__id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_res_config_settings__id
msgid "ID"
msgstr ""
@ -335,6 +344,7 @@ msgstr ""
#. module: account_mass_reconcile
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile_method__journal_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced__journal_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_name__journal_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_ref__journal_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_base__journal_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_options__journal_id
@ -349,6 +359,7 @@ msgstr ""
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile____last_update
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile_method____last_update
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced____last_update
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_name____last_update
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_ref____last_update
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_base____last_update
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_history____last_update
@ -357,12 +368,15 @@ msgstr ""
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple_name____last_update
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple_partner____last_update
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple_reference____last_update
#: model:ir.model.fields,field_description:account_mass_reconcile.field_res_company____last_update
#: model:ir.model.fields,field_description:account_mass_reconcile.field_res_config_settings____last_update
msgid "Last Modified on"
msgstr ""
#. module: account_mass_reconcile
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile__write_uid
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile_method__write_uid
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_name__write_uid
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_ref__write_uid
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_history__write_uid
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple_name__write_uid
@ -374,6 +388,7 @@ msgstr ""
#. module: account_mass_reconcile
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile__write_date
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile_method__write_date
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_name__write_date
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_ref__write_date
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_history__write_date
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple_name__write_date
@ -393,19 +408,6 @@ msgstr ""
msgid "Leave zero to commit only at the end of the process."
msgstr ""
#. module: account_mass_reconcile
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile_method__expense_exchange_account_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced__expense_exchange_account_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_ref__expense_exchange_account_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_base__expense_exchange_account_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_options__expense_exchange_account_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple__expense_exchange_account_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple_name__expense_exchange_account_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple_partner__expense_exchange_account_id
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple_reference__expense_exchange_account_id
msgid "Loss Exchange Rate Account"
msgstr ""
#. module: account_mass_reconcile
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile__message_main_attachment_id
msgid "Main Attachment"
@ -427,6 +429,11 @@ msgstr ""
msgid "Mass Reconcile Advanced"
msgstr ""
#. module: account_mass_reconcile
#: model:ir.model,name:account_mass_reconcile.model_mass_reconcile_advanced_name
msgid "Mass Reconcile Advanced Name"
msgstr ""
#. module: account_mass_reconcile
#: model:ir.model,name:account_mass_reconcile.model_mass_reconcile_advanced_ref
msgid "Mass Reconcile Advanced Ref"
@ -578,6 +585,7 @@ msgstr ""
#. module: account_mass_reconcile
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced__partner_ids
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_name__partner_ids
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_ref__partner_ids
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_base__partner_ids
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_simple__partner_ids
@ -698,6 +706,7 @@ msgstr ""
#. module: account_mass_reconcile
#: model:ir.model.fields,field_description:account_mass_reconcile.field_account_mass_reconcile_method__write_off
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced__write_off
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_name__write_off
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_advanced_ref__write_off
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_base__write_off
#: model:ir.model.fields,field_description:account_mass_reconcile.field_mass_reconcile_options__write_off

View File

@ -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(),),
)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
10 access_mass_reconcile_simple_name mass.reconcile.simple.name model_mass_reconcile_simple_name account.group_account_user 1 1 1 1
11 access_mass_reconcile_simple_partner mass.reconcile.simple.partner model_mass_reconcile_simple_partner account.group_account_user 1 1 1 1
12 access_mass_reconcile_simple_reference mass.reconcile.simple.reference model_mass_reconcile_simple_reference account.group_account_user 1 1 1 1
13 access_mass_reconcile_advanced_ref_acc_user mass.reconcile.advanced.ref model_mass_reconcile_advanced_ref account.group_account_user 1 1 1 1
14 access_mass_reconcile_advanced_name_acc_user mass.reconcile.advanced.name model_mass_reconcile_advanced_name account.group_account_user 1 1 1 1

View File

@ -367,7 +367,7 @@ ul.auto-toc {
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/account-reconcile/tree/13.0/account_mass_reconcile"><img alt="OCA/account-reconcile" src="https://img.shields.io/badge/github-OCA%2Faccount--reconcile-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/account-reconcile-13-0/account-reconcile-13-0-account_mass_reconcile"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/98/13.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/account-reconcile/tree/14.0/account_mass_reconcile"><img alt="OCA/account-reconcile" src="https://img.shields.io/badge/github-OCA%2Faccount--reconcile-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/account-reconcile-14-0/account-reconcile-14-0-account_mass_reconcile"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/98/14.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>This is a shared work between Akretion and Camptocamp
in order to provide:</p>
<ul class="simple">
@ -410,7 +410,7 @@ reconcile.</p>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/account-reconcile/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/account-reconcile/issues/new?body=module:%20account_mass_reconcile%0Aversion:%2013.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<a class="reference external" href="https://github.com/OCA/account-reconcile/issues/new?body=module:%20account_mass_reconcile%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
@ -453,7 +453,7 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/account-reconcile/tree/13.0/account_mass_reconcile">OCA/account-reconcile</a> project on GitHub.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/account-reconcile/tree/14.0/account_mass_reconcile">OCA/account-reconcile</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>

View File

@ -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

View File

@ -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"]

View File

@ -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", []))

View File

@ -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", []))

View File

@ -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,
)

View File

@ -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)]}"
/>
<field
name="income_exchange_account_id"
groups="base.group_multi_currency"
/>
<field
name="expense_exchange_account_id"
groups="base.group_multi_currency"
/>
<field name="journal_id" attrs="{'required':[('write_off','>',0)]}" />
<field name="date_base_on" />
</tree>