[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:
João Marques
2021-02-12 12:29:09 +00:00
parent 2d290b6700
commit 2646dc489e
7 changed files with 159 additions and 69 deletions

View File

@@ -21,6 +21,7 @@
"report_xlsx_helper", "report_xlsx_helper",
], ],
"excludes": ["account_intrastat"], "excludes": ["account_intrastat"],
"external_dependencies": {"python": ["python-stdnum>=1.16"]},
"data": [ "data": [
"security/intrastat_security.xml", "security/intrastat_security.xml",
"security/ir.model.access.csv", "security/ir.model.access.csv",

View File

@@ -25,15 +25,10 @@ class AccountMove(models.Model):
src_dest_country_id = fields.Many2one( src_dest_country_id = fields.Many2one(
comodel_name="res.country", comodel_name="res.country",
string="Origin/Destination Country", string="Origin/Destination Country",
compute="_compute_intrastat_country", compute="_compute_src_dest_country_id",
store=True, store=True,
help="Destination country for dispatches. Origin country for " "arrivals.", 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( src_dest_region_id = fields.Many2one(
comodel_name="intrastat.region", comodel_name="intrastat.region",
string="Origin/Destination Region", string="Origin/Destination Region",
@@ -52,13 +47,12 @@ class AccountMove(models.Model):
) )
@api.depends("partner_shipping_id.country_id", "partner_id.country_id") @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: for inv in self:
country = inv.partner_shipping_id.country_id or inv.partner_id.country_id country = inv.partner_shipping_id.country_id or inv.partner_id.country_id
if not country: if not country:
country = inv.company_id.country_id country = inv.company_id.country_id
inv.src_dest_country_id = country.id inv.src_dest_country_id = country.id
inv.intrastat_country = country.intrastat
@api.model @api.model
def _default_src_dest_region_id(self): def _default_src_dest_region_id(self):

View File

@@ -3,8 +3,7 @@
# @author Alexis de Lattre <alexis.delattre@akretion.com> # @author Alexis de Lattre <alexis.delattre@akretion.com>
# @author Luc de Meyer <info@noviat.com> # @author Luc de Meyer <info@noviat.com>
from odoo import _, api, fields, models from odoo import fields, models
from odoo.exceptions import ValidationError
class HSCode(models.Model): class HSCode(models.Model):
@@ -13,24 +12,3 @@ class HSCode(models.Model):
intrastat_unit_id = fields.Many2one( intrastat_unit_id = fields.Many2one(
comodel_name="intrastat.unit", string="Intrastat Supplementary Unit" 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))
)

View File

@@ -7,6 +7,7 @@ import logging
from datetime import date from datetime import date
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from stdnum.vatin import is_valid
from odoo import _, api, fields, models from odoo import _, api, fields, models
from odoo.exceptions import RedirectWarning, UserError, ValidationError from odoo.exceptions import RedirectWarning, UserError, ValidationError
@@ -241,15 +242,71 @@ class IntrastatProductDeclaration(models.Model):
msg, action.id, _("Go to Accounting Configuration Settings screen") msg, action.id, _("Go to Accounting Configuration Settings screen")
) )
def _get_partner_country(self, inv_line, notedict): def _get_partner_country(self, inv_line, notedict, eu_countries):
country = ( inv = inv_line.move_id
inv_line.move_id.src_dest_country_id country = inv.src_dest_country_id or inv.partner_id.country_id
or inv_line.move_id.partner_id.country_id if not country:
) line_notes = [
if not country.intrastat: _(
country = False "Missing country on invoice partner '%s' "
elif country == self.company_id.country_id: "or on the delivery address (partner '%s'). "
country = False )
% (
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 return country
def _get_intrastat_transaction(self, inv_line, notedict): def _get_intrastat_transaction(self, inv_line, notedict):
@@ -338,7 +395,7 @@ class IntrastatProductDeclaration(models.Model):
) )
% (source_uom.name, product.display_name) % (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
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): def _get_product_origin_country(self, inv_line, notedict):
return inv_line.product_id.origin_country_id 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): def _update_computation_line_vals(self, inv_line, line_vals, notedict):
""" placeholder for localization modules """ """ placeholder for localization modules """
@@ -516,6 +606,7 @@ class IntrastatProductDeclaration(models.Model):
"Product Unit of Measure" "Product Unit of Measure"
) )
accessory_costs = self.company_id.intrastat_accessory_costs accessory_costs = self.company_id.intrastat_accessory_costs
eu_countries = self.env.ref("base.europe").country_ids
self._gather_invoices_init(notedict) self._gather_invoices_init(notedict)
domain = self._prepare_invoice_domain() domain = self._prepare_invoice_domain()
@@ -559,22 +650,9 @@ class IntrastatProductDeclaration(models.Model):
) )
continue continue
partner_country = self._get_partner_country(inv_line, notedict) partner_country = self._get_partner_country(
if not partner_country: inv_line, notedict, eu_countries
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
if inv_intrastat_line: if inv_intrastat_line:
hs_code = inv_intrastat_line.hs_code_id hs_code = inv_intrastat_line.hs_code_id
@@ -622,6 +700,8 @@ class IntrastatProductDeclaration(models.Model):
region = self._get_region(inv_line, notedict) region = self._get_region(inv_line, notedict)
vat = self._get_vat(inv_line, notedict)
line_vals = { line_vals = {
"parent_id": self.id, "parent_id": self.id,
"invoice_line_id": inv_line.id, "invoice_line_id": inv_line.id,
@@ -635,6 +715,7 @@ class IntrastatProductDeclaration(models.Model):
"transaction_id": intrastat_transaction.id, "transaction_id": intrastat_transaction.id,
"product_origin_country_id": product_origin_country.id or False, "product_origin_country_id": product_origin_country.id or False,
"region_id": region and region.id or False, "region_id": region and region.id or False,
"vat": vat,
} }
# extended declaration # extended declaration
@@ -686,7 +767,6 @@ class IntrastatProductDeclaration(models.Model):
def action_gather(self): def action_gather(self):
self.ensure_one() self.ensure_one()
self.message_post(body=_("Generate Lines from Invoices")) self.message_post(body=_("Generate Lines from Invoices"))
self._check_generate_lines()
notedict = { notedict = {
"note": "", "note": "",
"line_nbr": 0, "line_nbr": 0,
@@ -739,6 +819,7 @@ class IntrastatProductDeclaration(models.Model):
"region": computation_line.region_id.id or False, "region": computation_line.region_id.id or False,
"product_origin_country": computation_line.product_origin_country_id.id "product_origin_country": computation_line.product_origin_country_id.id
or False, or False,
"vat": computation_line.vat or False,
} }
def group_line_hashcode(self, computation_line): def group_line_hashcode(self, computation_line):
@@ -758,6 +839,7 @@ class IntrastatProductDeclaration(models.Model):
"parent_id": computation_line.parent_id.id, "parent_id": computation_line.parent_id.id,
"product_origin_country_id": computation_line.product_origin_country_id.id, "product_origin_country_id": computation_line.product_origin_country_id.id,
"amount_company_currency": 0.0, "amount_company_currency": 0.0,
"vat": computation_line.vat,
} }
for field in fields_to_sum: for field in fields_to_sum:
vals[field] = 0.0 vals[field] = 0.0
@@ -867,6 +949,8 @@ class IntrastatProductDeclaration(models.Model):
"suppl_unit_qty", "suppl_unit_qty",
"suppl_unit", "suppl_unit",
"transport", "transport",
"vat",
"partner_id",
"invoice", "invoice",
] ]
@@ -884,6 +968,7 @@ class IntrastatProductDeclaration(models.Model):
"suppl_unit_qty", "suppl_unit_qty",
"suppl_unit", "suppl_unit",
"transport", "transport",
"vat",
] ]
@api.model @api.model
@@ -898,6 +983,11 @@ class IntrastatProductDeclaration(models.Model):
self.write({"state": "done"}) self.write({"state": "done"})
def back2draft(self): 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"}) self.write({"state": "draft"})
@@ -924,6 +1014,9 @@ class IntrastatProductComputationLine(models.Model):
invoice_id = fields.Many2one( invoice_id = fields.Many2one(
"account.move", related="invoice_line_id.move_id", string="Invoice" "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( declaration_line_id = fields.Many2one(
"intrastat.product.declaration.line", string="Declaration Line", readonly=True "intrastat.product.declaration.line", string="Declaration Line", readonly=True
) )
@@ -978,6 +1071,7 @@ class IntrastatProductComputationLine(models.Model):
string="Country of Origin of the Product", string="Country of Origin of the Product",
help="Country of origin of the product i.e. product 'made in ____'", help="Country of origin of the product i.e. product 'made in ____'",
) )
vat = fields.Char(string="VAT Number")
@api.depends("transport_id") @api.depends("transport_id")
def _compute_check_validity(self): def _compute_check_validity(self):
@@ -985,6 +1079,12 @@ class IntrastatProductComputationLine(models.Model):
for this in self: for this in self:
this.valid = True 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' # 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 # 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 # field or we keep it a related field and we remove this onchange
@@ -1027,7 +1127,6 @@ class IntrastatProductDeclarationLine(models.Model):
"res.country", "res.country",
string="Country", string="Country",
help="Country of Origin/Destination", help="Country of Origin/Destination",
domain=[("intrastat", "=", True)],
) )
hs_code_id = fields.Many2one("hs.code", string="Intrastat Code") hs_code_id = fields.Many2one("hs.code", string="Intrastat Code")
intrastat_unit_id = fields.Many2one( intrastat_unit_id = fields.Many2one(
@@ -1058,3 +1157,10 @@ class IntrastatProductDeclarationLine(models.Model):
string="Country of Origin of the Product", string="Country of Origin of the Product",
help="Country of origin of the product i.e. product 'made in ____'", 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)

View File

@@ -145,6 +145,16 @@ class IntrastatProductDeclarationXlsx(models.AbstractModel):
"line": {"value": self._render("line.region_id.name or ''")}, "line": {"value": self._render("line.region_id.name or ''")},
"width": 28, "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": { "invoice": {
"header": {"type": "string", "value": self._("Invoice")}, "header": {"type": "string", "value": self._("Invoice")},
"line": {"value": self._render("line.invoice_id.name")}, "line": {"value": self._render("line.invoice_id.name")},

View File

@@ -224,10 +224,7 @@
/> />
<field name="product_id" /> <field name="product_id" />
<field name="hs_code_id" /> <field name="hs_code_id" />
<field <field name="src_dest_country_id" />
name="src_dest_country_id"
domain="[('intrastat', '=', True)]"
/>
<field <field
name="amount_company_currency" name="amount_company_currency"
widget="monetary" widget="monetary"
@@ -255,6 +252,8 @@
<field name="incoterm_id" invisible="1" /> <field name="incoterm_id" invisible="1" />
<field name="region_id" invisible="1" /> <field name="region_id" invisible="1" />
<field name="product_origin_country_id" invisible="1" /> <field name="product_origin_country_id" invisible="1" />
<field name="vat" />
<field name="partner_id" />
<field name="invoice_id" /> <field name="invoice_id" />
</group> </group>
<group string="Declaration" name="declaration"> <group string="Declaration" name="declaration">
@@ -274,7 +273,7 @@
/> />
<field name="product_id" /> <field name="product_id" />
<field name="hs_code_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_company_currency" />
<field name="amount_accessory_cost_company_currency" /> <field name="amount_accessory_cost_company_currency" />
<field name="transaction_id" /> <field name="transaction_id" />
@@ -294,6 +293,7 @@
invisible="1" invisible="1"
string="Product C/O" string="Product C/O"
/> />
<field name="vat" />
<field name="invoice_id" /> <field name="invoice_id" />
<field name="declaration_type" invisible="1" /> <field name="declaration_type" invisible="1" />
<field name="reporting_level" invisible="1" /> <field name="reporting_level" invisible="1" />
@@ -311,10 +311,7 @@
invisible="not context.get('intrastat_product_declaration_line_main_view')" invisible="not context.get('intrastat_product_declaration_line_main_view')"
/> />
<field name="hs_code_id" /> <field name="hs_code_id" />
<field <field name="src_dest_country_id" />
name="src_dest_country_id"
domain="[('intrastat', '=', True)]"
/>
<field <field
name="amount_company_currency" name="amount_company_currency"
widget="monetary" widget="monetary"
@@ -337,6 +334,7 @@
<field name="region_id" invisible="1" /> <field name="region_id" invisible="1" />
<field name="incoterm_id" invisible="1" /> <field name="incoterm_id" invisible="1" />
<field name="product_origin_country_id" invisible="1" /> <field name="product_origin_country_id" invisible="1" />
<field name="vat" />
</group> </group>
<group name="computation" string="Related Transactions"> <group name="computation" string="Related Transactions">
<field name="computation_line_ids" nolabel="1" /> <field name="computation_line_ids" nolabel="1" />
@@ -354,7 +352,7 @@
invisible="not context.get('intrastat_product_declaration_line_main_view')" invisible="not context.get('intrastat_product_declaration_line_main_view')"
/> />
<field name="hs_code_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_company_currency" />
<field name="transaction_id" /> <field name="transaction_id" />
<field name="weight" /> <field name="weight" />
@@ -373,6 +371,7 @@
invisible="1" invisible="1"
string="Product C/O" string="Product C/O"
/> />
<field name="vat" />
</tree> </tree>
</field> </field>
</record> </record>

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
# generated from manifests external_dependencies
python-stdnum>=1.16