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,428 @@
"""
Fusion Accounting - Integration Bridge Modules
===============================================
Provides optional glue code between Fusion Accounting and other Odoo
applications. Each bridge class extends a core accounting model with
fields and methods that only become meaningful when the target module
(fleet, hr_expense, helpdesk) is installed.
All dependencies are **soft**: the bridges use ``try/except ImportError``
guards so that Fusion Accounting installs and operates normally even
when the partner modules are absent.
Bridges
-------
* **FusionFleetBridge** -- tags journal-item expenses to fleet vehicles.
* **FusionExpenseBridge** -- creates journal entries from approved
HR expense sheets.
* **FusionHelpdeskBridge** -- generates credit notes linked to helpdesk
tickets for rapid customer resolution.
Copyright (c) Nexa Systems Inc. - All rights reserved.
"""
import logging
from odoo import api, fields, models, Command, _
from odoo.exceptions import UserError, ValidationError
_logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Soft-dependency probes
# ---------------------------------------------------------------------------
# Each flag is True only when the corresponding Odoo module is importable.
# The flags are evaluated at *module-load* time, so they reflect the state
# of the Odoo installation rather than the database registry.
_fleet_available = False
try:
from odoo.addons.fleet import models as _fleet_models # noqa: F401
_fleet_available = True
except ImportError:
_logger.debug("fleet module not available -- FusionFleetBridge will be inert.")
_hr_expense_available = False
try:
from odoo.addons.hr_expense import models as _hr_expense_models # noqa: F401
_hr_expense_available = True
except ImportError:
_logger.debug("hr_expense module not available -- FusionExpenseBridge will be inert.")
_helpdesk_available = False
try:
from odoo.addons.helpdesk import models as _helpdesk_models # noqa: F401
_helpdesk_available = True
except ImportError:
_logger.debug("helpdesk module not available -- FusionHelpdeskBridge will be inert.")
# ═══════════════════════════════════════════════════════════════════════════
# Fleet Bridge
# ═══════════════════════════════════════════════════════════════════════════
class FusionFleetBridge(models.Model):
"""Extends journal items so each line can optionally reference a fleet
vehicle, enabling per-vehicle cost tracking and reporting.
When the *fleet* module is **not** installed the ``fusion_vehicle_id``
field is still created (as an orphan Many2one) but it will never
resolve, and the UI hides it via conditional visibility.
"""
_name = "account.move.line"
_inherit = "account.move.line"
# ---- Fields ----
fusion_vehicle_id = fields.Many2one(
comodel_name="fleet.vehicle",
string="Vehicle",
index="btree_not_null",
ondelete="set null",
copy=True,
help=(
"Optionally link this journal item to a fleet vehicle for "
"per-vehicle expense tracking and cost-center analysis."
),
)
fusion_vehicle_license_plate = fields.Char(
related="fusion_vehicle_id.license_plate",
string="License Plate",
readonly=True,
store=False,
)
# ---- Helpers ----
def _fusion_is_fleet_installed(self):
"""Runtime check: is the *fleet* model actually registered?"""
return "fleet.vehicle" in self.env
@api.onchange("fusion_vehicle_id")
def _onchange_fusion_vehicle_id(self):
"""When a vehicle is selected, suggest the vehicle's display name
as the line label if the label is currently empty."""
for line in self:
if line.fusion_vehicle_id and not line.name:
line.name = _("Expense: %s", line.fusion_vehicle_id.display_name)
# ═══════════════════════════════════════════════════════════════════════════
# HR Expense Bridge
# ═══════════════════════════════════════════════════════════════════════════
class FusionExpenseBridge(models.Model):
"""Links journal entries to approved HR expense sheets and provides a
method to generate accounting entries from those sheets.
When *hr_expense* is **not** installed the field and method remain on
the model but are functionally inert.
"""
_name = "account.move"
_inherit = "account.move"
# ---- Fields ----
fusion_expense_sheet_id = fields.Many2one(
comodel_name="hr.expense.sheet",
string="Expense Report",
index="btree_not_null",
ondelete="set null",
copy=False,
readonly=True,
help=(
"The HR expense sheet from which this journal entry was "
"generated. Populated automatically by the bridge."
),
)
fusion_expense_employee_id = fields.Many2one(
related="fusion_expense_sheet_id.employee_id",
string="Expense Employee",
readonly=True,
store=False,
)
# ---- Helpers ----
def _fusion_is_hr_expense_installed(self):
"""Runtime check: is the *hr.expense.sheet* model registered?"""
return "hr.expense.sheet" in self.env
# ---- Actions ----
def action_open_expense_sheet(self):
"""Navigate to the linked expense sheet form."""
self.ensure_one()
if not self.fusion_expense_sheet_id:
raise UserError(_("No expense report is linked to this entry."))
return {
"type": "ir.actions.act_window",
"res_model": "hr.expense.sheet",
"res_id": self.fusion_expense_sheet_id.id,
"view_mode": "form",
"target": "current",
}
# ---- Core method: create journal entry from expense sheet ----
@api.model
def create_move_from_expense_sheet(self, expense_sheet_id):
"""Generate a journal entry from an approved HR expense sheet.
:param int expense_sheet_id: id of the ``hr.expense.sheet`` record.
:returns: the newly created ``account.move`` recordset.
:raises UserError: if the expense sheet is not in *approve* state,
or if the hr_expense module is not installed.
"""
if not self._fusion_is_hr_expense_installed():
raise UserError(
_("The HR Expense module is not installed. "
"Please install it before creating entries from expense sheets.")
)
sheet = self.env["hr.expense.sheet"].browse(expense_sheet_id)
if not sheet.exists():
raise UserError(_("Expense sheet #%d does not exist.", expense_sheet_id))
if sheet.state != "approve":
raise UserError(
_("Only approved expense reports can be converted to journal "
"entries. Current status: %s.", sheet.state)
)
# Determine the journal -- prefer the company's expense journal,
# fall back to the first available miscellaneous journal.
journal = self.env["account.journal"].search(
[
("company_id", "=", sheet.company_id.id),
("type", "=", "purchase"),
],
limit=1,
)
if not journal:
journal = self.env["account.journal"].search(
[
("company_id", "=", sheet.company_id.id),
("type", "=", "general"),
],
limit=1,
)
if not journal:
raise UserError(
_("No suitable purchase or miscellaneous journal found for "
"company %s.", sheet.company_id.name)
)
# Build move-line values from each expense line.
move_line_vals = []
total_amount = 0.0
for expense in sheet.expense_line_ids:
amount = expense.total_amount_company
total_amount += amount
# Debit: expense account
move_line_vals.append(Command.create({
"name": expense.name or _("Expense: %s", sheet.name),
"account_id": expense.account_id.id,
"debit": amount if amount > 0 else 0.0,
"credit": -amount if amount < 0 else 0.0,
"partner_id": sheet.employee_id.work_contact_id.id
if sheet.employee_id.work_contact_id else False,
"analytic_distribution": expense.analytic_distribution or False,
}))
# Credit: payable account (employee)
payable_account = (
sheet.employee_id.work_contact_id.property_account_payable_id
if sheet.employee_id.work_contact_id
else self.env["account.account"].search(
[
("company_id", "=", sheet.company_id.id),
("account_type", "=", "liability_payable"),
],
limit=1,
)
)
if not payable_account:
raise UserError(
_("No payable account found for employee %s.",
sheet.employee_id.name)
)
move_line_vals.append(Command.create({
"name": _("Payable: %s", sheet.name),
"account_id": payable_account.id,
"debit": -total_amount if total_amount < 0 else 0.0,
"credit": total_amount if total_amount > 0 else 0.0,
"partner_id": sheet.employee_id.work_contact_id.id
if sheet.employee_id.work_contact_id else False,
}))
move = self.create({
"journal_id": journal.id,
"date": fields.Date.context_today(self),
"ref": _("Expense Report: %s", sheet.name),
"fusion_expense_sheet_id": sheet.id,
"move_type": "entry",
"line_ids": move_line_vals,
})
_logger.info(
"Fusion Expense Bridge: created journal entry %s (id=%d) "
"from expense sheet '%s' (id=%d).",
move.name, move.id, sheet.name, sheet.id,
)
return move
# ═══════════════════════════════════════════════════════════════════════════
# Helpdesk Bridge
# ═══════════════════════════════════════════════════════════════════════════
class FusionHelpdeskBridge(models.Model):
"""Extends journal entries with helpdesk-ticket linkage and provides
an action to create a credit note directly from a ticket.
When *helpdesk* is **not** installed the fields remain on the model
but are functionally inert, and the UI hides the button.
"""
_name = "account.move"
_inherit = "account.move"
# ---- Fields ----
fusion_helpdesk_ticket_id = fields.Many2one(
comodel_name="helpdesk.ticket",
string="Helpdesk Ticket",
index="btree_not_null",
ondelete="set null",
copy=False,
help=(
"The helpdesk ticket associated with this credit note. "
"Set automatically when a credit note is created from a ticket."
),
)
fusion_helpdesk_ticket_ref = fields.Char(
related="fusion_helpdesk_ticket_id.name",
string="Ticket Reference",
readonly=True,
store=False,
)
# ---- Helpers ----
def _fusion_is_helpdesk_installed(self):
"""Runtime check: is the *helpdesk.ticket* model registered?"""
return "helpdesk.ticket" in self.env
# ---- Actions ----
def action_open_helpdesk_ticket(self):
"""Navigate to the linked helpdesk ticket form."""
self.ensure_one()
if not self.fusion_helpdesk_ticket_id:
raise UserError(_("No helpdesk ticket is linked to this entry."))
return {
"type": "ir.actions.act_window",
"res_model": "helpdesk.ticket",
"res_id": self.fusion_helpdesk_ticket_id.id,
"view_mode": "form",
"target": "current",
}
@api.model
def action_create_credit_note_from_ticket(self, ticket_id, invoice_id=None):
"""Create a credit note linked to a helpdesk ticket.
If *invoice_id* is provided the credit note reverses that specific
invoice. Otherwise a standalone credit note is created with the
ticket's partner and a reference back to the ticket.
:param int ticket_id: id of the ``helpdesk.ticket`` record.
:param int|None invoice_id: optional id of the invoice to reverse.
:returns: window action pointing to the new credit note form.
:raises UserError: if the helpdesk module is not installed.
"""
if not self._fusion_is_helpdesk_installed():
raise UserError(
_("The Helpdesk module is not installed. "
"Please install it before creating credit notes from tickets.")
)
Ticket = self.env["helpdesk.ticket"]
ticket = Ticket.browse(ticket_id)
if not ticket.exists():
raise UserError(_("Helpdesk ticket #%d does not exist.", ticket_id))
partner = ticket.partner_id
if not partner:
raise UserError(
_("Ticket '%s' has no customer set. A customer is required "
"to create a credit note.", ticket.name)
)
# ---- Path A: reverse an existing invoice ----
if invoice_id:
invoice = self.browse(invoice_id)
if not invoice.exists():
raise UserError(_("Invoice #%d does not exist.", invoice_id))
if invoice.move_type not in ("out_invoice", "out_receipt"):
raise UserError(
_("Only customer invoices can be reversed from a "
"helpdesk ticket.")
)
# Use the standard reversal wizard logic.
reversal_vals = {
"journal_id": invoice.journal_id.id,
"date": fields.Date.context_today(self),
"reason": _("Credit note from ticket: %s", ticket.name),
}
credit_note = invoice._reverse_moves(
default_values_list=[reversal_vals],
cancel=False,
)
credit_note.write({
"fusion_helpdesk_ticket_id": ticket.id,
"ref": _("Ticket: %s", ticket.name),
})
# ---- Path B: create a blank credit note ----
else:
journal = self.env["account.journal"].search(
[
("company_id", "=", ticket.company_id.id or self.env.company.id),
("type", "=", "sale"),
],
limit=1,
)
if not journal:
raise UserError(
_("No sales journal found. Please configure one before "
"creating credit notes.")
)
credit_note = self.create({
"move_type": "out_refund",
"journal_id": journal.id,
"partner_id": partner.id,
"date": fields.Date.context_today(self),
"ref": _("Ticket: %s", ticket.name),
"narration": _(
"Credit note generated from helpdesk ticket "
"'%s' (ID %d).", ticket.name, ticket.id,
),
"fusion_helpdesk_ticket_id": ticket.id,
})
_logger.info(
"Fusion Helpdesk Bridge: created credit note %s (id=%d) "
"from ticket '%s' (id=%d).",
credit_note.name, credit_note.id, ticket.name, ticket.id,
)
return {
"type": "ir.actions.act_window",
"res_model": "account.move",
"res_id": credit_note.id,
"view_mode": "form",
"target": "current",
}