first commit
This commit is contained in:
commit
90b85e947a
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
*.py[cod]
|
||||
__pycache__/
|
||||
*.swp
|
||||
*~
|
||||
.DS_Store
|
||||
24
README.md
Normal file
24
README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# Account Payment: Current Liability Override
|
||||
|
||||
## Overview
|
||||
By default, Odoo restricts the creation of payments strictly to accounts of type `asset_receivable` (Receivable) or `liability_payable` (Payable). This prevents businesses from cleanly mapping vendor or employee payables to `liability_current` (Current Liabilities) accounts, such as "Accrued Expenses".
|
||||
|
||||
This module overrides those strict native constraints to allow `liability_current` accounts to be natively selected and processed in payments.
|
||||
|
||||
## Features
|
||||
- Overrides the `property_account_payable_id` domain on the `res.partner` form to allow selecting `liability_current` as a valid payable account.
|
||||
- Overrides the `destination_account_id` domain in `account.payment` to support `liability_current`.
|
||||
- Overrides `_get_valid_payment_account_types()` in `account.payment` to natively include `liability_current` in the payment registration wizard validation check.
|
||||
|
||||
## Setup Requirements ⚠️
|
||||
Even with this module installed, Odoo still requires any account used for payment clearing/matching to allow reconciliation.
|
||||
|
||||
**You MUST:**
|
||||
1. Go to your Chart of Accounts.
|
||||
2. Locate the specific `Current Liabilities` account you wish to use (e.g., `216109`).
|
||||
3. Manually **tick** the `"Allow Reconciliation"` checkbox for that account.
|
||||
|
||||
Without this checkbox, Odoo will raise errors during the Payment Registration process because the journal items cannot be paired.
|
||||
|
||||
## Dependencies
|
||||
- `account`
|
||||
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import models
|
||||
14
__manifest__.py
Normal file
14
__manifest__.py
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
'name': 'Account Payment: Current Liability Override',
|
||||
'version': '17.0.1.0.0',
|
||||
'summary': 'Allow employees to use a Current Liability account (liability_current) as their payable account and process payments correctly.',
|
||||
'category': 'Accounting/Accounting',
|
||||
'author': 'Suherdy Yacob',
|
||||
'depends': ['account'],
|
||||
'data': [
|
||||
'views/account_move_line_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
4
models/__init__.py
Normal file
4
models/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from . import res_partner
|
||||
from . import account_payment
|
||||
from . import account_move_line
|
||||
from . import account_move
|
||||
140
models/account_move.py
Normal file
140
models/account_move.py
Normal file
@ -0,0 +1,140 @@
|
||||
from odoo import api, models, fields
|
||||
from odoo.tools.translate import _
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
@api.depends('amount_residual', 'move_type', 'state', 'company_id')
|
||||
def _compute_payment_state(self):
|
||||
super()._compute_payment_state()
|
||||
for invoice in self:
|
||||
if invoice.payment_state == 'paid' and invoice.is_invoice(include_receipts=True):
|
||||
term_lines = invoice.line_ids.filtered(lambda l: l.display_type == 'payment_term' and l.account_id.account_type == 'liability_current')
|
||||
if term_lines and invoice.currency_id.is_zero(invoice.amount_residual):
|
||||
payments = (term_lines.matched_debit_ids.debit_move_id.move_id.payment_id |
|
||||
term_lines.matched_credit_ids.credit_move_id.move_id.payment_id)
|
||||
if any(not p.is_matched for p in payments if p):
|
||||
invoice.payment_state = invoice._get_invoice_in_payment_state()
|
||||
|
||||
def _get_all_reconciled_invoice_partials(self):
|
||||
reconciled_partials = super()._get_all_reconciled_invoice_partials()
|
||||
if isinstance(reconciled_partials, dict):
|
||||
reconciled_partials = []
|
||||
|
||||
self.ensure_one()
|
||||
|
||||
# If the super() returns something, it might only be for payable/receivable.
|
||||
# We also need to fetch partials for liability_current
|
||||
reconciled_lines = self.line_ids.filtered(lambda line: line.account_id.account_type == 'liability_current')
|
||||
if not reconciled_lines:
|
||||
return reconciled_partials
|
||||
|
||||
self.env['account.partial.reconcile'].flush_model([
|
||||
'credit_amount_currency', 'credit_move_id', 'debit_amount_currency',
|
||||
'debit_move_id', 'exchange_move_id',
|
||||
])
|
||||
query = '''
|
||||
SELECT
|
||||
part.id,
|
||||
part.exchange_move_id,
|
||||
part.debit_amount_currency AS amount,
|
||||
part.credit_move_id AS aml_id,
|
||||
'debit' AS direction,
|
||||
part.create_date
|
||||
FROM account_partial_reconcile part
|
||||
WHERE part.debit_move_id IN %s
|
||||
UNION ALL
|
||||
SELECT
|
||||
part.id,
|
||||
part.exchange_move_id,
|
||||
part.credit_amount_currency AS amount,
|
||||
part.debit_move_id AS aml_id,
|
||||
'credit' AS direction,
|
||||
part.create_date
|
||||
FROM account_partial_reconcile part
|
||||
WHERE part.credit_move_id IN %s
|
||||
ORDER BY create_date DESC
|
||||
'''
|
||||
self.env.cr.execute(query, [tuple(reconciled_lines.ids), tuple(reconciled_lines.ids)])
|
||||
|
||||
aml_ids = set()
|
||||
exchange_move_ids = set()
|
||||
partials_data = []
|
||||
for row in self.env.cr.dictfetchall():
|
||||
aml_ids.add(row['aml_id'])
|
||||
if row['exchange_move_id']:
|
||||
exchange_move_ids.add(row['exchange_move_id'])
|
||||
partials_data.append(row)
|
||||
|
||||
if not partials_data:
|
||||
return reconciled_partials
|
||||
|
||||
amls = self.env['account.move.line'].browse(list(aml_ids)).filtered(lambda aml: aml.move_id.id not in exchange_move_ids)
|
||||
amls_dict = {aml.id: aml for aml in amls}
|
||||
|
||||
for partial in partials_data:
|
||||
if partial['aml_id'] not in amls_dict:
|
||||
continue
|
||||
counterpart_aml = amls_dict[partial['aml_id']]
|
||||
reconciled_partials.append({
|
||||
'aml': counterpart_aml,
|
||||
'amount': partial['amount'],
|
||||
'currency': self.currency_id,
|
||||
'is_exchange': bool(partial['exchange_move_id']),
|
||||
'partial_id': partial['id'],
|
||||
})
|
||||
|
||||
return reconciled_partials
|
||||
|
||||
def _compute_payments_widget_to_reconcile_info(self):
|
||||
super()._compute_payments_widget_to_reconcile_info()
|
||||
for move in self:
|
||||
if move.state != 'posted' or move.payment_state not in ('not_paid', 'partial') or not move.is_invoice(include_receipts=True):
|
||||
continue
|
||||
|
||||
pay_term_lines = move.line_ids.filtered(lambda line: line.account_id.account_type == 'liability_current')
|
||||
if not pay_term_lines:
|
||||
continue
|
||||
|
||||
domain = [
|
||||
('account_id', 'in', pay_term_lines.account_id.ids),
|
||||
('parent_state', '=', 'posted'),
|
||||
('partner_id', '=', move.commercial_partner_id.id),
|
||||
('reconciled', '=', False),
|
||||
'|', ('amount_residual', '!=', 0.0), ('amount_residual_currency', '!=', 0.0),
|
||||
]
|
||||
|
||||
payments_widget_vals = move.invoice_outstanding_credits_debits_widget or {'outstanding': True, 'content': [], 'move_id': move.id}
|
||||
|
||||
if move.is_inbound():
|
||||
domain.append(('balance', '<', 0.0))
|
||||
payments_widget_vals['title'] = _('Outstanding credits')
|
||||
else:
|
||||
domain.append(('balance', '>', 0.0))
|
||||
payments_widget_vals['title'] = _('Outstanding debits')
|
||||
|
||||
for line in self.env['account.move.line'].search(domain):
|
||||
if line.currency_id == move.currency_id:
|
||||
amount = abs(line.amount_residual_currency)
|
||||
else:
|
||||
amount = line.company_currency_id._convert(abs(line.amount_residual), move.currency_id, move.company_id, line.date)
|
||||
if move.currency_id.is_zero(amount):
|
||||
continue
|
||||
|
||||
# avoid duplicates
|
||||
if payments_widget_vals['content'] and any(c['id'] == line.id for c in payments_widget_vals['content']):
|
||||
continue
|
||||
|
||||
payments_widget_vals['content'].append({
|
||||
'journal_name': line.ref or line.move_id.name,
|
||||
'amount': amount,
|
||||
'currency_id': move.currency_id.id,
|
||||
'id': line.id,
|
||||
'move_id': line.move_id.id,
|
||||
'date': fields.Date.to_string(line.date),
|
||||
'account_payment_id': line.payment_id.id,
|
||||
})
|
||||
|
||||
if payments_widget_vals['content']:
|
||||
move.invoice_outstanding_credits_debits_widget = payments_widget_vals
|
||||
move.invoice_has_outstanding = True
|
||||
30
models/account_move_line.py
Normal file
30
models/account_move_line.py
Normal file
@ -0,0 +1,30 @@
|
||||
from odoo import api, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.translate import _
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = 'account.move.line'
|
||||
|
||||
@api.depends('move_id', 'account_id', 'tax_line_id')
|
||||
def _compute_display_type(self):
|
||||
super()._compute_display_type()
|
||||
for line in self:
|
||||
if line.display_type == 'product' and line.move_id.is_invoice():
|
||||
if line.account_id and line.account_id.account_type == 'liability_current':
|
||||
line.display_type = 'payment_term'
|
||||
|
||||
@api.constrains('account_id', 'display_type')
|
||||
def _check_payable_receivable(self):
|
||||
for line in self:
|
||||
account_type = line.account_id.account_type
|
||||
if line.move_id.is_sale_document(include_receipts=True):
|
||||
if account_type in ('liability_payable', 'liability_current'):
|
||||
raise UserError(_("Account %s is of payable type, but is used in a sale operation.", line.account_id.code))
|
||||
if (line.display_type == 'payment_term') ^ (account_type == 'asset_receivable'):
|
||||
raise UserError(_("Any journal item on a receivable account must have a due date and vice versa."))
|
||||
if line.move_id.is_purchase_document(include_receipts=True):
|
||||
if account_type == 'asset_receivable':
|
||||
raise UserError(_("Account %s is of receivable type, but is used in a purchase operation.", line.account_id.code))
|
||||
# Patch: Allow 'liability_current' to also act as a payment_term
|
||||
if (line.display_type == 'payment_term') ^ (account_type in ('liability_payable', 'liability_current')):
|
||||
raise UserError(_("Any journal item on a payable account must have a due date and vice versa."))
|
||||
20
models/account_payment.py
Normal file
20
models/account_payment.py
Normal file
@ -0,0 +1,20 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
class AccountPayment(models.Model):
|
||||
_inherit = 'account.payment'
|
||||
|
||||
destination_account_id = fields.Many2one(
|
||||
comodel_name='account.account',
|
||||
string='Destination Account',
|
||||
store=True, readonly=False,
|
||||
compute='_compute_destination_account_id',
|
||||
domain="[('account_type', 'in', ('asset_receivable', 'liability_payable', 'liability_current'))]",
|
||||
check_company=True
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _get_valid_payment_account_types(self):
|
||||
res = super()._get_valid_payment_account_types()
|
||||
if 'liability_current' not in res:
|
||||
res.append('liability_current')
|
||||
return res
|
||||
13
models/res_partner.py
Normal file
13
models/res_partner.py
Normal file
@ -0,0 +1,13 @@
|
||||
from odoo import fields, models
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
property_account_payable_id = fields.Many2one(
|
||||
'account.account',
|
||||
company_dependent=True,
|
||||
string="Account Payable",
|
||||
domain="[('account_type', 'in', ('liability_payable', 'liability_current')), ('deprecated', '=', False)]",
|
||||
help="This account will be used instead of the default one as the payable account for the current partner. Extended to allow Current Liabilities.",
|
||||
required=True
|
||||
)
|
||||
16
views/account_move_line_views.xml
Normal file
16
views/account_move_line_views.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_account_move_line_filter_liability" model="ir.ui.view">
|
||||
<field name="name">account.move.line.search.liability</field>
|
||||
<field name="model">account.move.line</field>
|
||||
<field name="inherit_id" ref="account.view_account_move_line_filter"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//filter[@name='trade_payable']" position="attributes">
|
||||
<attribute name="domain">['|', ('account_id.account_type', '=', 'liability_payable'), '&', ('account_id.account_type', '=', 'liability_current'), ('account_id.reconcile', '=', True)]</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//filter[@name='trade_receivable']" position="attributes">
|
||||
<attribute name="domain">['|', ('account_id.account_type', '=', 'asset_receivable'), '&', ('account_id.account_type', '=', 'asset_current'), ('account_id.reconcile', '=', True)]</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Loading…
Reference in New Issue
Block a user