[ADD] module account_move_line_reconcile_manual

pull/532/head
Alexis de Lattre 2023-03-04 09:38:28 +01:00
parent 0fd9c80976
commit 526f383473
15 changed files with 456 additions and 0 deletions

View File

@ -0,0 +1 @@
Will be auto-generated from readme subdir

View File

@ -0,0 +1 @@
from . import wizards

View File

@ -0,0 +1,21 @@
# Copyright 2023 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Account Move Line Reconcile Manual",
"version": "16.0.1.0.0",
"category": "Accounting",
"license": "AGPL-3",
"summary": "Manually reconcile Journal Items",
"author": "Akretion,Odoo Community Association (OCA)",
"maintainers": ["alexis-via"],
"website": "https://github.com/OCA/account-reconcile",
"depends": ["account"],
"data": [
"security/ir.model.access.csv",
"wizards/account_move_line_reconcile_manual_view.xml",
"views/account_move_line.xml",
],
"installable": True,
}

View File

@ -0,0 +1 @@
* Alexis de Lattre <alexis.delattre@akretion.com>

View File

@ -0,0 +1,15 @@
This module adds a wizard to reconcile manually selected journal items. If the selected journal items are balanced, it will propose a full reconcile. Otherwise, the user will have to choose between partial reconciliation and full reconciliation with a write-off.
For the old-time Odoo users, the feature provided by this module is similar to the wizard that was provided in the **account** module up to Odoo 11.0. It was later replaced by the special reconciliation JS interface, which was working well, but was not as fast and convenient.
Full reconciliation:
.. figure:: ../static/description/sshot_full_rec.png
:alt: Full reconciliation
Choose between partial reconciliation and full reconciliation with a write-off:
.. figure:: ../static/description/sshot_partial_rec.png
:alt: Choose between partial reconciliation and full reconciliation with a write-off
Reconcile with write-off:
.. figure:: ../static/description/sshot_rec_writeoff.png
:alt: Reconcile with write-off

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_account_move_line_reconcile_manual,Full access on account.move.line.reconcile.manual wizard,model_account_move_line_reconcile_manual,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
2 access_account_move_line_reconcile_manual Full access on account.move.line.reconcile.manual wizard model_account_move_line_reconcile_manual account.group_account_user 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2023 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_move_line_tree" model="ir.ui.view">
<field name="model">account.move.line</field>
<field name="inherit_id" ref="account.view_move_line_tree" />
<field name="arch" type="xml">
<field name="move_id" position="before">
<header>
<button
name="%(account_move_line_reconcile_manual_action)d"
type="action"
string="Reconcile"
groups="account.group_account_user"
/>
</header>
</field>
</field>
</record>
</odoo>

View File

@ -0,0 +1 @@
from . import account_move_line_reconcile_manual

View File

@ -0,0 +1,282 @@
# Copyright 2023 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import logging
from odoo import _, api, fields, models
from odoo.exceptions import UserError
logger = logging.getLogger(__name__)
class AccountMoveLineReconcileManual(models.TransientModel):
_name = "account.move.line.reconcile.manual"
_description = "Manual Reconciliation Wizard"
_check_company_auto = True
_inherit = "analytic.mixin"
account_id = fields.Many2one(
"account.account", required=True, readonly=True, check_company=True
)
company_id = fields.Many2one("res.company", required=True, readonly=True)
company_currency_id = fields.Many2one(related="company_id.currency_id")
count = fields.Integer(string="# of Journal Items", readonly=True)
total_debit = fields.Monetary(currency_field="company_currency_id", readonly=True)
total_credit = fields.Monetary(currency_field="company_currency_id", readonly=True)
move_line_ids = fields.Many2many(
"account.move.line", readonly=True, check_company=True
)
partner_count = fields.Integer(readonly=True)
partner_id = fields.Many2one("res.partner", readonly=True)
state = fields.Selection(
[
("start", "Start"),
("writeoff", "Write-off"),
],
readonly=True,
default="start",
)
# START WRITE-OFF FIELDS
writeoff_journal_id = fields.Many2one(
"account.journal",
string="Journal",
domain="[('type', '=', 'general'), ('company_id', '=', company_id)]",
check_company=True,
)
writeoff_date = fields.Date(string="Date", default=fields.Date.context_today)
writeoff_ref = fields.Char(string="Reference", default=lambda self: _("Write-off"))
writeoff_type = fields.Selection(
[
("income", "Income"),
("expense", "Expense"),
("none", "None"),
],
readonly=True,
string="Type",
)
writeoff_amount = fields.Monetary(
currency_field="company_currency_id", readonly=True, string="Amount"
)
writeoff_account_id = fields.Many2one(
"account.account",
string="Write-off Account",
domain="[('company_id', '=', company_id), ('deprecated', '=', False)]",
check_company=True,
)
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
if self._context.get("active_model") == self._name: # write-off step
return res
assert self._context.get("active_model") == "account.move.line"
move_lines = self.env["account.move.line"].browse(
self._context.get("active_ids")
)
company = move_lines[0].account_id.company_id
ccur = company.currency_id
count = 0
account = False
total_debit = total_credit = 0.0
partner_set = set()
for line in move_lines:
count += 1
total_debit += line.debit
total_credit += line.credit
if line.full_reconcile_id:
raise UserError(
_("Line '%s' is already fully reconciled.") % line.display_name
)
if account:
if account != line.account_id:
raise UserError(
_(
"The Journal Items selected have different accounts: "
"%(account1)s and %(account2)s.",
account1=account.code,
account2=line.account_id.code,
)
)
else:
account = line.account_id
if line.partner_id:
partner_set.add(line.partner_id.id)
# if lines have the same account, they are in the same company
if not account.reconcile:
raise UserError(
_("Account '%s' is not reconciliable.") % account.display_name
)
if count <= 1:
raise UserError(_("You must select at least 2 journal items!"))
if ccur.is_zero(total_debit):
raise UserError(_("You selected only credit journal items."))
if ccur.is_zero(total_credit):
raise UserError(_("You selected only debit journal items."))
writeoff_amount = ccur.round(abs(total_debit - total_credit))
total_debit = ccur.round(total_debit)
total_credit = ccur.round(total_credit)
compare_res = ccur.compare_amounts(total_debit, total_credit)
if compare_res > 0:
writeoff_type = "expense"
elif compare_res < 0:
writeoff_type = "income"
else:
writeoff_type = "none"
general_journals = self.env["account.journal"].search(
[("type", "=", "general"), ("company_id", "=", company.id)]
)
writeoff_journal_id = False
if len(general_journals) == 1:
writeoff_journal_id = general_journals.id
res.update(
{
"count": count,
"account_id": account.id,
"company_id": account.company_id.id,
"total_debit": total_debit,
"total_credit": total_credit,
"partner_count": len(partner_set),
"partner_id": len(partner_set) == 1 and partner_set.pop() or False,
"move_line_ids": move_lines.ids,
"writeoff_type": writeoff_type,
"writeoff_amount": writeoff_amount,
"writeoff_journal_id": writeoff_journal_id,
}
)
return res
def full_reconcile(self):
self.ensure_one()
self.move_line_ids.remove_move_reconcile()
res = self.move_line_ids.reconcile()
if not res.get("full_reconcile"):
raise UserError(_("Full reconciliation failed. It should never happen!"))
action = {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": _("Successful reconciliation"),
"message": _("Reconcile mark: %s") % res["full_reconcile"].display_name,
"next": {"type": "ir.actions.act_window_close"},
},
}
return action
def partial_reconcile(self):
self.ensure_one()
self.move_line_ids.remove_move_reconcile()
self.move_line_ids.reconcile()
return
def go_to_writeoff(self):
self.ensure_one()
self.write({"state": "writeoff"})
action = self.env["ir.actions.actions"]._for_xml_id(
"account_move_line_reconcile_manual.account_move_line_reconcile_manual_action"
)
action["res_id"] = self.id
return action
def _prepare_writeoff_move(self):
ccur = self.company_currency_id
bal = ccur.round(self.total_debit - self.total_credit)
compare_res = ccur.compare_amounts(bal, 0)
assert compare_res
if compare_res > 0:
credit = bal
debit = 0
else:
debit = bal * -1
credit = 0
vals = {
"company_id": self.company_id.id,
"journal_id": self.writeoff_journal_id.id,
"ref": self.writeoff_ref,
"date": self.writeoff_date,
"line_ids": [
(
0,
0,
{
"account_id": self.account_id.id,
"partner_id": self.partner_id and self.partner_id.id or False,
"debit": debit,
"credit": credit,
},
),
(
0,
0,
{
"account_id": self.writeoff_account_id.id,
"partner_id": self.partner_id and self.partner_id.id or False,
"debit": credit,
"credit": debit,
"analytic_distribution": self.analytic_distribution,
},
),
],
}
return vals
def reconcile_with_writeoff(self):
self.ensure_one()
assert self.writeoff_journal_id
assert self.writeoff_date
assert self.writeoff_account_id
assert self.state == "writeoff"
self.move_line_ids.remove_move_reconcile()
vals = self._prepare_writeoff_move()
woff_move = self.env["account.move"].create(vals)
woff_move._post(soft=False)
to_rec_woff_line = woff_move.line_ids.filtered(
lambda x: x.account_id.id == self.account_id.id
)
assert len(to_rec_woff_line) == 1
to_rec_lines = self.move_line_ids + to_rec_woff_line
res = to_rec_lines.reconcile()
if not res.get("full_reconcile"):
raise UserError(_("Full reconciliation failed. It should never happen!"))
action = {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": _("Successful reconciliation"),
"message": _(
"Write-off journal entry: %(writeoff_move)s\nReconcile mark: %(full_rec)s",
full_rec=res["full_reconcile"].display_name,
writeoff_move=woff_move.name,
),
"next": {"type": "ir.actions.act_window_close"},
},
}
return action
@api.onchange("writeoff_account_id")
def writeoff_account_id_change(self):
account = self.writeoff_account_id
if (
self.writeoff_type in ("income", "expense")
and account
and self.writeoff_type not in account.account_type
):
message = _(
"This is a/an '%(writeoff_type)s' write-off, "
"but you selected account %(account_code)s "
"which is a/an '%(account_type)s' account.",
writeoff_type=self._fields["writeoff_type"].convert_to_export(
self.writeoff_type, self
),
account_code=account.code,
account_type=account._fields["account_type"].convert_to_export(
account.account_type, account
),
)
res = {
"warning": {
"title": _("Bad write-off account type"),
"message": message,
}
}
return res

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2023 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="account_move_line_reconcile_manual_form" model="ir.ui.view">
<field name="model">account.move.line.reconcile.manual</field>
<field name="arch" type="xml">
<form>
<div
class="alert alert-warning"
role="alert"
attrs="{'invisible': [('partner_count', '&lt;', 2)]}"
>
You are trying to reconcile journal items from <field
name="partner_count"
/> different partners: make sure it is intented.
</div>
<group name="main">
<field name="account_id" />
<field name="count" />
<field name="total_debit" />
<field name="total_credit" />
<field name="state" invisible="1" />
<field name="company_id" invisible="1" />
<field name="company_currency_id" invisible="1" />
<field name="partner_count" invisible="1" />
<field name="partner_id" invisible="1" />
</group>
<group name="writeoff" string="Write-off" states="writeoff">
<field
name="writeoff_journal_id"
widget="selection"
attrs="{'required': [('state', '=', 'writeoff')]}"
/>
<field
name="writeoff_date"
attrs="{'required': [('state', '=', 'writeoff')]}"
options="{'datepicker': {'warn_future': true}}"
/>
<field name="writeoff_ref" />
<field name="writeoff_amount" />
<field name="writeoff_type" />
<field
name="writeoff_account_id"
attrs="{'required': [('state', '=', 'writeoff')]}"
/>
<field
name="analytic_distribution"
widget="analytic_distribution"
groups="analytic.group_analytic_accounting"
/>
</group>
<footer>
<button
name="full_reconcile"
type="object"
attrs="{'invisible': ['|', ('state', '!=', 'start'), ('writeoff_type', '!=', 'none')]}"
class="btn-primary"
string="Full Reconcile"
/>
<button
name="partial_reconcile"
type="object"
attrs="{'invisible': ['|', ('state', '!=', 'start'), ('writeoff_type', '=', 'none')]}"
class="btn-primary"
string="Partial Reconcile"
/>
<button
name="go_to_writeoff"
type="object"
attrs="{'invisible': ['|', ('state', '!=', 'start'), ('writeoff_type', '=', 'none')]}"
class="btn-primary"
string="Reconcile with Write-off"
/>
<button
name="reconcile_with_writeoff"
type="object"
states="writeoff"
class="btn-primary"
string="Reconcile with Write-off"
/>
<button special="cancel" string="Cancel" />
</footer>
</form>
</field>
</record>
<record id="account_move_line_reconcile_manual_action" model="ir.actions.act_window">
<field name="name">Reconcile</field>
<field name="res_model">account.move.line.reconcile.manual</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>

View File

@ -0,0 +1 @@
../../../../account_move_line_reconcile_manual

View File

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)