mirror of
https://github.com/OCA/intrastat-extrastat.git
synced 2025-02-16 17:13:41 +02:00
[IMP] intrastat_product: add support for Brexit + small performance improvement + remove universal hs_code constaint
Add support for post-Brexit VAT numbers with latest python-stdnum version Remove use of country_id.intrastat Add 'vat' field on computation and declaration lines with a simple validity check Add 'partner_id' field on computation lines Prevent back to draft when an XML export is present Remove the universal constaint applied to the hs_codes for countries in the "Europe" list, as discussed in https://github.com/OCA/intrastat-extrastat/pull/116#discussion_r569735555 Co-authored-by: Alexis de Lattre <alexis.delattre@akretion.com>
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
"report_xlsx_helper",
|
||||
],
|
||||
"excludes": ["account_intrastat"],
|
||||
"external_dependencies": {"python": ["python-stdnum>=1.16"]},
|
||||
"data": [
|
||||
"security/intrastat_security.xml",
|
||||
"security/ir.model.access.csv",
|
||||
|
||||
@@ -25,15 +25,10 @@ class AccountMove(models.Model):
|
||||
src_dest_country_id = fields.Many2one(
|
||||
comodel_name="res.country",
|
||||
string="Origin/Destination Country",
|
||||
compute="_compute_intrastat_country",
|
||||
compute="_compute_src_dest_country_id",
|
||||
store=True,
|
||||
help="Destination country for dispatches. Origin country for " "arrivals.",
|
||||
)
|
||||
intrastat_country = fields.Boolean(
|
||||
compute="_compute_intrastat_country",
|
||||
string="Intrastat Country",
|
||||
store=True,
|
||||
)
|
||||
src_dest_region_id = fields.Many2one(
|
||||
comodel_name="intrastat.region",
|
||||
string="Origin/Destination Region",
|
||||
@@ -52,13 +47,12 @@ class AccountMove(models.Model):
|
||||
)
|
||||
|
||||
@api.depends("partner_shipping_id.country_id", "partner_id.country_id")
|
||||
def _compute_intrastat_country(self):
|
||||
def _compute_src_dest_country_id(self):
|
||||
for inv in self:
|
||||
country = inv.partner_shipping_id.country_id or inv.partner_id.country_id
|
||||
if not country:
|
||||
country = inv.company_id.country_id
|
||||
inv.src_dest_country_id = country.id
|
||||
inv.intrastat_country = country.intrastat
|
||||
|
||||
@api.model
|
||||
def _default_src_dest_region_id(self):
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# @author Luc de Meyer <info@noviat.com>
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HSCode(models.Model):
|
||||
@@ -13,24 +12,3 @@ class HSCode(models.Model):
|
||||
intrastat_unit_id = fields.Many2one(
|
||||
comodel_name="intrastat.unit", string="Intrastat Supplementary Unit"
|
||||
)
|
||||
|
||||
@api.constrains("local_code")
|
||||
def _hs_code(self):
|
||||
if self.company_id.country_id.intrastat:
|
||||
if not self.local_code.isdigit():
|
||||
raise ValidationError(
|
||||
_(
|
||||
"Intrastat Codes should only contain digits. "
|
||||
"This is not the case for code '%s'."
|
||||
)
|
||||
% self.local_code
|
||||
)
|
||||
if len(self.local_code) != 8:
|
||||
raise ValidationError(
|
||||
_(
|
||||
"Intrastat Codes should "
|
||||
"contain 8 digits. This is not the case for "
|
||||
"Intrastat Code '%s' which has %d digits."
|
||||
)
|
||||
% (self.local_code, len(self.local_code))
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import logging
|
||||
from datetime import date
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from stdnum.vatin import is_valid
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import RedirectWarning, UserError, ValidationError
|
||||
@@ -241,15 +242,71 @@ class IntrastatProductDeclaration(models.Model):
|
||||
msg, action.id, _("Go to Accounting Configuration Settings screen")
|
||||
)
|
||||
|
||||
def _get_partner_country(self, inv_line, notedict):
|
||||
country = (
|
||||
inv_line.move_id.src_dest_country_id
|
||||
or inv_line.move_id.partner_id.country_id
|
||||
)
|
||||
if not country.intrastat:
|
||||
country = False
|
||||
elif country == self.company_id.country_id:
|
||||
country = False
|
||||
def _get_partner_country(self, inv_line, notedict, eu_countries):
|
||||
inv = inv_line.move_id
|
||||
country = inv.src_dest_country_id or inv.partner_id.country_id
|
||||
if not country:
|
||||
line_notes = [
|
||||
_(
|
||||
"Missing country on invoice partner '%s' "
|
||||
"or on the delivery address (partner '%s'). "
|
||||
)
|
||||
% (
|
||||
inv.partner_id.display_name,
|
||||
inv.partner_shipping_id
|
||||
and inv.partner_shipping_id.display_name
|
||||
or "-",
|
||||
)
|
||||
]
|
||||
self._format_line_note(inv_line, notedict, line_notes)
|
||||
else:
|
||||
if country not in eu_countries and country.code != "GB":
|
||||
line_notes = [
|
||||
_(
|
||||
"On invoice '%s', the source/destination country "
|
||||
"is '%s' which is not part of the European Union."
|
||||
)
|
||||
% (inv.name, country.name)
|
||||
]
|
||||
self._format_line_note(inv_line, notedict, line_notes)
|
||||
if country and country.code == "GB" and self.year >= "2021":
|
||||
vat = inv.commercial_partner_id.vat
|
||||
if not vat:
|
||||
line_notes = [
|
||||
_(
|
||||
"On invoice '%s', the source/destination country "
|
||||
"is United-Kingdom and the fiscal position is '%s'. "
|
||||
"Make sure that the fiscal position is right. If "
|
||||
"the origin/destination is Northern Ireland, please "
|
||||
"set the VAT number of the partner '%s' in Odoo with "
|
||||
"its new VAT number starting with 'XI' following Brexit."
|
||||
)
|
||||
% (
|
||||
inv.name,
|
||||
inv.fiscal_position_id.display_name,
|
||||
inv.commercial_partner_id.display_name,
|
||||
)
|
||||
]
|
||||
self._format_line_note(inv_line, notedict, line_notes)
|
||||
elif not vat.startswith("XI"):
|
||||
line_notes = [
|
||||
_(
|
||||
"On invoice '%s', the source/destination country "
|
||||
"is United-Kingdom, the fiscal position is '%s' and "
|
||||
"the partner's VAT number is '%s'. "
|
||||
"Make sure that the fiscal position is right. If "
|
||||
"the origin/destination is Northern Ireland, please "
|
||||
"update the VAT number of the partner '%s' in Odoo with "
|
||||
"its new VAT number starting with 'XI' following Brexit."
|
||||
)
|
||||
% (
|
||||
inv.name,
|
||||
inv.fiscal_position_id.display_name,
|
||||
vat,
|
||||
inv.commercial_partner_id.display_name,
|
||||
)
|
||||
]
|
||||
self._format_line_note(inv_line, notedict, line_notes)
|
||||
return country
|
||||
|
||||
def _get_intrastat_transaction(self, inv_line, notedict):
|
||||
@@ -338,7 +395,7 @@ class IntrastatProductDeclaration(models.Model):
|
||||
)
|
||||
% (source_uom.name, product.display_name)
|
||||
]
|
||||
self._note += self._format_line_note(inv_line, notedict, line_notes)
|
||||
self._format_line_note(inv_line, notedict, line_notes)
|
||||
return weight, suppl_unit_qty
|
||||
|
||||
return weight, suppl_unit_qty
|
||||
@@ -421,6 +478,39 @@ class IntrastatProductDeclaration(models.Model):
|
||||
def _get_product_origin_country(self, inv_line, notedict):
|
||||
return inv_line.product_id.origin_country_id
|
||||
|
||||
def _get_vat(self, inv_line, notedict):
|
||||
vat = False
|
||||
inv = inv_line.move_id
|
||||
if self.declaration_type == "dispatches":
|
||||
vat = inv.commercial_partner_id.vat
|
||||
if vat:
|
||||
if vat.startswith("GB"):
|
||||
line_notes = [
|
||||
_(
|
||||
"VAT number of partner '%s' is '%s'. If this partner "
|
||||
"is from Northern Ireland, his VAT number should be "
|
||||
"updated to his new VAT number starting with 'XI' "
|
||||
"following Brexit. If this partner is from Great Britain, "
|
||||
"maybe the fiscal position was wrong on invoice '%s' "
|
||||
"(the fiscal position was '%s')."
|
||||
)
|
||||
% (
|
||||
inv.commercial_partner_id.display_name,
|
||||
vat,
|
||||
inv.name,
|
||||
inv.fiscal_position_id.display_name,
|
||||
)
|
||||
]
|
||||
self._format_line_note(inv_line, notedict, line_notes)
|
||||
|
||||
else:
|
||||
line_notes = [
|
||||
_("Missing VAT Number on partner '%s'")
|
||||
% inv.commercial_partner_id.display_name
|
||||
]
|
||||
self._format_line_note(inv_line, notedict, line_notes)
|
||||
return vat
|
||||
|
||||
def _update_computation_line_vals(self, inv_line, line_vals, notedict):
|
||||
""" placeholder for localization modules """
|
||||
|
||||
@@ -516,6 +606,7 @@ class IntrastatProductDeclaration(models.Model):
|
||||
"Product Unit of Measure"
|
||||
)
|
||||
accessory_costs = self.company_id.intrastat_accessory_costs
|
||||
eu_countries = self.env.ref("base.europe").country_ids
|
||||
|
||||
self._gather_invoices_init(notedict)
|
||||
domain = self._prepare_invoice_domain()
|
||||
@@ -559,22 +650,9 @@ class IntrastatProductDeclaration(models.Model):
|
||||
)
|
||||
continue
|
||||
|
||||
partner_country = self._get_partner_country(inv_line, notedict)
|
||||
if not partner_country:
|
||||
line_notes = [
|
||||
_(
|
||||
"Missing country on invoice partner '%s' "
|
||||
"or on the delivery address (partner '%s'). "
|
||||
)
|
||||
% (
|
||||
invoice.partner_id.display_name,
|
||||
invoice.partner_shipping_id
|
||||
and invoice.partner_shipping_id.display_name
|
||||
or "-",
|
||||
)
|
||||
]
|
||||
self._format_line_note(inv_line, notedict, line_notes)
|
||||
continue
|
||||
partner_country = self._get_partner_country(
|
||||
inv_line, notedict, eu_countries
|
||||
)
|
||||
|
||||
if inv_intrastat_line:
|
||||
hs_code = inv_intrastat_line.hs_code_id
|
||||
@@ -622,6 +700,8 @@ class IntrastatProductDeclaration(models.Model):
|
||||
|
||||
region = self._get_region(inv_line, notedict)
|
||||
|
||||
vat = self._get_vat(inv_line, notedict)
|
||||
|
||||
line_vals = {
|
||||
"parent_id": self.id,
|
||||
"invoice_line_id": inv_line.id,
|
||||
@@ -635,6 +715,7 @@ class IntrastatProductDeclaration(models.Model):
|
||||
"transaction_id": intrastat_transaction.id,
|
||||
"product_origin_country_id": product_origin_country.id or False,
|
||||
"region_id": region and region.id or False,
|
||||
"vat": vat,
|
||||
}
|
||||
|
||||
# extended declaration
|
||||
@@ -686,7 +767,6 @@ class IntrastatProductDeclaration(models.Model):
|
||||
def action_gather(self):
|
||||
self.ensure_one()
|
||||
self.message_post(body=_("Generate Lines from Invoices"))
|
||||
self._check_generate_lines()
|
||||
notedict = {
|
||||
"note": "",
|
||||
"line_nbr": 0,
|
||||
@@ -739,6 +819,7 @@ class IntrastatProductDeclaration(models.Model):
|
||||
"region": computation_line.region_id.id or False,
|
||||
"product_origin_country": computation_line.product_origin_country_id.id
|
||||
or False,
|
||||
"vat": computation_line.vat or False,
|
||||
}
|
||||
|
||||
def group_line_hashcode(self, computation_line):
|
||||
@@ -758,6 +839,7 @@ class IntrastatProductDeclaration(models.Model):
|
||||
"parent_id": computation_line.parent_id.id,
|
||||
"product_origin_country_id": computation_line.product_origin_country_id.id,
|
||||
"amount_company_currency": 0.0,
|
||||
"vat": computation_line.vat,
|
||||
}
|
||||
for field in fields_to_sum:
|
||||
vals[field] = 0.0
|
||||
@@ -867,6 +949,8 @@ class IntrastatProductDeclaration(models.Model):
|
||||
"suppl_unit_qty",
|
||||
"suppl_unit",
|
||||
"transport",
|
||||
"vat",
|
||||
"partner_id",
|
||||
"invoice",
|
||||
]
|
||||
|
||||
@@ -884,6 +968,7 @@ class IntrastatProductDeclaration(models.Model):
|
||||
"suppl_unit_qty",
|
||||
"suppl_unit",
|
||||
"transport",
|
||||
"vat",
|
||||
]
|
||||
|
||||
@api.model
|
||||
@@ -898,6 +983,11 @@ class IntrastatProductDeclaration(models.Model):
|
||||
self.write({"state": "done"})
|
||||
|
||||
def back2draft(self):
|
||||
for decl in self:
|
||||
if decl.xml_attachment_id:
|
||||
raise UserError(
|
||||
_("Before going back to draft, you must delete the XML export.")
|
||||
)
|
||||
self.write({"state": "draft"})
|
||||
|
||||
|
||||
@@ -924,6 +1014,9 @@ class IntrastatProductComputationLine(models.Model):
|
||||
invoice_id = fields.Many2one(
|
||||
"account.move", related="invoice_line_id.move_id", string="Invoice"
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
related="invoice_line_id.move_id.commercial_partner_id", string="Partner"
|
||||
)
|
||||
declaration_line_id = fields.Many2one(
|
||||
"intrastat.product.declaration.line", string="Declaration Line", readonly=True
|
||||
)
|
||||
@@ -978,6 +1071,7 @@ class IntrastatProductComputationLine(models.Model):
|
||||
string="Country of Origin of the Product",
|
||||
help="Country of origin of the product i.e. product 'made in ____'",
|
||||
)
|
||||
vat = fields.Char(string="VAT Number")
|
||||
|
||||
@api.depends("transport_id")
|
||||
def _compute_check_validity(self):
|
||||
@@ -985,6 +1079,12 @@ class IntrastatProductComputationLine(models.Model):
|
||||
for this in self:
|
||||
this.valid = True
|
||||
|
||||
@api.constrains("vat")
|
||||
def _check_vat(self):
|
||||
for this in self:
|
||||
if this.vat and not is_valid(this.vat):
|
||||
raise ValidationError(_("The VAT number '%s' is invalid.") % this.vat)
|
||||
|
||||
# TODO: product_id is a readonly related field 'invoice_line_id.product_id'
|
||||
# so the onchange is non-sense. Either we convert product_id to a regular
|
||||
# field or we keep it a related field and we remove this onchange
|
||||
@@ -1027,7 +1127,6 @@ class IntrastatProductDeclarationLine(models.Model):
|
||||
"res.country",
|
||||
string="Country",
|
||||
help="Country of Origin/Destination",
|
||||
domain=[("intrastat", "=", True)],
|
||||
)
|
||||
hs_code_id = fields.Many2one("hs.code", string="Intrastat Code")
|
||||
intrastat_unit_id = fields.Many2one(
|
||||
@@ -1058,3 +1157,10 @@ class IntrastatProductDeclarationLine(models.Model):
|
||||
string="Country of Origin of the Product",
|
||||
help="Country of origin of the product i.e. product 'made in ____'",
|
||||
)
|
||||
vat = fields.Char(string="VAT Number")
|
||||
|
||||
@api.constrains("vat")
|
||||
def _check_vat(self):
|
||||
for this in self:
|
||||
if this.vat and not is_valid(this.vat):
|
||||
raise ValidationError(_("The VAT number '%s' is invalid.") % this.vat)
|
||||
|
||||
@@ -145,6 +145,16 @@ class IntrastatProductDeclarationXlsx(models.AbstractModel):
|
||||
"line": {"value": self._render("line.region_id.name or ''")},
|
||||
"width": 28,
|
||||
},
|
||||
"vat": {
|
||||
"header": {"type": "string", "value": self._("VAT")},
|
||||
"line": {"value": self._render("line.vat or ''")},
|
||||
"width": 20,
|
||||
},
|
||||
"partner_id": {
|
||||
"header": {"type": "string", "value": self._("Partner")},
|
||||
"line": {"value": self._render("line.partner_id.display_name or ''")},
|
||||
"width": 28,
|
||||
},
|
||||
"invoice": {
|
||||
"header": {"type": "string", "value": self._("Invoice")},
|
||||
"line": {"value": self._render("line.invoice_id.name")},
|
||||
|
||||
@@ -224,10 +224,7 @@
|
||||
/>
|
||||
<field name="product_id" />
|
||||
<field name="hs_code_id" />
|
||||
<field
|
||||
name="src_dest_country_id"
|
||||
domain="[('intrastat', '=', True)]"
|
||||
/>
|
||||
<field name="src_dest_country_id" />
|
||||
<field
|
||||
name="amount_company_currency"
|
||||
widget="monetary"
|
||||
@@ -255,6 +252,8 @@
|
||||
<field name="incoterm_id" invisible="1" />
|
||||
<field name="region_id" invisible="1" />
|
||||
<field name="product_origin_country_id" invisible="1" />
|
||||
<field name="vat" />
|
||||
<field name="partner_id" />
|
||||
<field name="invoice_id" />
|
||||
</group>
|
||||
<group string="Declaration" name="declaration">
|
||||
@@ -274,7 +273,7 @@
|
||||
/>
|
||||
<field name="product_id" />
|
||||
<field name="hs_code_id" />
|
||||
<field name="src_dest_country_id" domain="[('intrastat', '=', True)]" />
|
||||
<field name="src_dest_country_id" />
|
||||
<field name="amount_company_currency" />
|
||||
<field name="amount_accessory_cost_company_currency" />
|
||||
<field name="transaction_id" />
|
||||
@@ -294,6 +293,7 @@
|
||||
invisible="1"
|
||||
string="Product C/O"
|
||||
/>
|
||||
<field name="vat" />
|
||||
<field name="invoice_id" />
|
||||
<field name="declaration_type" invisible="1" />
|
||||
<field name="reporting_level" invisible="1" />
|
||||
@@ -311,10 +311,7 @@
|
||||
invisible="not context.get('intrastat_product_declaration_line_main_view')"
|
||||
/>
|
||||
<field name="hs_code_id" />
|
||||
<field
|
||||
name="src_dest_country_id"
|
||||
domain="[('intrastat', '=', True)]"
|
||||
/>
|
||||
<field name="src_dest_country_id" />
|
||||
<field
|
||||
name="amount_company_currency"
|
||||
widget="monetary"
|
||||
@@ -337,6 +334,7 @@
|
||||
<field name="region_id" invisible="1" />
|
||||
<field name="incoterm_id" invisible="1" />
|
||||
<field name="product_origin_country_id" invisible="1" />
|
||||
<field name="vat" />
|
||||
</group>
|
||||
<group name="computation" string="Related Transactions">
|
||||
<field name="computation_line_ids" nolabel="1" />
|
||||
@@ -354,7 +352,7 @@
|
||||
invisible="not context.get('intrastat_product_declaration_line_main_view')"
|
||||
/>
|
||||
<field name="hs_code_id" />
|
||||
<field name="src_dest_country_id" domain="[('intrastat', '=', True)]" />
|
||||
<field name="src_dest_country_id" />
|
||||
<field name="amount_company_currency" />
|
||||
<field name="transaction_id" />
|
||||
<field name="weight" />
|
||||
@@ -373,6 +371,7 @@
|
||||
invisible="1"
|
||||
string="Product C/O"
|
||||
/>
|
||||
<field name="vat" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
# generated from manifests external_dependencies
|
||||
python-stdnum>=1.16
|
||||
Reference in New Issue
Block a user