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