Initial commit
This commit is contained in:
180
Fusion Accounting/wizard/loan_import_wizard.py
Normal file
180
Fusion Accounting/wizard/loan_import_wizard.py
Normal 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',
|
||||
}
|
||||
Reference in New Issue
Block a user