[ADD] account_loan
This commit is contained in:
parent
0c3f2b4708
commit
99e55bd991
84
account_loan/README.rst
Normal file
84
account_loan/README.rst
Normal file
@ -0,0 +1,84 @@
|
||||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
|
||||
=======================
|
||||
Account Loan management
|
||||
=======================
|
||||
|
||||
This module extends the functionality of accounting to support loans.
|
||||
It will create automatically moves or invoices for loans.
|
||||
Moreover, you can check the pending amount to be paid and reduce the debt.
|
||||
|
||||
It currently supports two kinds of debts:
|
||||
|
||||
* Loans: a standard debt with banks, that only creates account moves
|
||||
* Leases: a debt with a bank where purchase invoices are necessary
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
To install this module, you need to:
|
||||
|
||||
#. Install numpy : ``pip install numpy``
|
||||
#. Follow the standard process
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
To use this module, you need to:
|
||||
|
||||
#. Go to `Invoicing / Accounting > Adviser > Loans`
|
||||
#. Configure a loan selecting the company, loan type, amount, rate and accounts
|
||||
#. Post the loan, it will automatically create an account move with the
|
||||
expected amounts
|
||||
#. Create automatically the account moves / invoices related to loans and
|
||||
leases before a selected date
|
||||
|
||||
On a posted loan you can:
|
||||
|
||||
* Create moves or invoices (according to the configuration)
|
||||
* Modify rates when needed (only unposted lines will be modified)
|
||||
* Reduce or cancel the debt of a loan / lease
|
||||
|
||||
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
||||
:alt: Try me on Runbot
|
||||
:target: https://runbot.odoo-community.org/runbot/92/11.0
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues
|
||||
<https://github.com/OCA/{project_repo}/issues>`_. In case of trouble, please
|
||||
check there if your issue has already been reported. If you spotted it first,
|
||||
help us smash it by providing detailed and welcomed feedback.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Images
|
||||
------
|
||||
|
||||
* Odoo Community Association: `Icon <https://odoo-community.org/logo.png>`_.
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* Enric Tobella <etobella@creublanca.es>
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
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.
|
||||
|
||||
To contribute to this module, please visit https://odoo-community.org.
|
4
account_loan/__init__.py
Normal file
4
account_loan/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from . import model
|
||||
from . import wizard
|
29
account_loan/__manifest__.py
Normal file
29
account_loan/__manifest__.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Copyright 2018 Creu Blanca
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
{
|
||||
"name": "Account Loan management",
|
||||
"version": "11.0.1.0.0",
|
||||
"author": "Creu Blanca,Odoo Community Association (OCA)",
|
||||
"website": "http://github.com/OCA/account-financial-tools",
|
||||
"license": "AGPL-3",
|
||||
"category": "Accounting",
|
||||
"depends": [
|
||||
"account"
|
||||
],
|
||||
"data": [
|
||||
'data/ir_sequence_data.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'security/account_loan_security.xml',
|
||||
'wizard/account_loan_generate_entries_view.xml',
|
||||
'wizard/account_loan_pay_amount_view.xml',
|
||||
'wizard/account_loan_post_view.xml',
|
||||
'views/account_loan_view.xml',
|
||||
'views/account_move_view.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'external_dependencies': {
|
||||
'python': [
|
||||
'numpy',
|
||||
],
|
||||
},
|
||||
}
|
18
account_loan/data/ir_sequence_data.xml
Normal file
18
account_loan/data/ir_sequence_data.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
||||
<!--
|
||||
Copyright 2017 Eficent Business and IT Consulting Services, S.L.
|
||||
Copyright 2017 Creu Blanca
|
||||
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
<record id="seq_account_loan" model="ir.sequence">
|
||||
<field name="name">Account loan sequence</field>
|
||||
<field name="code">account.loan</field>
|
||||
<field name="prefix">ACL</field>
|
||||
<field name="padding">6</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
7
account_loan/model/__init__.py
Normal file
7
account_loan/model/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
# Copyright 2018 Creu Blanca
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from . import account_invoice
|
||||
from . import account_loan
|
||||
from . import account_loan_line
|
||||
from . import account_move
|
50
account_loan/model/account_invoice.py
Normal file
50
account_loan/model/account_invoice.py
Normal file
@ -0,0 +1,50 @@
|
||||
# Copyright 2018 Creu Blanca
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class AccountInvoice(models.Model):
|
||||
_inherit = 'account.invoice'
|
||||
|
||||
loan_line_id = fields.Many2one(
|
||||
'account.loan.line',
|
||||
readonly=True,
|
||||
ondelete='restrict',
|
||||
)
|
||||
loan_id = fields.Many2one(
|
||||
'account.loan',
|
||||
readonly=True,
|
||||
store=True,
|
||||
ondelete='restrict',
|
||||
)
|
||||
|
||||
@api.multi
|
||||
def action_move_create(self):
|
||||
if self.loan_line_id:
|
||||
return super(AccountInvoice, self.with_context(
|
||||
default_loan_line_id=self.loan_line_id.id,
|
||||
default_loan_id=self.loan_id.id,
|
||||
)).action_move_create()
|
||||
return super().action_move_create()
|
||||
|
||||
@api.multi
|
||||
def finalize_invoice_move_lines(self, move_lines):
|
||||
vals = super().finalize_invoice_move_lines(move_lines)
|
||||
if self.loan_line_id:
|
||||
ll = self.loan_line_id
|
||||
if (
|
||||
ll.long_term_loan_account_id and
|
||||
ll.long_term_principal_amount != 0
|
||||
):
|
||||
vals.append((0, 0, {
|
||||
'account_id': ll.loan_id.short_term_loan_account_id.id,
|
||||
'credit': ll.long_term_principal_amount,
|
||||
'debit': 0,
|
||||
}))
|
||||
vals.append((0, 0, {
|
||||
'account_id': ll.long_term_loan_account_id.id,
|
||||
'credit': 0,
|
||||
'debit': ll.long_term_principal_amount,
|
||||
}))
|
||||
return vals
|
484
account_loan/model/account_loan.py
Normal file
484
account_loan/model/account_loan.py
Normal file
@ -0,0 +1,484 @@
|
||||
# Copyright 2018 Creu Blanca
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF
|
||||
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import logging
|
||||
import math
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
try:
|
||||
import numpy
|
||||
except (ImportError, IOError) as err:
|
||||
_logger.error(err)
|
||||
|
||||
|
||||
class AccountLoan(models.Model):
|
||||
_name = 'account.loan'
|
||||
_description = 'Loan'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
def _default_company(self):
|
||||
force_company = self._context.get('force_company')
|
||||
if not force_company:
|
||||
return self.env.user.company_id.id
|
||||
return force_company
|
||||
|
||||
name = fields.Char(
|
||||
copy=False,
|
||||
required=True,
|
||||
readonly=True,
|
||||
default='/',
|
||||
states={'draft': [('readonly', False)]},
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner',
|
||||
required=True,
|
||||
string='Lender',
|
||||
help='Company or individual that lends the money at an interest rate.',
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
required=True,
|
||||
default=_default_company,
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
)
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('posted', 'Posted'),
|
||||
('cancelled', 'Cancelled'),
|
||||
('closed', 'Closed'),
|
||||
], required=True, copy=False, default='draft')
|
||||
line_ids = fields.One2many(
|
||||
'account.loan.line',
|
||||
readonly=True,
|
||||
inverse_name='loan_id',
|
||||
copy=False,
|
||||
)
|
||||
periods = fields.Integer(
|
||||
required=True,
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
help='Number of periods that the loan will last',
|
||||
)
|
||||
method_period = fields.Integer(
|
||||
string='Period Length',
|
||||
default=1,
|
||||
help="State here the time between 2 depreciations, in months",
|
||||
required=True,
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
)
|
||||
start_date = fields.Date(
|
||||
help='Start of the moves',
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
copy=False,
|
||||
)
|
||||
rate = fields.Float(
|
||||
required=True,
|
||||
default=0.0,
|
||||
digits=(8, 6),
|
||||
help='Currently applied rate',
|
||||
track_visibility='always',
|
||||
)
|
||||
rate_period = fields.Float(
|
||||
compute='_compute_rate_period', digits=(8, 6),
|
||||
help='Real rate that will be applied on each period',
|
||||
)
|
||||
rate_type = fields.Selection(
|
||||
[
|
||||
('napr', 'Nominal APR'),
|
||||
('ear', 'EAR'),
|
||||
('real', 'Real rate'),
|
||||
],
|
||||
required=True,
|
||||
help='Method of computation of the applied rate',
|
||||
default='napr',
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
)
|
||||
loan_type = fields.Selection(
|
||||
[
|
||||
('fixed-annuity', 'Fixed Annuity'),
|
||||
('fixed-principal', 'Fixed Principal'),
|
||||
('interest', 'Only interest'),
|
||||
],
|
||||
required=True,
|
||||
help='Method of computation of the period annuity',
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
default='fixed-annuity'
|
||||
)
|
||||
fixed_amount = fields.Monetary(
|
||||
currency_field='currency_id',
|
||||
compute='_compute_fixed_amount',
|
||||
)
|
||||
fixed_loan_amount = fields.Monetary(
|
||||
currency_field='currency_id',
|
||||
readonly=True,
|
||||
copy=False,
|
||||
default=0,
|
||||
)
|
||||
fixed_periods = fields.Integer(
|
||||
readonly=True,
|
||||
copy=False,
|
||||
default=0,
|
||||
)
|
||||
loan_amount = fields.Monetary(
|
||||
currency_field='currency_id',
|
||||
required=True,
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
)
|
||||
residual_amount = fields.Monetary(
|
||||
currency_field='currency_id',
|
||||
default=0.,
|
||||
required=True,
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
help='Residual amount of the lease that must be payed on the end in '
|
||||
'order to acquire the asset',
|
||||
)
|
||||
round_on_end = fields.Boolean(
|
||||
default=False,
|
||||
help='When checked, the differences will be applied on the last period'
|
||||
', if it is unchecked, the annuity will be recalculated on each '
|
||||
'period.',
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
)
|
||||
payment_on_first_period = fields.Boolean(
|
||||
default=False,
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
help='When checked, the first payment will be on start date',
|
||||
)
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency',
|
||||
compute='_compute_currency',
|
||||
readonly=True,
|
||||
)
|
||||
journal_type = fields.Char(compute='_compute_journal_type')
|
||||
journal_id = fields.Many2one(
|
||||
'account.journal',
|
||||
domain="[('company_id', '=', company_id),('type', '=', journal_type)]",
|
||||
required=True,
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
)
|
||||
short_term_loan_account_id = fields.Many2one(
|
||||
'account.account',
|
||||
domain="[('company_id', '=', company_id)]",
|
||||
string='Short term account',
|
||||
help='Account that will contain the pending amount on short term',
|
||||
required=True,
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
)
|
||||
long_term_loan_account_id = fields.Many2one(
|
||||
'account.account',
|
||||
string='Long term account',
|
||||
help='Account that will contain the pending amount on Long term',
|
||||
domain="[('company_id', '=', company_id)]",
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
)
|
||||
interest_expenses_account_id = fields.Many2one(
|
||||
'account.account',
|
||||
domain="[('company_id', '=', company_id)]",
|
||||
string='Interests account',
|
||||
help='Account where the interests will be assigned to',
|
||||
required=True,
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
)
|
||||
is_leasing = fields.Boolean(
|
||||
default=False,
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
)
|
||||
leased_asset_account_id = fields.Many2one(
|
||||
'account.account',
|
||||
domain="[('company_id', '=', company_id)]",
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
)
|
||||
product_id = fields.Many2one(
|
||||
'product.product',
|
||||
string='Loan product',
|
||||
help='Product where the amount of the loan will be assigned when the '
|
||||
'invoice is created',
|
||||
)
|
||||
interests_product_id = fields.Many2one(
|
||||
'product.product',
|
||||
string='Interest product',
|
||||
help='Product where the amount of interests will be assigned when the '
|
||||
'invoice is created',
|
||||
)
|
||||
move_ids = fields.One2many(
|
||||
'account.move',
|
||||
copy=False,
|
||||
inverse_name='loan_id'
|
||||
)
|
||||
pending_principal_amount = fields.Monetary(
|
||||
currency_field='currency_id',
|
||||
compute='_compute_total_amounts',
|
||||
)
|
||||
payment_amount = fields.Monetary(
|
||||
currency_field='currency_id',
|
||||
string='Total payed amount',
|
||||
compute='_compute_total_amounts',
|
||||
)
|
||||
interests_amount = fields.Monetary(
|
||||
currency_field='currency_id',
|
||||
string='Total interests payed',
|
||||
compute='_compute_total_amounts',
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique(name, company_id)',
|
||||
'Loan name must be unique'),
|
||||
]
|
||||
|
||||
@api.depends('line_ids', 'currency_id', 'loan_amount')
|
||||
def _compute_total_amounts(self):
|
||||
for record in self:
|
||||
lines = record.line_ids.filtered(lambda r: r.move_ids)
|
||||
record.payment_amount = sum(
|
||||
lines.mapped('payment_amount')) or 0.
|
||||
record.interests_amount = sum(
|
||||
lines.mapped('interests_amount')) or 0.
|
||||
record.pending_principal_amount = (
|
||||
record.loan_amount -
|
||||
record.payment_amount +
|
||||
record.interests_amount
|
||||
)
|
||||
|
||||
@api.depends('rate_period', 'fixed_loan_amount', 'fixed_periods',
|
||||
'currency_id')
|
||||
def _compute_fixed_amount(self):
|
||||
"""
|
||||
Computes the fixed amount in order to be used if round_on_end is
|
||||
checked. On fix-annuity interests are included and on fixed-principal
|
||||
and interests it isn't.
|
||||
:return:
|
||||
"""
|
||||
for record in self:
|
||||
if record.loan_type == 'fixed-annuity':
|
||||
record.fixed_amount = - record.currency_id.round(numpy.pmt(
|
||||
record.rate_period / 100,
|
||||
record.fixed_periods,
|
||||
record.fixed_loan_amount,
|
||||
-record.residual_amount
|
||||
))
|
||||
elif record.loan_type == 'fixed-principal':
|
||||
record.fixed_amount = record.currency_id.round(
|
||||
(record.fixed_loan_amount - record.residual_amount) /
|
||||
record.fixed_periods
|
||||
)
|
||||
else:
|
||||
record.fixed_amount = 0.0
|
||||
|
||||
@api.model
|
||||
def compute_rate(self, rate, rate_type, method_period):
|
||||
"""
|
||||
Returns the real rate
|
||||
:param rate: Rate
|
||||
:param rate_type: Computation rate
|
||||
:param method_period: Number of months between payments
|
||||
:return:
|
||||
"""
|
||||
if rate_type == 'napr':
|
||||
return rate / 12 * method_period
|
||||
if rate_type == 'ear':
|
||||
return math.pow(1 + rate, method_period / 12) - 1
|
||||
return rate
|
||||
|
||||
@api.depends('rate', 'method_period', 'rate_type')
|
||||
def _compute_rate_period(self):
|
||||
for record in self:
|
||||
record.rate_period = record.compute_rate(
|
||||
record.rate, record.rate_type, record.method_period)
|
||||
|
||||
@api.depends('journal_id', 'company_id')
|
||||
def _compute_currency(self):
|
||||
for rec in self:
|
||||
rec.currency_id = (
|
||||
rec.journal_id.currency_id or rec.company_id.currency_id)
|
||||
|
||||
@api.depends('is_leasing')
|
||||
def _compute_journal_type(self):
|
||||
for record in self:
|
||||
if record.is_leasing:
|
||||
record.journal_type = 'purchase'
|
||||
else:
|
||||
record.journal_type = 'general'
|
||||
|
||||
@api.onchange('is_leasing')
|
||||
def _onchange_is_leasing(self):
|
||||
self.journal_id = self.env['account.journal'].search([
|
||||
('company_id', '=', self.company_id.id),
|
||||
('type', '=', 'purchase' if self.is_leasing else 'general')
|
||||
], limit=1)
|
||||
self.residual_amount = 0.0
|
||||
|
||||
@api.onchange('company_id')
|
||||
def _onchange_company(self):
|
||||
self._onchange_is_leasing()
|
||||
self.interest_expenses_account_id = False
|
||||
self.short_term_loan_account_id = False
|
||||
self.long_term_loan_account_id = False
|
||||
|
||||
def get_default_name(self, vals):
|
||||
return self.env['ir.sequence'].next_by_code('account.loan') or '/'
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
if vals.get('name', '/') == '/':
|
||||
vals['name'] = self.get_default_name(vals)
|
||||
return super().create(vals)
|
||||
|
||||
@api.multi
|
||||
def post(self):
|
||||
self.ensure_one()
|
||||
if not self.start_date:
|
||||
self.start_date = fields.Datetime.now()
|
||||
self.compute_draft_lines()
|
||||
self.write({'state': 'posted'})
|
||||
|
||||
@api.multi
|
||||
def close(self):
|
||||
self.write({'state': 'closed'})
|
||||
|
||||
@api.multi
|
||||
def compute_lines(self):
|
||||
self.ensure_one()
|
||||
if self.state == 'draft':
|
||||
return self.compute_draft_lines()
|
||||
return self.compute_posted_lines()
|
||||
|
||||
def compute_posted_lines(self):
|
||||
"""
|
||||
Recompute the amounts of not finished lines. Useful if rate is changed
|
||||
"""
|
||||
amount = self.loan_amount
|
||||
for line in self.line_ids.sorted('sequence'):
|
||||
if line.move_ids:
|
||||
amount = line.final_pending_principal_amount
|
||||
else:
|
||||
line.rate = self.rate_period
|
||||
line.pending_principal_amount = amount
|
||||
line.check_amount()
|
||||
amount -= line.payment_amount - line.interests_amount
|
||||
if self.long_term_loan_account_id:
|
||||
self.check_long_term_principal_amount()
|
||||
|
||||
def check_long_term_principal_amount(self):
|
||||
"""
|
||||
Recomputes the long term pending principal of unfinished lines.
|
||||
"""
|
||||
lines = self.line_ids.filtered(lambda r: not r.move_ids)
|
||||
amount = 0
|
||||
final_sequence = min(lines.mapped('sequence'))
|
||||
for line in lines.sorted('sequence', reverse=True):
|
||||
date = datetime.strptime(
|
||||
line.date, DF).date() + relativedelta(months=12)
|
||||
if self.state == 'draft' or line.sequence != final_sequence:
|
||||
line.long_term_pending_principal_amount = sum(
|
||||
self.line_ids.filtered(
|
||||
lambda r: datetime.strptime(r.date, DF).date() >= date
|
||||
).mapped('principal_amount'))
|
||||
line.long_term_principal_amount = (
|
||||
line.long_term_pending_principal_amount - amount)
|
||||
amount = line.long_term_pending_principal_amount
|
||||
|
||||
def new_line_vals(self, sequence, date, amount):
|
||||
return {
|
||||
'loan_id': self.id,
|
||||
'sequence': sequence,
|
||||
'date': date,
|
||||
'pending_principal_amount': amount,
|
||||
'rate': self.rate_period,
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def compute_draft_lines(self):
|
||||
self.ensure_one()
|
||||
self.fixed_periods = self.periods
|
||||
self.fixed_loan_amount = self.loan_amount
|
||||
self.line_ids.unlink()
|
||||
amount = self.loan_amount
|
||||
if self.start_date:
|
||||
date = datetime.strptime(self.start_date, DF).date()
|
||||
else:
|
||||
date = datetime.today().date()
|
||||
delta = relativedelta(months=self.method_period)
|
||||
if not self.payment_on_first_period:
|
||||
date += delta
|
||||
for i in range(1, self.periods + 1):
|
||||
line = self.env['account.loan.line'].create(
|
||||
self.new_line_vals(i, date, amount)
|
||||
)
|
||||
line.check_amount()
|
||||
date += delta
|
||||
amount -= line.payment_amount - line.interests_amount
|
||||
if self.long_term_loan_account_id:
|
||||
self.check_long_term_principal_amount()
|
||||
|
||||
@api.multi
|
||||
def view_account_moves(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref('account.action_move_line_form')
|
||||
result = action.read()[0]
|
||||
result['domain'] = [('loan_id', '=', self.id)]
|
||||
return result
|
||||
|
||||
@api.multi
|
||||
def view_account_invoices(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref('account.action_invoice_tree2')
|
||||
result = action.read()[0]
|
||||
result['domain'] = [
|
||||
('loan_id', '=', self.id),
|
||||
('type', '=', 'in_invoice')
|
||||
]
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def generate_loan_entries(self, date):
|
||||
"""
|
||||
Generate the moves of unfinished loans before date
|
||||
:param date:
|
||||
:return:
|
||||
"""
|
||||
res = []
|
||||
for record in self.search([
|
||||
('state', '=', 'posted'),
|
||||
('is_leasing', '=', False)
|
||||
]):
|
||||
lines = record.line_ids.filtered(
|
||||
lambda r: datetime.strptime(
|
||||
r.date, DF).date() <= date and not r.move_ids
|
||||
)
|
||||
res += lines.generate_move()
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def generate_leasing_entries(self, date):
|
||||
res = []
|
||||
for record in self.search([
|
||||
('state', '=', 'posted'),
|
||||
('is_leasing', '=', True)
|
||||
]):
|
||||
res += record.line_ids.filtered(
|
||||
lambda r: datetime.strptime(
|
||||
r.date, DF).date() <= date and not r.invoice_ids
|
||||
).generate_invoice()
|
||||
return res
|
384
account_loan/model/account_loan_line.py
Normal file
384
account_loan/model/account_loan_line.py
Normal file
@ -0,0 +1,384 @@
|
||||
# Copyright 2018 Creu Blanca
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
try:
|
||||
import numpy
|
||||
except (ImportError, IOError) as err:
|
||||
_logger.error(err)
|
||||
|
||||
|
||||
class AccountLoanLine(models.Model):
|
||||
_name = 'account.loan.line'
|
||||
_description = 'Annuity'
|
||||
_order = 'sequence asc'
|
||||
|
||||
name = fields.Char(compute='_compute_name')
|
||||
loan_id = fields.Many2one(
|
||||
'account.loan',
|
||||
required=True,
|
||||
readonly=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
is_leasing = fields.Boolean(related='loan_id.is_leasing', readonly=True, )
|
||||
loan_type = fields.Selection([
|
||||
('fixed-annuity', 'Fixed Annuity'),
|
||||
('fixed-principal', 'Fixed Principal'),
|
||||
('interest', 'Only interest'),
|
||||
], related='loan_id.loan_type', readonly=True,
|
||||
)
|
||||
loan_state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('posted', 'Posted'),
|
||||
('cancelled', 'Cancelled'),
|
||||
('closed', 'Closed'),
|
||||
], related='loan_id.state', readonly=True, store=True)
|
||||
sequence = fields.Integer(required=True, readonly=True)
|
||||
date = fields.Date(
|
||||
required=True,
|
||||
readonly=True,
|
||||
help='Date when the payment will be accounted',
|
||||
)
|
||||
long_term_loan_account_id = fields.Many2one(
|
||||
'account.account',
|
||||
readony=True,
|
||||
related='loan_id.long_term_loan_account_id',
|
||||
)
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency',
|
||||
related='loan_id.currency_id',
|
||||
)
|
||||
rate = fields.Float(
|
||||
required=True,
|
||||
readonly=True,
|
||||
digits=(8, 6),
|
||||
)
|
||||
pending_principal_amount = fields.Monetary(
|
||||
currency_field='currency_id',
|
||||
readonly=True,
|
||||
help='Pending amount of the loan before the payment',
|
||||
)
|
||||
long_term_pending_principal_amount = fields.Monetary(
|
||||
currency_field='currency_id',
|
||||
readonly=True,
|
||||
help='Pending amount of the loan before the payment that will not be '
|
||||
'payed in, at least, 12 months',
|
||||
)
|
||||
payment_amount = fields.Monetary(
|
||||
currency_field='currency_id',
|
||||
readonly=True,
|
||||
help='Total amount that will be payed (Annuity)',
|
||||
)
|
||||
interests_amount = fields.Monetary(
|
||||
currency_field='currency_id',
|
||||
readonly=True,
|
||||
help='Amount of the payment that will be assigned to interests',
|
||||
)
|
||||
principal_amount = fields.Monetary(
|
||||
currency_field='currency_id',
|
||||
compute='_compute_amounts',
|
||||
help='Amount of the payment that will reduce the pending loan amount',
|
||||
)
|
||||
long_term_principal_amount = fields.Monetary(
|
||||
currency_field='currency_id',
|
||||
readonly=True,
|
||||
help='Amount that will reduce the pending loan amount on long term',
|
||||
)
|
||||
final_pending_principal_amount = fields.Monetary(
|
||||
currency_field='currency_id',
|
||||
compute='_compute_amounts',
|
||||
help='Pending amount of the loan after the payment',
|
||||
)
|
||||
move_ids = fields.One2many(
|
||||
'account.move',
|
||||
inverse_name='loan_line_id',
|
||||
)
|
||||
has_moves = fields.Boolean(
|
||||
compute='_compute_has_moves'
|
||||
)
|
||||
invoice_ids = fields.One2many(
|
||||
'account.invoice',
|
||||
inverse_name='loan_line_id',
|
||||
)
|
||||
has_invoices = fields.Boolean(
|
||||
compute='_compute_has_invoices'
|
||||
)
|
||||
_sql_constraints = [
|
||||
('sequence_loan',
|
||||
'unique(loan_id, sequence)',
|
||||
'Sequence must be unique in a loan')
|
||||
]
|
||||
|
||||
@api.depends('move_ids')
|
||||
def _compute_has_moves(self):
|
||||
for record in self:
|
||||
record.has_moves = bool(record.move_ids)
|
||||
|
||||
@api.depends('invoice_ids')
|
||||
def _compute_has_invoices(self):
|
||||
for record in self:
|
||||
record.has_invoices = bool(record.invoice_ids)
|
||||
|
||||
@api.depends('loan_id.name', 'sequence')
|
||||
def _compute_name(self):
|
||||
for record in self:
|
||||
record.name = '%s-%d' % (record.loan_id.name, record.sequence)
|
||||
|
||||
@api.depends('payment_amount', 'interests_amount',
|
||||
'pending_principal_amount')
|
||||
def _compute_amounts(self):
|
||||
for rec in self:
|
||||
rec.final_pending_principal_amount = (
|
||||
rec.pending_principal_amount - rec.payment_amount +
|
||||
rec.interests_amount
|
||||
)
|
||||
rec.principal_amount = rec.payment_amount - rec.interests_amount
|
||||
|
||||
def compute_amount(self):
|
||||
"""
|
||||
Computes the payment amount
|
||||
:return: Amount to be payed on the annuity
|
||||
"""
|
||||
if self.sequence == self.loan_id.periods:
|
||||
return (self.pending_principal_amount + self.interests_amount -
|
||||
self.loan_id.residual_amount)
|
||||
if self.loan_type == 'fixed-principal' and self.loan_id.round_on_end:
|
||||
return self.loan_id.fixed_amount + self.interests_amount
|
||||
if self.loan_type == 'fixed-principal':
|
||||
return (
|
||||
self.pending_principal_amount -
|
||||
self.loan_id.residual_amount
|
||||
) / (
|
||||
self.loan_id.periods - self.sequence + 1
|
||||
) + self.interests_amount
|
||||
if self.loan_type == 'interest':
|
||||
return self.interests_amount
|
||||
if self.loan_type == 'fixed-annuity' and self.loan_id.round_on_end:
|
||||
return self.loan_id.fixed_amount
|
||||
if self.loan_type == 'fixed-annuity':
|
||||
return self.currency_id.round(- numpy.pmt(
|
||||
self.rate / 100,
|
||||
self.loan_id.periods - self.sequence + 1,
|
||||
self.pending_principal_amount,
|
||||
-self.loan_id.residual_amount
|
||||
))
|
||||
|
||||
def check_amount(self):
|
||||
"""Recompute amounts if the annuity has not been processed"""
|
||||
if self.move_ids or self.invoice_ids:
|
||||
raise UserError(_(
|
||||
'Amount cannot be recomputed if moves or invoices exists '
|
||||
'already'
|
||||
))
|
||||
if not self.loan_id.round_on_end:
|
||||
self.interests_amount = self.currency_id.round(
|
||||
self.pending_principal_amount * self.rate / 100)
|
||||
self.payment_amount = self.currency_id.round(self.compute_amount())
|
||||
else:
|
||||
self.interests_amount = (
|
||||
self.pending_principal_amount * self.rate / 100)
|
||||
self.payment_amount = self.compute_amount()
|
||||
|
||||
@api.multi
|
||||
def check_move_amount(self):
|
||||
"""
|
||||
Changes the amounts of the annuity once the move is posted
|
||||
:return:
|
||||
"""
|
||||
self.ensure_one()
|
||||
interests_moves = self.move_ids.mapped('line_ids').filtered(
|
||||
lambda r: r.account_id == self.loan_id.interest_expenses_account_id
|
||||
)
|
||||
short_term_moves = self.move_ids.mapped('line_ids').filtered(
|
||||
lambda r: r.account_id == self.loan_id.short_term_loan_account_id
|
||||
)
|
||||
long_term_moves = self.move_ids.mapped('line_ids').filtered(
|
||||
lambda r: r.account_id == self.loan_id.long_term_loan_account_id
|
||||
)
|
||||
self.interests_amount = (
|
||||
sum(interests_moves.mapped('debit')) -
|
||||
sum(interests_moves.mapped('credit'))
|
||||
)
|
||||
self.long_term_principal_amount = (
|
||||
sum(long_term_moves.mapped('debit')) -
|
||||
sum(long_term_moves.mapped('credit'))
|
||||
)
|
||||
self.payment_amount = (
|
||||
sum(short_term_moves.mapped('debit')) -
|
||||
sum(short_term_moves.mapped('credit')) +
|
||||
self.long_term_principal_amount +
|
||||
self.interests_amount
|
||||
)
|
||||
|
||||
def move_vals(self):
|
||||
return {
|
||||
'loan_line_id': self.id,
|
||||
'loan_id': self.loan_id.id,
|
||||
'date': self.date,
|
||||
'ref': self.name,
|
||||
'journal_id': self.loan_id.journal_id.id,
|
||||
'line_ids': [(0, 0, vals) for vals in self.move_line_vals()]
|
||||
}
|
||||
|
||||
def move_line_vals(self):
|
||||
vals = []
|
||||
partner = self.loan_id.partner_id.with_context(
|
||||
force_company=self.loan_id.company_id.id)
|
||||
vals.append({
|
||||
'account_id': partner.property_account_payable_id.id,
|
||||
'partner_id': partner.id,
|
||||
'credit': self.payment_amount,
|
||||
'debit': 0,
|
||||
})
|
||||
vals.append({
|
||||
'account_id': self.loan_id.interest_expenses_account_id.id,
|
||||
'credit': 0,
|
||||
'debit': self.interests_amount,
|
||||
})
|
||||
vals.append({
|
||||
'account_id': self.loan_id.short_term_loan_account_id.id,
|
||||
'credit': 0,
|
||||
'debit': self.payment_amount - self.interests_amount,
|
||||
})
|
||||
if self.long_term_loan_account_id and self.long_term_principal_amount:
|
||||
vals.append({
|
||||
'account_id': self.loan_id.short_term_loan_account_id.id,
|
||||
'credit': self.long_term_principal_amount,
|
||||
'debit': 0,
|
||||
})
|
||||
vals.append({
|
||||
'account_id': self.long_term_loan_account_id.id,
|
||||
'credit': 0,
|
||||
'debit': self.long_term_principal_amount,
|
||||
})
|
||||
return vals
|
||||
|
||||
def invoice_vals(self):
|
||||
partner = self.loan_id.partner_id.with_context(
|
||||
force_company=self.loan_id.company_id.id)
|
||||
return {
|
||||
'loan_line_id': self.id,
|
||||
'loan_id': self.loan_id.id,
|
||||
'type': 'in_invoice',
|
||||
'partner_id': self.loan_id.partner_id.id,
|
||||
'date_invoice': self.date,
|
||||
'account_id': partner.property_account_payable_id.id,
|
||||
'journal_id': self.loan_id.journal_id.id,
|
||||
'company_id': self.loan_id.company_id.id,
|
||||
'invoice_line_ids': [(0, 0, vals) for vals in
|
||||
self.invoice_line_vals()]
|
||||
}
|
||||
|
||||
def invoice_line_vals(self):
|
||||
vals = list()
|
||||
vals.append({
|
||||
'product_id': self.loan_id.product_id.id,
|
||||
'name': self.loan_id.product_id.name,
|
||||
'quantity': 1,
|
||||
'price_unit': self.principal_amount,
|
||||
'account_id': self.loan_id.short_term_loan_account_id.id,
|
||||
})
|
||||
vals.append({
|
||||
'product_id': self.loan_id.interests_product_id.id,
|
||||
'name': self.loan_id.interests_product_id.name,
|
||||
'quantity': 1,
|
||||
'price_unit': self.interests_amount,
|
||||
'account_id': self.loan_id.interest_expenses_account_id.id,
|
||||
})
|
||||
return vals
|
||||
|
||||
@api.multi
|
||||
def generate_move(self):
|
||||
"""
|
||||
Computes and post the moves of loans
|
||||
:return: list of account.move generated
|
||||
"""
|
||||
res = []
|
||||
for record in self:
|
||||
if not record.move_ids:
|
||||
if record.loan_id.line_ids.filtered(
|
||||
lambda r: r.date < record.date and not r.move_ids
|
||||
):
|
||||
raise UserError(_('Some moves must be created first'))
|
||||
move = self.env['account.move'].create(record.move_vals())
|
||||
move.post()
|
||||
res.append(move.id)
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def generate_invoice(self):
|
||||
"""
|
||||
Computes invoices of leases
|
||||
:return: list of account.invoice generated
|
||||
"""
|
||||
res = []
|
||||
for record in self:
|
||||
if not record.invoice_ids:
|
||||
if record.loan_id.line_ids.filtered(
|
||||
lambda r: r.date < record.date and not r.invoice_ids
|
||||
):
|
||||
raise UserError(_('Some invoices must be created first'))
|
||||
invoice = self.env['account.invoice'].create(
|
||||
record.invoice_vals())
|
||||
res.append(invoice.id)
|
||||
for line in invoice.invoice_line_ids:
|
||||
line._set_taxes()
|
||||
invoice.compute_taxes()
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def view_account_values(self):
|
||||
"""Shows the invoice if it is a leasing or the move if it is a loan"""
|
||||
self.ensure_one()
|
||||
if self.is_leasing:
|
||||
return self.view_account_invoices()
|
||||
return self.view_account_moves()
|
||||
|
||||
@api.multi
|
||||
def view_process_values(self):
|
||||
"""Computes the annuity and returns the result"""
|
||||
self.ensure_one()
|
||||
if self.is_leasing:
|
||||
self.generate_invoice()
|
||||
else:
|
||||
self.generate_move()
|
||||
return self.view_account_values()
|
||||
|
||||
@api.multi
|
||||
def view_account_moves(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref('account.action_move_line_form')
|
||||
result = action.read()[0]
|
||||
result['context'] = {
|
||||
'default_loan_line_id': self.id,
|
||||
'default_loan_id': self.loan_id.id
|
||||
}
|
||||
result['domain'] = [('loan_line_id', '=', self.id)]
|
||||
if len(self.move_ids) == 1:
|
||||
res = self.env.ref('account.move.form', False)
|
||||
result['views'] = [(res and res.id or False, 'form')]
|
||||
result['res_id'] = self.move_ids.id
|
||||
return result
|
||||
|
||||
@api.multi
|
||||
def view_account_invoices(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref('account.action_invoice_tree2')
|
||||
result = action.read()[0]
|
||||
result['context'] = {
|
||||
'default_loan_line_id': self.id,
|
||||
'default_loan_id': self.loan_id.id
|
||||
}
|
||||
result['domain'] = [
|
||||
('loan_line_id', '=', self.id),
|
||||
('type', '=', 'in_invoice')
|
||||
]
|
||||
if len(self.invoice_ids) == 1:
|
||||
res = self.env.ref('account.invoice.supplier.form', False)
|
||||
result['views'] = [(res and res.id or False, 'form')]
|
||||
result['res_id'] = self.invoice_ids.id
|
||||
return result
|
32
account_loan/model/account_move.py
Normal file
32
account_loan/model/account_move.py
Normal file
@ -0,0 +1,32 @@
|
||||
# Copyright 2018 Creu Blanca
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
loan_line_id = fields.Many2one(
|
||||
'account.loan.line',
|
||||
readonly=True,
|
||||
ondelete='restrict',
|
||||
)
|
||||
loan_id = fields.Many2one(
|
||||
'account.loan',
|
||||
readonly=True,
|
||||
store=True,
|
||||
ondelete='restrict',
|
||||
)
|
||||
|
||||
@api.multi
|
||||
def post(self):
|
||||
res = super().post()
|
||||
for record in self:
|
||||
if record.loan_line_id:
|
||||
record.loan_id = record.loan_line_id.loan_id
|
||||
record.loan_line_id.check_move_amount()
|
||||
record.loan_line_id.loan_id.compute_posted_lines()
|
||||
if record.loan_line_id.sequence == record.loan_id.periods:
|
||||
record.loan_id.close()
|
||||
return res
|
12
account_loan/security/account_loan_security.xml
Normal file
12
account_loan/security/account_loan_security.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="account_loan_multi_company_rule" model="ir.rule">
|
||||
<field name="name">Account loan multi-company</field>
|
||||
<field ref="model_account_loan" name="model_id"/>
|
||||
<field eval="True" name="global"/>
|
||||
<field name="domain_force">
|
||||
['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
5
account_loan/security/ir.model.access.csv
Normal file
5
account_loan/security/ir.model.access.csv
Normal file
@ -0,0 +1,5 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_account_loan,account.loan,model_account_loan,account.group_account_user,1,0,0,0
|
||||
access_account_loan_manager,account.loan,model_account_loan,account.group_account_manager,1,1,1,1
|
||||
access_account_loan_line,account.loan.line,model_account_loan_line,account.group_account_user,1,0,0,0
|
||||
access_account_loan_line_manager,account.loan.line,model_account_loan_line,account.group_account_manager,1,1,1,1
|
|
BIN
account_loan/static/description/icon.png
Normal file
BIN
account_loan/static/description/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
3
account_loan/tests/__init__.py
Normal file
3
account_loan/tests/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from . import test_loan
|
389
account_loan/tests/test_loan.py
Normal file
389
account_loan/tests/test_loan.py
Normal file
@ -0,0 +1,389 @@
|
||||
# Copyright 2018 Creu Blanca
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import fields
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import TransactionCase
|
||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF
|
||||
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
try:
|
||||
import numpy
|
||||
except (ImportError, IOError) as err:
|
||||
_logger.error(err)
|
||||
|
||||
|
||||
class TestLoan(TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.company = self.browse_ref('base.main_company')
|
||||
self.company_02 = self.env['res.company'].create({
|
||||
'name': 'Auxiliar company'
|
||||
})
|
||||
self.journal = self.env['account.journal'].create({
|
||||
'company_id': self.company.id,
|
||||
'type': 'purchase',
|
||||
'name': 'Debts',
|
||||
'code': 'DBT',
|
||||
})
|
||||
self.loan_account = self.create_account(
|
||||
'DEP',
|
||||
'depreciation',
|
||||
self.browse_ref('account.data_account_type_current_liabilities').id
|
||||
)
|
||||
self.payable_account = self.create_account(
|
||||
'PAY',
|
||||
'payable',
|
||||
self.browse_ref('account.data_account_type_payable').id
|
||||
)
|
||||
self.asset_account = self.create_account(
|
||||
'ASSET',
|
||||
'asset',
|
||||
self.browse_ref('account.data_account_type_payable').id
|
||||
)
|
||||
self.interests_account = self.create_account(
|
||||
'FEE',
|
||||
'Fees',
|
||||
self.browse_ref('account.data_account_type_expenses').id)
|
||||
self.lt_loan_account = self.create_account(
|
||||
'LTD',
|
||||
'Long term depreciation',
|
||||
self.browse_ref(
|
||||
'account.data_account_type_non_current_liabilities').id)
|
||||
self.partner = self.env['res.partner'].create({
|
||||
'name': 'Bank'
|
||||
})
|
||||
self.product = self.env['product.product'].create({
|
||||
'name': 'Payment',
|
||||
'type': 'service'
|
||||
})
|
||||
self.interests_product = self.env['product.product'].create({
|
||||
'name': 'Bank fee',
|
||||
'type': 'service'
|
||||
})
|
||||
|
||||
def test_onchange(self):
|
||||
loan = self.env['account.loan'].new({
|
||||
'name': 'LOAN',
|
||||
'company_id': self.company.id,
|
||||
'journal_id': self.journal.id,
|
||||
'loan_type': 'fixed-annuity',
|
||||
'loan_amount': 100,
|
||||
'rate': 1,
|
||||
'periods': 2,
|
||||
'short_term_loan_account_id': self.loan_account.id,
|
||||
'interest_expenses_account_id': self.interests_account.id,
|
||||
'product_id': self.product.id,
|
||||
'interests_product_id': self.interests_product.id,
|
||||
'partner_id': self.partner.id,
|
||||
})
|
||||
journal = loan.journal_id.id
|
||||
loan.is_leasing = True
|
||||
loan._onchange_is_leasing()
|
||||
self.assertNotEqual(journal, loan.journal_id.id)
|
||||
loan.company_id = self.company_02
|
||||
loan._onchange_company()
|
||||
self.assertFalse(loan.interest_expenses_account_id)
|
||||
|
||||
def test_round_on_end(self):
|
||||
loan = self.create_loan('fixed-annuity', 500000, 1, 60)
|
||||
loan.round_on_end = True
|
||||
loan.compute_lines()
|
||||
line_1 = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
line_end = loan.line_ids.filtered(lambda r: r.sequence == 60)
|
||||
self.assertNotAlmostEqual(
|
||||
line_1.payment_amount, line_end.payment_amount, 2)
|
||||
loan.loan_type = 'fixed-principal'
|
||||
loan.compute_lines()
|
||||
line_1 = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
line_end = loan.line_ids.filtered(lambda r: r.sequence == 60)
|
||||
self.assertNotAlmostEqual(
|
||||
line_1.payment_amount, line_end.payment_amount, 2)
|
||||
loan.loan_type = 'interest'
|
||||
loan.compute_lines()
|
||||
line_1 = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
line_end = loan.line_ids.filtered(lambda r: r.sequence == 60)
|
||||
self.assertEqual(line_1.principal_amount, 0)
|
||||
self.assertEqual(line_end.principal_amount, 500000)
|
||||
|
||||
def test_pay_amount_validation(self):
|
||||
amount = 10000
|
||||
periods = 24
|
||||
loan = self.create_loan('fixed-annuity', amount, 1, periods)
|
||||
self.assertTrue(loan.line_ids)
|
||||
self.assertEqual(len(loan.line_ids), periods)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
self.assertAlmostEqual(
|
||||
- numpy.pmt(1 / 100 / 12, 24, 10000), line.payment_amount, 2)
|
||||
self.assertEqual(line.long_term_principal_amount, 0)
|
||||
loan.long_term_loan_account_id = self.lt_loan_account
|
||||
loan.compute_lines()
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
self.assertGreater(line.long_term_principal_amount, 0)
|
||||
self.post(loan)
|
||||
self.assertTrue(loan.start_date)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
self.assertTrue(line)
|
||||
self.assertFalse(line.move_ids)
|
||||
self.assertFalse(line.invoice_ids)
|
||||
wzd = self.env['account.loan.generate.wizard'].create({})
|
||||
action = wzd.run()
|
||||
self.assertTrue(action)
|
||||
self.assertFalse(wzd.run())
|
||||
self.assertTrue(line.move_ids)
|
||||
self.assertIn(line.move_ids.id, action['domain'][0][2])
|
||||
line.move_ids.post()
|
||||
with self.assertRaises(UserError):
|
||||
self.env['account.loan.pay.amount'].create({
|
||||
'loan_id': loan.id,
|
||||
'amount': (amount - amount / periods) / 2,
|
||||
'fees': 100,
|
||||
'date': datetime.strptime(
|
||||
line.date, DF
|
||||
).date() + relativedelta(months=-1)
|
||||
}).run()
|
||||
with self.assertRaises(UserError):
|
||||
self.env['account.loan.pay.amount'].create({
|
||||
'loan_id': loan.id,
|
||||
'amount': amount,
|
||||
'fees': 100,
|
||||
'date': datetime.strptime(
|
||||
line.date, DF
|
||||
).date()
|
||||
}).run()
|
||||
with self.assertRaises(UserError):
|
||||
self.env['account.loan.pay.amount'].create({
|
||||
'loan_id': loan.id,
|
||||
'amount': 0,
|
||||
'fees': 100,
|
||||
'date': datetime.strptime(
|
||||
line.date, DF
|
||||
).date()
|
||||
}).run()
|
||||
with self.assertRaises(UserError):
|
||||
self.env['account.loan.pay.amount'].create({
|
||||
'loan_id': loan.id,
|
||||
'amount': -100,
|
||||
'fees': 100,
|
||||
'date': datetime.strptime(
|
||||
line.date, DF
|
||||
).date()
|
||||
}).run()
|
||||
|
||||
def test_fixed_annuity_loan(self):
|
||||
amount = 10000
|
||||
periods = 24
|
||||
loan = self.create_loan('fixed-annuity', amount, 1, periods)
|
||||
self.assertTrue(loan.line_ids)
|
||||
self.assertEqual(len(loan.line_ids), periods)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
self.assertAlmostEqual(
|
||||
- numpy.pmt(1 / 100 / 12, 24, 10000), line.payment_amount, 2)
|
||||
self.assertEqual(line.long_term_principal_amount, 0)
|
||||
loan.long_term_loan_account_id = self.lt_loan_account
|
||||
loan.compute_lines()
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
self.assertGreater(line.long_term_principal_amount, 0)
|
||||
self.post(loan)
|
||||
self.assertTrue(loan.start_date)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
self.assertTrue(line)
|
||||
self.assertFalse(line.move_ids)
|
||||
self.assertFalse(line.invoice_ids)
|
||||
wzd = self.env['account.loan.generate.wizard'].create({})
|
||||
action = wzd.run()
|
||||
self.assertTrue(action)
|
||||
self.assertFalse(wzd.run())
|
||||
self.assertTrue(line.move_ids)
|
||||
self.assertIn(line.move_ids.id, action['domain'][0][2])
|
||||
line.move_ids.post()
|
||||
loan.rate = 2
|
||||
loan.compute_lines()
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
self.assertAlmostEqual(
|
||||
- numpy.pmt(1 / 100 / 12, periods, amount), line.payment_amount, 2)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 2)
|
||||
self.assertAlmostEqual(
|
||||
- numpy.pmt(2 / 100 / 12, periods - 1,
|
||||
line.pending_principal_amount),
|
||||
line.payment_amount, 2
|
||||
)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 3)
|
||||
with self.assertRaises(UserError):
|
||||
line.view_process_values()
|
||||
|
||||
def test_fixed_principal_loan(self):
|
||||
amount = 24000
|
||||
periods = 24
|
||||
loan = self.create_loan('fixed-principal', amount, 1, periods)
|
||||
self.partner.property_account_payable_id = self.payable_account
|
||||
self.assertEqual(loan.journal_type, 'general')
|
||||
loan.is_leasing = True
|
||||
self.assertEqual(loan.journal_type, 'purchase')
|
||||
loan.long_term_loan_account_id = self.lt_loan_account
|
||||
loan.rate_type = 'real'
|
||||
loan.compute_lines()
|
||||
self.assertTrue(loan.line_ids)
|
||||
self.assertEqual(len(loan.line_ids), periods)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
self.assertEqual(amount / periods, line.principal_amount)
|
||||
self.assertEqual(amount / periods, line.long_term_principal_amount)
|
||||
self.post(loan)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
self.assertTrue(line)
|
||||
self.assertFalse(line.has_invoices)
|
||||
self.assertFalse(line.has_moves)
|
||||
action = self.env['account.loan.generate.wizard'].create({
|
||||
'date': fields.date.today(),
|
||||
'loan_type': 'leasing',
|
||||
}).run()
|
||||
self.assertTrue(line.has_invoices)
|
||||
self.assertFalse(line.has_moves)
|
||||
self.assertTrue(line.invoice_ids)
|
||||
self.assertFalse(line.move_ids)
|
||||
self.assertIn(line.invoice_ids.id, action['domain'][0][2])
|
||||
with self.assertRaises(UserError):
|
||||
self.env['account.loan.pay.amount'].create({
|
||||
'loan_id': loan.id,
|
||||
'amount': (amount - amount / periods) / 2,
|
||||
'fees': 100,
|
||||
'date': loan.line_ids.filtered(
|
||||
lambda r: r.sequence == 2).date
|
||||
}).run()
|
||||
with self.assertRaises(UserError):
|
||||
self.env['account.loan.pay.amount'].create({
|
||||
'loan_id': loan.id,
|
||||
'amount': (amount - amount / periods) / 2,
|
||||
'fees': 100,
|
||||
'date': datetime.strptime(
|
||||
loan.line_ids.filtered(lambda r: r.sequence == 1).date, DF
|
||||
).date() + relativedelta(months=-1)
|
||||
}).run()
|
||||
line.invoice_ids.action_invoice_open()
|
||||
self.assertTrue(line.has_moves)
|
||||
self.assertIn(
|
||||
line.move_ids.id,
|
||||
self.env['account.move'].search(
|
||||
loan.view_account_moves()['domain']).ids
|
||||
)
|
||||
self.assertEqual(
|
||||
line.invoice_ids.id,
|
||||
self.env['account.invoice'].search(
|
||||
loan.view_account_invoices()['domain']).id
|
||||
)
|
||||
with self.assertRaises(UserError):
|
||||
self.env['account.loan.pay.amount'].create({
|
||||
'loan_id': loan.id,
|
||||
'amount': (amount - amount / periods) / 2,
|
||||
'fees': 100,
|
||||
'date': loan.line_ids.filtered(
|
||||
lambda r: r.sequence == periods).date
|
||||
}).run()
|
||||
self.env['account.loan.pay.amount'].create({
|
||||
'loan_id': loan.id,
|
||||
'amount': (amount - amount / periods) / 2,
|
||||
'date': line.date,
|
||||
'fees': 100,
|
||||
}).run()
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 2)
|
||||
self.assertEqual(loan.periods, periods + 1)
|
||||
self.assertAlmostEqual(
|
||||
line.principal_amount, (amount - amount / periods) / 2, 2)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 3)
|
||||
self.assertEqual(amount / periods / 2, line.principal_amount)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 4)
|
||||
with self.assertRaises(UserError):
|
||||
line.view_process_values()
|
||||
|
||||
def test_interests_on_end_loan(self):
|
||||
amount = 10000
|
||||
periods = 10
|
||||
loan = self.create_loan('interest', amount, 1, periods)
|
||||
loan.payment_on_first_period = False
|
||||
loan.start_date = fields.Date.today()
|
||||
loan.rate_type = 'ear'
|
||||
loan.compute_lines()
|
||||
self.assertTrue(loan.line_ids)
|
||||
self.assertEqual(len(loan.line_ids), periods)
|
||||
self.assertEqual(0, loan.line_ids[0].principal_amount)
|
||||
self.assertEqual(amount, loan.line_ids.filtered(
|
||||
lambda r: r.sequence == periods
|
||||
).principal_amount)
|
||||
self.post(loan)
|
||||
self.assertEqual(loan.payment_amount, 0)
|
||||
self.assertEqual(loan.interests_amount, 0)
|
||||
self.assertEqual(loan.pending_principal_amount, amount)
|
||||
self.assertFalse(loan.line_ids.filtered(
|
||||
lambda r: (
|
||||
datetime.strptime(r.date, DF).date() <=
|
||||
datetime.strptime(loan.start_date, DF).date())))
|
||||
for line in loan.line_ids:
|
||||
self.assertEqual(loan.state, 'posted')
|
||||
line.view_process_values()
|
||||
line.move_ids.post()
|
||||
self.assertEqual(loan.state, 'closed')
|
||||
|
||||
self.assertEqual(loan.payment_amount - loan.interests_amount, amount)
|
||||
self.assertEqual(loan.pending_principal_amount, 0)
|
||||
|
||||
def test_cancel_loan(self):
|
||||
amount = 10000
|
||||
periods = 10
|
||||
loan = self.create_loan('fixed-annuity', amount, 1, periods)
|
||||
self.post(loan)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
line.view_process_values()
|
||||
line.move_ids.post()
|
||||
pay = self.env['account.loan.pay.amount'].create({
|
||||
'loan_id': loan.id,
|
||||
'amount': 0,
|
||||
'fees': 100,
|
||||
'date': line.date
|
||||
})
|
||||
pay.cancel_loan = True
|
||||
pay._onchange_cancel_loan()
|
||||
self.assertEqual(pay.amount, line.final_pending_principal_amount)
|
||||
pay.run()
|
||||
self.assertEqual(loan.state, 'cancelled')
|
||||
|
||||
def post(self, loan):
|
||||
self.assertFalse(loan.move_ids)
|
||||
post = self.env['account.loan.post'].with_context(
|
||||
default_loan_id=loan.id
|
||||
).create({})
|
||||
post.run()
|
||||
self.assertTrue(loan.move_ids)
|
||||
with self.assertRaises(UserError):
|
||||
post.run()
|
||||
|
||||
def create_account(self, code, name, type_id):
|
||||
return self.env['account.account'].create({
|
||||
'company_id': self.company.id,
|
||||
'name': name,
|
||||
'code': code,
|
||||
'user_type_id': type_id,
|
||||
'reconcile': True,
|
||||
})
|
||||
|
||||
def create_loan(self, type, amount, rate, periods):
|
||||
loan = self.env['account.loan'].create({
|
||||
'journal_id': self.journal.id,
|
||||
'rate_type': 'napr',
|
||||
'loan_type': type,
|
||||
'loan_amount': amount,
|
||||
'payment_on_first_period': True,
|
||||
'rate': rate,
|
||||
'periods': periods,
|
||||
'leased_asset_account_id': self.asset_account.id,
|
||||
'short_term_loan_account_id': self.loan_account.id,
|
||||
'interest_expenses_account_id': self.interests_account.id,
|
||||
'product_id': self.product.id,
|
||||
'interests_product_id': self.interests_product.id,
|
||||
'partner_id': self.partner.id,
|
||||
})
|
||||
loan.compute_lines()
|
||||
return loan
|
201
account_loan/views/account_loan_view.xml
Normal file
201
account_loan/views/account_loan_view.xml
Normal file
@ -0,0 +1,201 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2011 Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
Copyright 2016 Antonio Espinosa <antonio.espinosa@tecnativa.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo>
|
||||
|
||||
<record id="account_loan_tree" model="ir.ui.view">
|
||||
<field name="name">account.loan.tree</field>
|
||||
<field name="model">account.loan</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Loans">
|
||||
<field name="name"/>
|
||||
<field name="company_id"/>
|
||||
<field name="is_leasing"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="account_loan_form" model="ir.ui.view">
|
||||
<field name="name">account.loan.form</field>
|
||||
<field name="model">account.loan</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Loan">
|
||||
<header>
|
||||
<button name="compute_lines" type="object"
|
||||
string="Compute items"/>
|
||||
<button name="%(account_loan_post_action)d"
|
||||
states="draft" type="action"
|
||||
string="Post"/>
|
||||
<field name="state" widget="statusbar"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box">
|
||||
<button name="view_account_moves"
|
||||
class="oe_stat_button"
|
||||
icon="fa-bars"
|
||||
attrs="{'invisible': [('state', '=', 'draft')]}"
|
||||
type="object" string="Moves"/>
|
||||
<button name="view_account_invoices"
|
||||
class="oe_stat_button"
|
||||
icon="fa-pencil-square-o"
|
||||
attrs="{'invisible': ['|', ('state', '=', 'draft'), ('is_leasing', '=', False)]}"
|
||||
type="object" string="Invoices"/>
|
||||
<button name="%(account_loan_pay_amount_action)d"
|
||||
class="oe_stat_button" icon="fa-usd"
|
||||
attrs="{'invisible': [('state', '!=', 'posted')]}"
|
||||
type="action" string="Pay amount"/>
|
||||
</div>
|
||||
<h1>
|
||||
<field name="name"/>
|
||||
</h1>
|
||||
<group>
|
||||
<group>
|
||||
<field name="company_id"
|
||||
options="{'no_create': True}"/>
|
||||
<field name="loan_type"/>
|
||||
<field name="loan_amount"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="rate_type"/>
|
||||
<field name="rate"/>
|
||||
<field name="rate_period"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="partner_id"/>
|
||||
<field name="start_date"/>
|
||||
<field name="periods"/>
|
||||
<field name="method_period"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="is_leasing"/>
|
||||
<field name="round_on_end"/>
|
||||
<field name="payment_on_first_period"/>
|
||||
</group>
|
||||
</group>
|
||||
<group attrs="{'invisible':[('state', '=', 'draft')]}">
|
||||
<group>
|
||||
<field name="pending_principal_amount"/>
|
||||
<field name="payment_amount"/>
|
||||
<field name="interests_amount"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Items" id="items">
|
||||
<field name="line_ids"/>
|
||||
</page>
|
||||
<page string="Accounts" id="accounting">
|
||||
<group>
|
||||
<group>
|
||||
<field name="journal_id"/>
|
||||
<field name="short_term_loan_account_id"/>
|
||||
<field name="journal_type" invisible="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="long_term_loan_account_id"/>
|
||||
<field name="interest_expenses_account_id"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Leasing" id="leasing"
|
||||
attrs="{'invisible': [('is_leasing', '=', False)]}">
|
||||
<group>
|
||||
<group>
|
||||
<field name="leased_asset_account_id"
|
||||
attrs="{'required': [('is_leasing', '=', True)]}"/>
|
||||
<field name="residual_amount"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="product_id"
|
||||
attrs="{'required': [('is_leasing', '=', True)]}"/>
|
||||
<field name="interests_product_id"
|
||||
attrs="{'required': [('is_leasing', '=', True)]}"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids"
|
||||
widget="mail_followers"/>
|
||||
<field name="activity_ids" widget="mail_activity"/>
|
||||
<field name="message_ids" widget="mail_thread"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="account_loan_line_tree" model="ir.ui.view">
|
||||
<field name="name">account.loan.line.tree</field>
|
||||
<field name="model">account.loan.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Loan items" create="0">
|
||||
<field name="sequence"/>
|
||||
<field name="date"/>
|
||||
<field name="rate"/>
|
||||
<field name="pending_principal_amount"/>
|
||||
<field name="payment_amount" sum="Total payments"/>
|
||||
<field name="principal_amount"/>
|
||||
<field name="interests_amount" sum="Total interests"/>
|
||||
<field name="long_term_pending_principal_amount"
|
||||
attrs="{'invisible': [('long_term_loan_account_id', '=', False)]}"/>
|
||||
<field name="long_term_principal_amount"
|
||||
attrs="{'invisible': [('long_term_loan_account_id', '=', False)]}"/>
|
||||
<field name="long_term_loan_account_id" invisible="1"/>
|
||||
<field name="loan_state" invisible="1"/>
|
||||
<field name="is_leasing" invisible="1"/>
|
||||
<field name="has_invoices" invisible="1"/>
|
||||
<field name="has_moves" invisible="1"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<button name="view_account_values" string="Values"
|
||||
type="object" icon="fa-eye"
|
||||
attrs="{'invisible': [('has_moves', '=', False), ('has_invoices', '=', False)]}"/>
|
||||
<button name="view_process_values" string="Process"
|
||||
type="object" icon="fa-cogs"
|
||||
attrs="{'invisible': ['|', '|', ('has_moves', '=', True), ('has_invoices', '=', True), ('loan_state', '!=', 'posted')]}"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="account_loan_line_form" model="ir.ui.view">
|
||||
<field name="name">account.loan.line.form</field>
|
||||
<field name="model">account.loan.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="sequence"/>
|
||||
<field name="rate"/>
|
||||
<field name="date"/>
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="pending_principal_amount"/>
|
||||
<field name="payment_amount"/>
|
||||
<field name="principal_amount"/>
|
||||
<field name="interests_amount"/>
|
||||
<field name="final_pending_principal_amount"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="long_term_pending_principal_amount"/>
|
||||
<field name="long_term_principal_amount"/>
|
||||
</group>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<act_window
|
||||
id="account_loan_action"
|
||||
name="Loans"
|
||||
res_model="account.loan"/>
|
||||
|
||||
<menuitem id="account_loan_menu"
|
||||
parent="account.menu_finance_entries" sequence="80"
|
||||
name="Loans"
|
||||
action="account_loan_action"/>
|
||||
|
||||
</odoo>
|
21
account_loan/views/account_move_view.xml
Normal file
21
account_loan/views/account_move_view.xml
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2011 Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
Copyright 2016 Antonio Espinosa <antonio.espinosa@tecnativa.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo>
|
||||
|
||||
|
||||
<record id="view_move_form" model="ir.ui.view">
|
||||
<field name="name">Add to_be_reversed and reversal_id fields</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="company_id" position="after">
|
||||
<field name="loan_line_id"
|
||||
attrs="{'invisible': [('loan_line_id', '=', False)]}"
|
||||
readonly="True"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
5
account_loan/wizard/__init__.py
Normal file
5
account_loan/wizard/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from . import account_loan_generate_entries
|
||||
from . import account_loan_pay_amount
|
||||
from . import account_loan_post
|
52
account_loan/wizard/account_loan_generate_entries.py
Normal file
52
account_loan/wizard/account_loan_generate_entries.py
Normal file
@ -0,0 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class AccountLoanGenerateWizard(models.TransientModel):
|
||||
_name = "account.loan.generate.wizard"
|
||||
|
||||
date = fields.Date(
|
||||
'Account Date',
|
||||
required=True,
|
||||
help="Choose the period for which you want to automatically post the "
|
||||
"depreciation lines of running assets",
|
||||
default=fields.Date.context_today)
|
||||
loan_type = fields.Selection([
|
||||
('leasing', 'Leasings'),
|
||||
('loan', 'Loans'),
|
||||
], required=True, default='loan')
|
||||
|
||||
def run_leasing(self):
|
||||
created_ids = self.env['account.loan'].generate_leasing_entries(
|
||||
datetime.strptime(self.date, DF).date())
|
||||
action = self.env.ref('account.action_invoice_tree2')
|
||||
result = action.read()[0]
|
||||
if len(created_ids) == 0:
|
||||
return
|
||||
result['domain'] = [
|
||||
('id', 'in', created_ids),
|
||||
('type', '=', 'in_invoice')
|
||||
]
|
||||
return result
|
||||
|
||||
def run_loan(self):
|
||||
created_ids = self.env['account.loan'].generate_loan_entries(
|
||||
datetime.strptime(self.date, DF).date())
|
||||
action = self.env.ref('account.action_move_line_form')
|
||||
result = action.read()[0]
|
||||
if len(created_ids) == 0:
|
||||
return
|
||||
result['domain'] = [('id', 'in', created_ids)]
|
||||
return result
|
||||
|
||||
@api.multi
|
||||
def run(self):
|
||||
self.ensure_one()
|
||||
if self.loan_type == 'leasing':
|
||||
return self.run_leasing()
|
||||
return self.run_loan()
|
37
account_loan/wizard/account_loan_generate_entries_view.xml
Normal file
37
account_loan/wizard/account_loan_generate_entries_view.xml
Normal file
@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2011 Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
Copyright 2016 Antonio Espinosa <antonio.espinosa@tecnativa.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo>
|
||||
|
||||
|
||||
<record id="account_loan_generate_wizard_form" model="ir.ui.view">
|
||||
<field name="name">Pay amount</field>
|
||||
<field name="model">account.loan.generate.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Generate moves">
|
||||
<group>
|
||||
<field name="date"/>
|
||||
<field name="loan_type"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="run" string="Run"
|
||||
type="object" class="oe_highlight"/>
|
||||
or
|
||||
<button string="Cancel" class="oe_link" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="account_loan_generate_wizard_action" model="ir.actions.act_window">
|
||||
<field name="name">Generate moves</field>
|
||||
<field name="res_model">account.loan.generate.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<menuitem name="Generate Loan Entries" action="account_loan_generate_wizard_action"
|
||||
id="account_loan_generate_wizard_menu"
|
||||
parent="account.menu_finance_entries_generate_entries" sequence="111" groups="base.group_no_one"/>
|
||||
</odoo>
|
105
account_loan/wizard/account_loan_pay_amount.py
Normal file
105
account_loan/wizard/account_loan_pay_amount.py
Normal file
@ -0,0 +1,105 @@
|
||||
# Copyright 2018 Creu Blanca
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountLoan(models.TransientModel):
|
||||
_name = 'account.loan.pay.amount'
|
||||
|
||||
loan_id = fields.Many2one(
|
||||
'account.loan',
|
||||
required=True,
|
||||
readonly=True,
|
||||
)
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency',
|
||||
related='loan_id.currency_id',
|
||||
readonly=True
|
||||
)
|
||||
cancel_loan = fields.Boolean(
|
||||
default=False,
|
||||
)
|
||||
date = fields.Date(required=True, default=fields.Date.today())
|
||||
amount = fields.Monetary(
|
||||
currency_field='currency_id',
|
||||
string='Amount to reduce from Principal',
|
||||
)
|
||||
fees = fields.Monetary(
|
||||
currency_field='currency_id',
|
||||
string='Bank fees'
|
||||
)
|
||||
|
||||
@api.onchange('cancel_loan')
|
||||
def _onchange_cancel_loan(self):
|
||||
if self.cancel_loan:
|
||||
self.amount = max(self.loan_id.line_ids.filtered(
|
||||
lambda r: not r.move_ids and not r.invoice_ids).mapped(
|
||||
'pending_principal_amount'
|
||||
)
|
||||
)
|
||||
|
||||
def new_line_vals(self, sequence):
|
||||
return {
|
||||
'loan_id': self.loan_id.id,
|
||||
'sequence': sequence,
|
||||
'payment_amount': self.amount + self.fees,
|
||||
'rate': 0,
|
||||
'interests_amount': self.fees,
|
||||
'date': self.date,
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def run(self):
|
||||
self.ensure_one()
|
||||
if self.loan_id.is_leasing:
|
||||
if self.loan_id.line_ids.filtered(
|
||||
lambda r: r.date < self.date and not r.invoice_ids
|
||||
):
|
||||
raise UserError(_('Some invoices are not created'))
|
||||
if self.loan_id.line_ids.filtered(
|
||||
lambda r: r.date > self.date and r.invoice_ids
|
||||
):
|
||||
raise UserError(_('Some future invoices already exists'))
|
||||
if self.loan_id.line_ids.filtered(
|
||||
lambda r: r.date < self.date and not r.move_ids
|
||||
):
|
||||
raise UserError(_('Some moves are not created'))
|
||||
if self.loan_id.line_ids.filtered(
|
||||
lambda r: r.date > self.date and r.move_ids
|
||||
):
|
||||
raise UserError(_('Some future moves already exists'))
|
||||
lines = self.loan_id.line_ids.filtered(
|
||||
lambda r: r.date > self.date).sorted('sequence', reverse=True)
|
||||
sequence = min(lines.mapped('sequence'))
|
||||
for line in lines:
|
||||
line.sequence += 1
|
||||
old_line = lines.filtered(lambda r: r.sequence == sequence + 1)
|
||||
pending = old_line.pending_principal_amount
|
||||
if self.loan_id.currency_id.compare_amounts(self.amount, pending) == 1:
|
||||
raise UserError(_('Amount cannot be bigger than debt'))
|
||||
if self.loan_id.currency_id.compare_amounts(self.amount, 0) <= 0:
|
||||
raise UserError(_('Amount cannot be less than zero'))
|
||||
self.loan_id.periods += 1
|
||||
self.loan_id.fixed_periods = self.loan_id.periods - sequence
|
||||
self.loan_id.fixed_loan_amount = pending - self.amount
|
||||
new_line = self.env['account.loan.line'].create(
|
||||
self.new_line_vals(sequence))
|
||||
new_line.long_term_pending_principal_amount = (
|
||||
old_line.long_term_pending_principal_amount)
|
||||
amount = self.loan_id.loan_amount
|
||||
for line in self.loan_id.line_ids.sorted('sequence'):
|
||||
if line.move_ids:
|
||||
amount = line.final_pending_principal_amount
|
||||
else:
|
||||
line.pending_principal_amount = amount
|
||||
if line.sequence != sequence:
|
||||
line.rate = self.loan_id.rate_period
|
||||
line.check_amount()
|
||||
amount -= line.payment_amount - line.interests_amount
|
||||
if self.loan_id.long_term_loan_account_id:
|
||||
self.loan_id.check_long_term_principal_amount()
|
||||
if self.loan_id.currency_id.compare_amounts(pending, self.amount) == 0:
|
||||
self.loan_id.write({'state': 'cancelled'})
|
||||
return new_line.view_process_values()
|
41
account_loan/wizard/account_loan_pay_amount_view.xml
Normal file
41
account_loan/wizard/account_loan_pay_amount_view.xml
Normal file
@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2011 Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
Copyright 2016 Antonio Espinosa <antonio.espinosa@tecnativa.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo>
|
||||
|
||||
|
||||
<record id="account_loan_pay_amount_form" model="ir.ui.view">
|
||||
<field name="name">Pay amount</field>
|
||||
<field name="model">account.loan.pay.amount</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Pay amount">
|
||||
<group>
|
||||
<field name="loan_id" readonly="True"/>
|
||||
<field name="date"/>
|
||||
<field name="cancel_loan"/>
|
||||
<field name="amount"/>
|
||||
<field name="fees"/>
|
||||
<field name="currency_id"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="run" string="Run"
|
||||
type="object" class="oe_highlight"/>
|
||||
or
|
||||
<button string="Cancel" class="oe_link" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
|
||||
<record id="account_loan_pay_amount_action" model="ir.actions.act_window">
|
||||
<field name="name">Pay amount</field>
|
||||
<field name="res_model">account.loan.pay.amount</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="context">{'default_loan_id': active_id}</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
94
account_loan/wizard/account_loan_post.py
Normal file
94
account_loan/wizard/account_loan_post.py
Normal file
@ -0,0 +1,94 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountLoanPost(models.TransientModel):
|
||||
_name = "account.loan.post"
|
||||
|
||||
@api.model
|
||||
def _default_journal_id(self):
|
||||
loan_id = self._context.get('default_loan_id')
|
||||
if loan_id:
|
||||
return self.env['account.loan'].browse(loan_id).journal_id.id
|
||||
|
||||
@api.model
|
||||
def _default_account_id(self):
|
||||
loan_id = self._context.get('default_loan_id')
|
||||
if loan_id:
|
||||
loan = self.env['account.loan'].browse(loan_id)
|
||||
if loan.is_leasing:
|
||||
return loan.leased_asset_account_id.id
|
||||
else:
|
||||
return loan.partner_id.with_context(
|
||||
force_company=loan.company_id.id
|
||||
).property_account_receivable_id.id
|
||||
|
||||
loan_id = fields.Many2one(
|
||||
'account.loan',
|
||||
required=True,
|
||||
readonly=True,
|
||||
)
|
||||
journal_id = fields.Many2one(
|
||||
'account.journal',
|
||||
required=True,
|
||||
default=_default_journal_id
|
||||
)
|
||||
account_id = fields.Many2one(
|
||||
'account.account',
|
||||
required=True,
|
||||
default=_default_account_id
|
||||
)
|
||||
|
||||
def move_line_vals(self):
|
||||
res = list()
|
||||
partner = self.loan_id.partner_id.with_context(
|
||||
force_company=self.loan_id.company_id.id)
|
||||
line = self.loan_id.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
res.append({
|
||||
'account_id': self.account_id.id,
|
||||
'partner_id': partner.id,
|
||||
'credit': 0,
|
||||
'debit': line.pending_principal_amount,
|
||||
})
|
||||
if (
|
||||
line.pending_principal_amount -
|
||||
line.long_term_pending_principal_amount > 0
|
||||
):
|
||||
res.append({
|
||||
'account_id': self.loan_id.short_term_loan_account_id.id,
|
||||
'credit': (line.pending_principal_amount -
|
||||
line.long_term_pending_principal_amount),
|
||||
'debit': 0,
|
||||
})
|
||||
if (
|
||||
line.long_term_pending_principal_amount > 0 and
|
||||
self.loan_id.long_term_loan_account_id
|
||||
):
|
||||
res.append({
|
||||
'account_id': self.loan_id.long_term_loan_account_id.id,
|
||||
'credit': line.long_term_pending_principal_amount,
|
||||
'debit': 0,
|
||||
})
|
||||
|
||||
return res
|
||||
|
||||
def move_vals(self):
|
||||
return {
|
||||
'loan_id': self.loan_id.id,
|
||||
'date': self.loan_id.start_date,
|
||||
'ref': self.loan_id.name,
|
||||
'journal_id': self.journal_id.id,
|
||||
'line_ids': [(0, 0, vals) for vals in self.move_line_vals()]
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def run(self):
|
||||
self.ensure_one()
|
||||
if self.loan_id.state != 'draft':
|
||||
raise UserError(_('Only loans in draft state can be posted'))
|
||||
self.loan_id.post()
|
||||
move = self.env['account.move'].create(self.move_vals())
|
||||
move.post()
|
38
account_loan/wizard/account_loan_post_view.xml
Normal file
38
account_loan/wizard/account_loan_post_view.xml
Normal file
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2011 Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
Copyright 2016 Antonio Espinosa <antonio.espinosa@tecnativa.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo>
|
||||
|
||||
|
||||
<record id="account_loan_post_form" model="ir.ui.view">
|
||||
<field name="name">Post loan</field>
|
||||
<field name="model">account.loan.post</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Pay amount">
|
||||
<group>
|
||||
<field name="loan_id" readonly="True"/>
|
||||
<field name="account_id"/>
|
||||
<field name="journal_id"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="run" string="Run"
|
||||
type="object" class="oe_highlight"/>
|
||||
or
|
||||
<button string="Cancel" class="oe_link" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
|
||||
<record id="account_loan_post_action" model="ir.actions.act_window">
|
||||
<field name="name">Post loan</field>
|
||||
<field name="res_model">account.loan.post</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="context">{'default_loan_id': active_id}</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
Loading…
Reference in New Issue
Block a user