Initial commit

This commit is contained in:
gsinghpal
2026-02-22 01:22:18 -05:00
commit 5200d5baf0
2394 changed files with 386834 additions and 0 deletions

View File

@@ -0,0 +1,180 @@
"""
Fusion Accounting - Loan CSV Import Wizard
Allows bulk-importing loan records from a CSV file with the columns:
name, principal, rate, term, start_date.
After import, each loan is created in Draft state so the user can
review parameters and compute the amortization schedule manually.
"""
import base64
import csv
import io
from datetime import datetime
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
class FusionLoanImportWizard(models.TransientModel):
"""Transient wizard to import loans from a CSV file.
Expected CSV columns (header required):
- name : Loan reference / description
- principal : Principal amount (float)
- rate : Annual interest rate in percent (float)
- term : Loan term in months (integer)
- start_date : Start date in YYYY-MM-DD format
"""
_name = 'fusion.loan.import.wizard'
_description = 'Import Loans from CSV'
csv_file = fields.Binary(
string='CSV File',
required=True,
help="Upload a CSV file with columns: name, principal, rate, term, start_date.",
)
csv_filename = fields.Char(string='Filename')
journal_id = fields.Many2one(
'account.journal',
string='Default Journal',
required=True,
domain="[('type', 'in', ['bank', 'general'])]",
help="Journal to assign to all imported loans.",
)
loan_account_id = fields.Many2one(
'account.account',
string='Default Loan Account',
required=True,
help="Liability account for imported loans.",
)
interest_account_id = fields.Many2one(
'account.account',
string='Default Interest Account',
required=True,
help="Expense account for interest on imported loans.",
)
partner_id = fields.Many2one(
'res.partner',
string='Default Lender',
required=True,
help="Default lender assigned when the CSV doesn't specify one.",
)
payment_frequency = fields.Selection(
selection=[
('monthly', 'Monthly'),
('quarterly', 'Quarterly'),
('semi_annually', 'Semi-Annually'),
('annually', 'Annually'),
],
string='Default Payment Frequency',
default='monthly',
required=True,
)
amortization_method = fields.Selection(
selection=[
('french', 'French (Equal Payments)'),
('linear', 'Linear (Equal Principal)'),
],
string='Default Amortization Method',
default='french',
required=True,
)
auto_compute = fields.Boolean(
string='Auto-Compute Schedules',
default=False,
help="Automatically compute the amortization schedule after import.",
)
# Required CSV header columns
_REQUIRED_COLUMNS = {'name', 'principal', 'rate', 'term', 'start_date'}
def action_import(self):
"""Parse the uploaded CSV and create loan records."""
self.ensure_one()
if not self.csv_file:
raise UserError(_("Please upload a CSV file."))
# Decode file
try:
raw = base64.b64decode(self.csv_file)
text = raw.decode('utf-8-sig') # handles BOM gracefully
except Exception as exc:
raise UserError(
_("Unable to read the file. Ensure it is a valid UTF-8 CSV. (%s)", exc)
)
reader = csv.DictReader(io.StringIO(text))
# Validate headers
if not reader.fieldnames:
raise UserError(_("The CSV file appears to be empty."))
headers = {h.strip().lower() for h in reader.fieldnames}
missing = self._REQUIRED_COLUMNS - headers
if missing:
raise UserError(
_("Missing required CSV columns: %s", ', '.join(sorted(missing)))
)
# Build column name mapping (stripped & lowered -> original)
col_map = {h.strip().lower(): h for h in reader.fieldnames}
loan_vals_list = []
errors = []
for row_num, row in enumerate(reader, start=2):
try:
name = (row.get(col_map['name']) or '').strip()
principal = float(row.get(col_map['principal'], 0))
rate = float(row.get(col_map['rate'], 0))
term = int(row.get(col_map['term'], 0))
start_raw = (row.get(col_map['start_date']) or '').strip()
start_date = datetime.strptime(start_raw, '%Y-%m-%d').date()
except (ValueError, TypeError) as exc:
errors.append(_("Row %s: %s", row_num, exc))
continue
if principal <= 0:
errors.append(_("Row %s: principal must be positive.", row_num))
continue
if term <= 0:
errors.append(_("Row %s: term must be positive.", row_num))
continue
loan_vals_list.append({
'name': name or _('New'),
'partner_id': self.partner_id.id,
'journal_id': self.journal_id.id,
'loan_account_id': self.loan_account_id.id,
'interest_account_id': self.interest_account_id.id,
'principal_amount': principal,
'interest_rate': rate,
'loan_term': term,
'start_date': start_date,
'payment_frequency': self.payment_frequency,
'amortization_method': self.amortization_method,
})
if errors:
raise UserError(
_("Import errors:\n%s", '\n'.join(str(e) for e in errors))
)
if not loan_vals_list:
raise UserError(_("No valid loan rows found in the CSV file."))
loans = self.env['fusion.loan'].create(loan_vals_list)
if self.auto_compute:
for loan in loans:
loan.compute_amortization_schedule()
# Return the newly created loans
return {
'name': _('Imported Loans'),
'type': 'ir.actions.act_window',
'res_model': 'fusion.loan',
'view_mode': 'list,form',
'domain': [('id', 'in', loans.ids)],
'target': 'current',
}