[ADD] module account_move_line_reconcile_manual
parent
0fd9c80976
commit
526f383473
|
@ -0,0 +1 @@
|
|||
Will be auto-generated from readme subdir
|
|
@ -0,0 +1 @@
|
|||
from . import wizards
|
|
@ -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,
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
* Alexis de Lattre <alexis.delattre@akretion.com>
|
|
@ -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
|
|
@ -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
|
|
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 |
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
from . import account_move_line_reconcile_manual
|
|
@ -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
|
|
@ -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', '<', 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>
|
|
@ -0,0 +1 @@
|
|||
../../../../account_move_line_reconcile_manual
|
|
@ -0,0 +1,6 @@
|
|||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['setuptools-odoo'],
|
||||
odoo_addon=True,
|
||||
)
|
Loading…
Reference in New Issue