Initial commit
This commit is contained in:
428
Fusion Accounting/models/integration_bridges.py
Normal file
428
Fusion Accounting/models/integration_bridges.py
Normal 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",
|
||||
}
|
||||
Reference in New Issue
Block a user