Initial commit on branch odoo_DuyDat

This commit is contained in:
Duy Dat
2025-06-13 14:20:29 +07:00
commit 7819a1d6db
228 changed files with 4502 additions and 0 deletions
@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Gayathri V (odoo@cybrosys.com)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
from . import account_move
from . import account_payment_register
from . import loan_documents
from . import loan_request
from . import loan_type
from . import repayment_lines
from . import res_config_settings
from . import res_partner
@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Gayathri V (odoo@cybrosys.com)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
from odoo import models
class AccountMove(models.Model):
"""Alter loan repayment line state on draft and cancel button click"""
_inherit = 'account.move'
def button_draft(self):
"""Change repayment record state to 'invoiced'
while reset to draft the invoice"""
res = super().button_draft()
loan_line_ids = self.env['repayment.line'].search([
('name', 'ilike', self.payment_reference)])
if loan_line_ids:
loan_line_ids.update({
'state': 'invoiced',
'invoice': True
})
return res
def button_cancel(self):
"""Change repayment record state to 'unpaid'
while cancelling the invoice"""
res = super().button_cancel()
for record in self:
loan_line_ids = self.env['repayment.line'].search([
('name', 'ilike', record.payment_reference)])
if loan_line_ids:
loan_line_ids.update({
'state': 'unpaid',
'invoice': False
})
return res
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Gayathri V (odoo@cybrosys.com)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
from odoo import models
class AccountPaymentRegister(models.TransientModel):
"""Alter loan repayment line state based on invoice status"""
_inherit = 'account.payment.register'
def _post_payments(self, to_process, edit_mode=False):
"""Change repayment record state to 'paid' while registering the
payment"""
res = super()._post_payments(to_process, edit_mode=False)
for record in self:
loan_line_id = self.env['repayment.line'].search([
('name', 'ilike', record.communication)])
loan_line_id.write({'state': 'paid'})
return res
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Gayathri V (odoo@cybrosys.com)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
from odoo import fields, models
class LoanDocuments(models.Model):
"""Documents required to approve loan, eg:-Aadhar, Pan"""
_name = 'loan.documents'
_description = 'Loan Documents'
_rec_name = 'loan_proofs'
loan_proofs = fields.Char(string="Proofs", help="Document name "
"for identification", required=True)
company_id = fields.Many2one('res.company', string='Company',
readonly=True,
help="Company Name",
default=lambda self:
self.env.company)
@@ -0,0 +1,265 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Gayathri V (odoo@cybrosys.com)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
from datetime import datetime
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class LoanRequest(models.Model):
"""Can create new loan requests and manage records"""
_name = 'loan.request'
_inherit = ['mail.thread']
_description = 'Loan Request'
name = fields.Char(string='Loan Reference', readonly=True,
copy=False, help="Sequence number for loan requests",
default=lambda self: 'New')
company_id = fields.Many2one('res.company', string='Company',
readonly=True,
help="Company Name",
default=lambda self: self.env.company)
currency_id = fields.Many2one('res.currency', string='Currency',
required=True, help="Currency",
default=lambda self: self.env.user.company_id.
currency_id)
loan_type_id = fields.Many2one('loan.type', string='Loan Type',
required=True, help="Can choose different "
"loan types suitable")
loan_amount = fields.Float(string="Loan Amount",
help="Total loan amount", )
disbursal_amount = fields.Float(string="Disbursal Amount",
help="Total loan amount "
"available to disburse")
tenure = fields.Integer(string="Tenure", default=1,
help="Installment period")
interest_rate = fields.Float(string="Interest Rate", help="Interest "
"percentage")
date = fields.Date(string="Date", default=fields.Date.today(),
readonly=True, help="Date")
partner_id = fields.Many2one('res.partner', string="Partner",
required=True,
help="Partner")
repayment_lines_ids = fields.One2many('repayment.line',
'loan_id',
string="Loan Line", index=True,
help="Repayment lines")
documents_ids = fields.Many2many('loan.documents',
string="Proofs",
help="Documents as proof")
img_attachment_ids = fields.Many2many('ir.attachment',
relation="m2m_ir_identity_card_rel",
column1="documents_ids",
string="Images",
help="Image proofs")
journal_id = fields.Many2one('account.journal',
string="Journal",
help="Journal types",
domain="[('type', '=', 'purchase'),"
"('company_id', '=', company_id)]",
)
debit_account_id = fields.Many2one('account.account',
string="Debit account",
help="Choose account for "
"disbursement debit")
credit_account_id = fields.Many2one('account.account',
string="Credit account",
help="Choose account for "
"disbursement credit")
reject_reason = fields.Text(string="Reason", help="Displays "
"rejected reason")
request = fields.Boolean(string="Request",
help="For monitoring the record")
state = fields.Selection(string='State',
selection=[('draft', 'Draft'), ('confirmed', 'Confirmed'),
('waiting', 'Waiting For Approval'),
('approved', 'Approved'), ('disbursed', 'Disbursed'),
('rejected', 'Rejected'), ('closed', 'Closed')],
copy=False, tracking=True, default='draft', help="Loan request states")
@api.model
def create(self, vals):
"""create auto sequence for the loan request records"""
loan_count = self.env['loan.request'].search(
[('partner_id', '=', vals['partner_id']),
('state', 'not in', ('draft', 'rejected', 'closed'))])
if loan_count:
for rec in loan_count:
if rec.state != 'closed':
raise UserError(
_('The partner has already an ongoing loan.'))
else:
if vals.get('name', 'New') == 'New':
vals['name'] = self.env['ir.sequence'].next_by_code(
'increment_loan_ref')
res = super().create(vals)
return res
@api.onchange('loan_type_id')
def _onchange_loan_type_id(self):
"""Changing field values based on the chosen loan type"""
type_id = self.loan_type_id
self.loan_amount = type_id.loan_amount
self.disbursal_amount = type_id.disbursal_amount
self.tenure = type_id.tenure
self.interest_rate = type_id.interest_rate
self.documents_ids = type_id.documents_ids
def action_loan_request(self):
"""Changes the state to confirmed and send confirmation mail"""
self.write({'state': "confirmed"})
partner = self.partner_id
loan_no = self.name
subject = 'Loan Confirmation'
message = (f"Dear {partner.name},<br/> This is a confirmation mail "
f"for your loan{loan_no}. We have submitted your loan "
f"for approval.")
outgoing_mail = self.company_id.email
mail_values = {
'subject': subject,
'email_from': outgoing_mail,
'author_id': self.env.user.partner_id.id,
'email_to': partner.email,
'body_html': message,
}
mail = self.env['mail.mail'].sudo().create(mail_values)
mail.send()
def action_request_for_loan(self):
"""Change the state to waiting for approval"""
if self.request:
self.write({'state': "waiting"})
else:
message_id = self.env['message.popup'].create(
{'message': _("Compute the repayments before requesting")})
return {
'name': _('Repayment'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'message.popup',
'res_id': message_id.id,
'target': 'new'
}
def action_loan_approved(self):
"""Change to Approved state"""
self.write({'state': "approved"})
def action_disburse_loan(self):
"""Disbursing the loan to customer and creating journal
entry for the disbursement"""
self.write({'state': "disbursed"})
for loan in self:
amount = loan.disbursal_amount
loan_name = loan.partner_id.name
reference = loan.name
journal_id = loan.journal_id.id
debit_account_id = loan.debit_account_id.id
credit_account_id = loan.credit_account_id.id
date_now = loan.date
debit_vals = {
'name': loan_name,
'account_id': debit_account_id,
'journal_id': journal_id,
'date': date_now,
'debit': amount > 0.0 and amount or 0.0,
'credit': amount < 0.0 and -amount or 0.0,
}
credit_vals = {
'name': loan_name,
'account_id': credit_account_id,
'journal_id': journal_id,
'date': date_now,
'debit': amount < 0.0 and -amount or 0.0,
'credit': amount > 0.0 and amount or 0.0,
}
vals = {
'name': f'DIS / {reference}',
'narration': reference,
'ref': reference,
'journal_id': journal_id,
'date': date_now,
'line_ids': [(0, 0, debit_vals), (0, 0, credit_vals)]
}
move = self.env['account.move'].create(vals)
move.action_post()
return True
def action_close_loan(self):
"""Closing the loan"""
demo = []
for check in self.repayment_lines_ids:
if check.state == 'unpaid':
demo.append(check)
if len(demo) >= 1:
message_id = self.env['message.popup'].create(
{'message': _("Pending Repayments")})
return {
'name': _('Repayment'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'message.popup',
'res_id': message_id.id,
'target': 'new'
}
self.write({'state': "closed"})
def action_loan_rejected(self):
"""You can add reject reasons here"""
return {'type': 'ir.actions.act_window',
'name': 'Loan Rejection',
'res_model': 'reject.reason',
'target': 'new',
'view_mode': 'form',
'context': {'default_loan': self.name}
}
def action_compute_repayment(self):
"""This automatically create the installment the employee need to pay to
company based on payment start date and the no of installments.
"""
self.request = True
for loan in self:
loan.repayment_lines_ids.unlink()
date_start = datetime.strptime(str(loan.date),'%Y-%m-%d') + relativedelta(months=1)
amount = loan.loan_amount / loan.tenure
interest = loan.loan_amount * loan.interest_rate
interest_amount = interest / loan.tenure
total_amount = amount + interest_amount
partner = self.partner_id
for rand_num in range(1, loan.tenure + 1):
self.env['repayment.line'].create({
'name': f"{loan.name}/{rand_num}",
'partner_id': partner.id,
'date': date_start,
'amount': amount,
'interest_amount': interest_amount,
'total_amount': total_amount,
'interest_account_id': self.env.ref('advanced_loan_management.'
'loan_management_'
'inrst_accounts').id,
'repayment_account_id': self.env.ref('advanced_loan_management.'
'demo_'
'loan_accounts').id,
'loan_id': loan.id})
date_start += relativedelta(months=1)
return True
@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Gayathri V (odoo@cybrosys.com)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
from odoo import api, fields, models
class LoanTypes(models.Model):
"""Create different types of Loans, And can wisely choose while requesting
for loan"""
_name = 'loan.type'
_inherit = ['mail.thread']
_description = 'Loan Type'
name = fields.Char(string='Name', help="LoanType Name", required=True)
loan_amount = fields.Integer(string='Loan Amount', help="Loan Amount", required=True)
tenure = fields.Integer(string='Tenure', default='1',
help="Amortization period")
tenure_plan = fields.Char(string="Tenure Plan", default='Monthly',
readonly='True', help="EMI payment plan")
interest_rate = fields.Float(string='Interest Rate',
help="Loan Interest Rate")
disbursal_amount = fields.Float(string='Disbursal Amount',
compute='_compute_disbursal_amount',
help="Total Amount To Be Disbursed")
documents_ids = fields.Many2many('loan.documents',
string="Documents",
help="Personal Proofs")
processing_fee = fields.Integer(string="Processing Fee",
help="Amount For Initializing The Loan")
note = fields.Text(string="Criteria", help="Criteria for approving "
"loan requests")
company_id = fields.Many2one('res.company', string='Company',
readonly=True, help="Company Name",
default=lambda self: self.env.company, )
@api.depends('processing_fee', 'loan_amount')
def _compute_disbursal_amount(self):
"""Calculating amount for disbursing"""
self.disbursal_amount = self.loan_amount - self.processing_fee
@@ -0,0 +1,149 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Gayathri V (odoo@cybrosys.com)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
from odoo import fields, models
class RepaymentLine(models.Model):
"""Loan repayments """
_name = "repayment.line"
_description = "Repayment Line"
name = fields.Char(string="Loan ", default="/", readonly=True,
help="Repayment no: of loan")
partner_id = fields.Many2one('res.partner', string="Partner",
required=True,
help="Partner")
company_id = fields.Many2one('res.company', string='Company',
readonly=True,
help="Company",
default=lambda self: self.env.company)
date = fields.Date(string="Payment Date", required=True,
default=fields.Date.today(),
readonly=True,
help="Date of the payment")
amount = fields.Float(string="Amount", required=True, help="Amount",
digits=(16, 2))
interest_amount = fields.Float(string="Interest Amount", required=True,
help="Interest Amount", digits=(16, 2))
total_amount = fields.Float(string="Total Amount", required=True,
help="Total Amount", digits=(16, 2))
loan_id = fields.Many2one('loan.request', string="Loan Ref.",
help="Loan",
readonly=True)
state = fields.Selection(string="State",
selection=[('unpaid', 'Unpaid'),
('invoiced', 'Invoiced'),
('paid', 'Paid')], copy=False,
default='unpaid',
help="Includes paid and unpaid states for each "
"repayments", )
journal_loan_id = fields.Many2one('account.journal',
string="Journal",
store=True, default=lambda self: self.
env['account.journal'].
search([('code', 'like', 'CSH1')]),
help="Journal Record")
interest_account_id = fields.Many2one('account.account',
string="Interest",
store=True,
help="Account For Interest")
repayment_account_id = fields.Many2one('account.account',
string="Repayment",
store=True,
help="Account For Repayment")
invoice = fields.Boolean(string="invoice", default=False,
help="For monitoring the record")
def action_pay_emi(self):
"""Creates invoice for each EMI"""
time_now = self.date
interest_product_id = self.env['ir.config_parameter'].sudo().get_param(
'advanced_loan_management.interest_product_id')
repayment_product_id = self.env['ir.config_parameter'].sudo().get_param(
'advanced_loan_management.repayment_product_id')
for rec in self:
loan_lines_ids = self.env['repayment.line'].search(
[('loan_id', '=', rec.loan_id.id)], order='date asc')
for line in loan_lines_ids:
if line.date < rec.date and line.state in \
('unpaid', 'invoiced'):
message_id = self.env['message.popup'].create(
{'message': (
"You have pending amounts")})
return {
'name': 'Repayment',
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'message.popup',
'res_id': message_id.id,
'target': 'new'
}
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'invoice_date': time_now,
'partner_id': self.partner_id.id,
'currency_id': self.company_id.currency_id.id,
'payment_reference': self.name,
'invoice_line_ids': [
(0, 0, {
'price_unit': self.amount,
'product_id': repayment_product_id,
'name': 'Repayment',
'account_id': self.repayment_account_id.id,
'quantity': 1,
}),
(0, 0, {
'price_unit': self.interest_amount,
'product_id': interest_product_id,
'name': 'Interest amount',
'account_id': self.interest_account_id.id,
'quantity': 1,
}),
],
})
if invoice:
invoice.action_post()
self.invoice=True
self.write({'state':'invoiced'})
return {
'name': 'Invoice',
'res_model': 'account.move',
'res_id': invoice.id,
'type': 'ir.actions.act_window',
'view_mode': 'form',
}
def action_view_invoice(self):
"""To view the invoices"""
invoice = self.env['account.move'].search([
('payment_reference', '=', self.name)
])
self.invoice = True
return {
'name': 'Invoice',
'res_model': 'account.move',
'res_id': invoice.id,
'type': 'ir.actions.act_window',
'view_mode': 'form',
}
@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Gayathri V (odoo@cybrosys.com)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
"""Add new fields to display service products"""
_inherit = 'res.config.settings'
interest_product_id = fields.Many2one('product.product',
string="Interest Product",
config_parameter="advanced_loan_management.interest_product_id",
help="Product For Interest "
"To Create Invoice Lines")
repayment_product_id = fields.Many2one('product.product',
string="Repayment Product",
config_parameter="advanced_loan_management.repayment_product_id",
help="Product For Repayment "
"To Create Invoice Lines")
@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Gayathri V (odoo@cybrosys.com)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
from odoo import fields, models
class ResPartner(models.Model):
"""Add new tab to display partner's loan count"""
_inherit = "res.partner"
def _compute_partner_loans(self):
"""This compute the loan amount and total loans count of a partner."""
self.loan_count = self.env['loan.request'].search_count(
[('partner_id', '=', self.id),
('state', 'in', ('disbursed', 'closed'))])
loan_count = fields.Integer(string="Loan Count",
compute='_compute_partner_loans',
help="Displays numbers of loans "
"ongoing and closed by the employee")
def action_view_loans(self):
"""Returns loan records of current employee"""
return {
'type': 'ir.actions.act_window',
'name': 'Loans',
'view_mode': 'tree',
'res_model': 'loan.request',
'domain': [('partner_id', '=', self.id)],
'context': "{'create': False}"
}