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,411 @@
"""
Fusion Accounting - Intrastat Reporting
Implements the EU Intrastat statistical declaration for intra-Community
trade in goods. The module introduces:
* ``fusion.intrastat.code`` the Combined Nomenclature (CN8) commodity
code table that products are classified against.
* ``fusion.intrastat.report`` a transient wizard that aggregates
invoice data for a given period and produces declaration-ready output
grouped by commodity code, partner country, and transaction type.
Products gain ``intrastat_code_id``, ``origin_country_id`` and
``weight`` fields; invoice lines gain ``intrastat_transaction_code``.
Reference: Regulation (EC) No 638/2004, Commission Regulation (EC)
No 1982/2004.
Original implementation by Nexa Systems Inc.
"""
import logging
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
_log = logging.getLogger(__name__)
# Standard Intrastat transaction nature codes (1-digit, most common)
INTRASTAT_TRANSACTION_CODES = [
("1", "1 - Purchase / Sale"),
("2", "2 - Return"),
("3", "3 - Trade without payment"),
("4", "4 - Processing under contract"),
("5", "5 - After processing under contract"),
("6", "6 - Repairs"),
("7", "7 - Military operations"),
("8", "8 - Construction materials"),
("9", "9 - Other transactions"),
]
# ======================================================================
# Intrastat Commodity Code
# ======================================================================
class FusionIntrastatCode(models.Model):
"""
Combined Nomenclature (CN8) commodity code for Intrastat reporting.
Each record represents a single 8-digit code from the European
Commission's Combined Nomenclature. Products reference these codes
to determine which statistical heading they fall under when goods
cross EU internal borders.
"""
_name = "fusion.intrastat.code"
_description = "Intrastat Commodity Code"
_order = "code"
_rec_name = "display_name"
code = fields.Char(
string="CN8 Code",
required=True,
size=8,
help="8-digit Combined Nomenclature code (e.g. 84713000).",
)
name = fields.Char(
string="Description",
required=True,
translate=True,
help="Human-readable description of the commodity group.",
)
active = fields.Boolean(
string="Active",
default=True,
)
supplementary_unit = fields.Char(
string="Supplementary Unit",
help=(
"Unit of measurement required for this heading in addition "
"to net mass (e.g. 'p/st' for pieces, 'l' for litres)."
),
)
_sql_constraints = [
(
"unique_code",
"UNIQUE(code)",
"The Intrastat commodity code must be unique.",
),
]
@api.depends("code", "name")
def _compute_display_name(self):
for record in self:
record.display_name = f"[{record.code}] {record.name}" if record.code else record.name or ""
# ======================================================================
# Mixin for product fields
# ======================================================================
class FusionProductIntrastat(models.Model):
"""
Extends ``product.template`` with Intrastat-specific fields.
These fields are used by the Intrastat report wizard to determine
the commodity code, country of origin, and net weight of goods.
"""
_inherit = "product.template"
intrastat_code_id = fields.Many2one(
comodel_name="fusion.intrastat.code",
string="Intrastat Code",
help="Combined Nomenclature (CN8) commodity code for this product.",
)
origin_country_id = fields.Many2one(
comodel_name="res.country",
string="Country of Origin",
help="Country where the goods were produced or manufactured.",
)
intrastat_weight = fields.Float(
string="Net Weight (kg)",
digits="Stock Weight",
help="Net weight in kilograms for Intrastat reporting.",
)
# ======================================================================
# Mixin for invoice-line fields
# ======================================================================
class FusionMoveLineIntrastat(models.Model):
"""
Extends ``account.move.line`` with an Intrastat transaction code.
The transaction code indicates the nature of the transaction
(purchase, return, processing, etc.) and is required in each
Intrastat declaration line.
"""
_inherit = "account.move.line"
intrastat_transaction_code = fields.Selection(
selection=INTRASTAT_TRANSACTION_CODES,
string="Intrastat Transaction",
help="Nature of the transaction for Intrastat reporting.",
)
# ======================================================================
# Intrastat Report Wizard
# ======================================================================
class FusionIntrastatReport(models.TransientModel):
"""
Wizard that aggregates invoice data into Intrastat declaration lines.
The user selects a period and flow direction (arrivals or
dispatches), then the wizard queries posted invoices that involve
intra-EU partners and products with an Intrastat commodity code.
Results are grouped by commodity code, partner country, and
transaction nature code to produce aggregated statistical values
(value, net mass, supplementary quantity).
"""
_name = "fusion.intrastat.report"
_description = "Intrastat Report Wizard"
# ------------------------------------------------------------------
# Fields
# ------------------------------------------------------------------
date_from = fields.Date(
string="From",
required=True,
help="Start of the reporting period (inclusive).",
)
date_to = fields.Date(
string="To",
required=True,
help="End of the reporting period (inclusive).",
)
company_id = fields.Many2one(
comodel_name="res.company",
string="Company",
required=True,
default=lambda self: self.env.company,
)
flow_type = fields.Selection(
selection=[
("arrival", "Arrivals (Purchases)"),
("dispatch", "Dispatches (Sales)"),
],
string="Flow",
required=True,
default="dispatch",
help="Direction of goods movement across EU borders.",
)
line_ids = fields.One2many(
comodel_name="fusion.intrastat.report.line",
inverse_name="report_id",
string="Declaration Lines",
readonly=True,
)
state = fields.Selection(
selection=[
("draft", "Draft"),
("done", "Computed"),
],
string="Status",
default="draft",
readonly=True,
)
# ------------------------------------------------------------------
# Validation
# ------------------------------------------------------------------
@api.constrains("date_from", "date_to")
def _check_date_range(self):
for record in self:
if record.date_from and record.date_to and record.date_to < record.date_from:
raise ValidationError(
_("The end date must not precede the start date.")
)
# ------------------------------------------------------------------
# Computation
# ------------------------------------------------------------------
def action_compute(self):
"""Aggregate invoice lines into Intrastat declaration rows.
Grouping key: ``(commodity_code, partner_country, transaction_code)``
For each group the wizard sums:
* ``total_value`` invoice line amounts in company currency
* ``total_weight`` product net weight × quantity
* ``supplementary_qty`` quantity in supplementary units (if
the commodity code requires it)
"""
self.ensure_one()
# Clear previous results
self.line_ids.unlink()
invoice_lines = self._get_eligible_lines()
if not invoice_lines:
raise UserError(
_("No eligible invoice lines found for the selected period and flow type.")
)
aggregation = {} # (code_id, country_id, txn_code) -> dict
for line in invoice_lines:
product = line.product_id.product_tmpl_id
code = product.intrastat_code_id
if not code:
continue
partner = line.partner_id
country = partner.country_id
if not country:
continue
txn_code = line.intrastat_transaction_code or "1"
key = (code.id, country.id, txn_code)
bucket = aggregation.setdefault(key, {
"intrastat_code_id": code.id,
"country_id": country.id,
"transaction_code": txn_code,
"total_value": 0.0,
"total_weight": 0.0,
"supplementary_qty": 0.0,
})
qty = abs(line.quantity)
unit_weight = product.intrastat_weight or 0.0
# Use price_subtotal as the statistical value
bucket["total_value"] += abs(line.price_subtotal)
bucket["total_weight"] += unit_weight * qty
bucket["supplementary_qty"] += qty
# Create declaration lines
ReportLine = self.env["fusion.intrastat.report.line"]
for vals in aggregation.values():
vals["report_id"] = self.id
ReportLine.create(vals)
self.write({"state": "done"})
return self._reopen_wizard()
def _get_eligible_lines(self):
"""Return ``account.move.line`` records for invoices that
qualify for Intrastat reporting.
Eligibility criteria:
* Invoice is posted
* Invoice date falls within the period
* Partner country is in the EU and differs from the company country
* Product has an Intrastat commodity code assigned
* Move type matches the selected flow direction
"""
company_country = self.company_id.country_id
if self.flow_type == "dispatch":
move_types = ("out_invoice", "out_refund")
else:
move_types = ("in_invoice", "in_refund")
lines = self.env["account.move.line"].search([
("company_id", "=", self.company_id.id),
("parent_state", "=", "posted"),
("date", ">=", self.date_from),
("date", "<=", self.date_to),
("move_id.move_type", "in", move_types),
("product_id", "!=", False),
("product_id.product_tmpl_id.intrastat_code_id", "!=", False),
("partner_id.country_id", "!=", False),
("partner_id.country_id", "!=", company_country.id),
])
# Filter to EU countries only
eu_countries = self._get_eu_country_ids()
if eu_countries:
lines = lines.filtered(
lambda l: l.partner_id.country_id.id in eu_countries
)
return lines
def _get_eu_country_ids(self):
"""Return a set of ``res.country`` IDs for current EU member states."""
eu_group = self.env.ref("base.europe", raise_if_not_found=False)
if eu_group:
return set(eu_group.country_ids.ids)
return set()
def _reopen_wizard(self):
"""Return an action that re-opens this wizard form."""
return {
"type": "ir.actions.act_window",
"res_model": self._name,
"res_id": self.id,
"view_mode": "form",
"target": "new",
}
# ======================================================================
# Intrastat Report Line
# ======================================================================
class FusionIntrastatReportLine(models.TransientModel):
"""
One aggregated row in an Intrastat declaration.
Each line represents a unique combination of commodity code, partner
country, and transaction nature code. Statistical values (amount,
weight, supplementary quantity) are totals across all qualifying
invoice lines that match the grouping key.
"""
_name = "fusion.intrastat.report.line"
_description = "Intrastat Report Line"
_order = "intrastat_code_id, country_id"
report_id = fields.Many2one(
comodel_name="fusion.intrastat.report",
string="Report",
required=True,
ondelete="cascade",
)
intrastat_code_id = fields.Many2one(
comodel_name="fusion.intrastat.code",
string="Commodity Code",
required=True,
readonly=True,
)
country_id = fields.Many2one(
comodel_name="res.country",
string="Partner Country",
required=True,
readonly=True,
)
transaction_code = fields.Selection(
selection=INTRASTAT_TRANSACTION_CODES,
string="Transaction Type",
readonly=True,
)
total_value = fields.Float(
string="Statistical Value",
digits="Account",
readonly=True,
help="Total invoice value in company currency.",
)
total_weight = fields.Float(
string="Net Mass (kg)",
digits="Stock Weight",
readonly=True,
help="Total net weight in kilograms.",
)
supplementary_qty = fields.Float(
string="Supplementary Qty",
digits="Product Unit of Measure",
readonly=True,
help="Total quantity in supplementary units (if applicable).",
)