[IMP] l10n_eu_service: OSS - Automatic Tax and Fiscal Position Mapping for EU Countries

Due to EU e-commerce 'One-Stop Shop' which becomes available on 1 July 2021, EU companies involved in distance sales are able to file their taxes using the new OSS process. Adopting OSS is now simplified in Odoo.

On installation of the module, all companies having a fiscal country in the EU will be processed.
For all existing domestic taxes in the company, an associated foreign tax (standard rate, reduced rate, etc) is found in the `EU_TAX_MAP`. This mapping will be created in a fiscal position that is automatically detected based on the customers' country.

All that is required from the user, is to review the tax mappings according to the products and services sold by the company.

A refresh button is also available in the odoo settings to redo/update the fiscal positions. This might be useful after adding a new tax.

Note: The tax mapping herein is not intended to cover all possible tax mappings between EU countries, but rather, the most commonly used ones. It is advised that users with special tax cases update the created Fiscal Positions according to their requirements.

Closes [Task 2579615]

closes odoo/odoo#73042

X-original-commit: 9c548c423614d1305542ebdffdbcd0da3cf09899
Signed-off-by: oco-odoo <oco-odoo@users.noreply.github.com>
Signed-off-by: Habib Ayob <h4818@users.noreply.github.com>
This commit is contained in:
Habib (ayh)
2021-07-01 12:22:15 +00:00
parent 7264d7fe90
commit 79e17df6f9
11 changed files with 2202 additions and 43 deletions
+1 -1
View File
@@ -107,7 +107,7 @@ class ResConfigSettings(models.TransientModel):
module_currency_rate_live = fields.Boolean(string="Automatic Currency Rates")
module_account_intrastat = fields.Boolean(string='Intrastat')
module_product_margin = fields.Boolean(string="Allow Product Margin")
module_l10n_eu_service = fields.Boolean(string="EU Digital Goods VAT")
module_l10n_eu_service = fields.Boolean(string="EU Intra-community Distance Selling")
module_account_taxcloud = fields.Boolean(string="Account TaxCloud")
module_account_invoice_extract = fields.Boolean(string="Bill Digitalization")
module_snailmail_account = fields.Boolean(string="Snailmail")
@@ -93,14 +93,15 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" id="eu_service" title="If you're selling digital goods to customers in the EU, you must charge VAT based on your customers' locations. This rule applies regardless of you are located. Digital goods are defined in the legislation as broadcasting, telecommunications, and services that are electronically supplied instead of shipped. Gift cards sent online are not included in the definition.">
<div class="col-12 col-lg-6 o_setting_box" id="eu_service" title="If you sell goods and services to customers in a foreign EU country, you must charge VAT based on the delivery address. This rule applies regardless of where you are located.">
<div class="o_setting_left_pane">
<field name="module_l10n_eu_service"/>
</div>
<div class="o_setting_right_pane" name="l10n_eu_service_right_pane">
<label for="module_l10n_eu_service"/>
<a href="https://www.odoo.com/documentation/14.0/applications/finance/accounting/taxation/taxes/eu_distance_selling.html" title="Documentation" class="o_doc_link" target="_blank"></a>
<div class="text-muted">
Apply right VAT rates for digital products sold in EU
Apply VAT of the EU country to which goods and services are delivered.
</div>
</div>
</div>
+8
View File
@@ -3,3 +3,11 @@
from . import wizard
from . import models
from odoo import api, SUPERUSER_ID
def l10n_eu_service_post_init(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
env['res.company']._map_all_eu_companies_taxes()
def l10n_eu_service_uninstall(cr, registry):
cr.execute("DELETE FROM ir_model_data WHERE module = 'l10n_eu_service' and model in ('account.tax.group', 'account.account');")
+12 -35
View File
@@ -2,48 +2,23 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'EU Mini One Stop Shop (MOSS)',
'name': 'EU One Stop Shop (OSS)',
'category': 'Accounting/Localizations',
'description': """
EU Mini One Stop Shop (MOSS) VAT for telecommunications, broadcasting and electronic services
=============================================================================================
EU One Stop Shop (OSS) VAT
==========================
As of January 1rst, 2015, telecommunications, broadcasting
and electronic services sold within the European Union
have to be always taxed in the country where the customer
belongs. In order to simplify the application of this EU
directive, the Mini One Stop Shop (MOSS) registration scheme
allows businesses to make a unique tax declaration.
From July 1st 2021, EU businesses that are selling goods within the EU above EUR 10 000 to buyers located in another EU Member State need to register and pay VAT in the buyers’ Member State.
Below this new EU-wide threshold you can continue to apply the domestic rules for VAT on your cross-border sales. In order to simplify the application of this EU directive, the One Stop Shop (OSS) registration scheme allows businesses to make a unique tax declaration.
This module makes it possible by helping with the creation
of the required EU fiscal positions and taxes in order to
automatically apply and record the required taxes.
This module makes it possible by helping with the creation of the required EU fiscal positions and taxes in order to automatically apply and record the required taxes.
This module installs a wizard to help setup fiscal positions
and taxes for selling electronic services inside EU.
The wizard lets you select:
- the EU countries to which you are selling these
services
- your national VAT tax for services, to be mapped
to the target country's tax
- optionally: a template fiscal position, in order
to copy the account mapping. Should be your
existing B2C Intra-EU fiscal position. (defaults
to no account mapping)
- optionally: an account to use for collecting the
tax amounts (defaults to the account used by your
national VAT tax for services)
It creates the corresponding fiscal positions and taxes,
automatically applicable for EU sales with a customer
in the selected countries.
The wizard can be run again for adding more countries.
All you have to do is check that the proposed mapping is suitable for the products and services you sell.
References
++++++++++
- Directive 2008/8/EC
- Council Implementing Regulation (EU) No 1042/2013
Council Directive (EU) 2017/2455 Council Directive (EU) 2019/1995
Council Implementing Regulation (EU) 2019/2026
""",
'depends': ['account'],
@@ -51,6 +26,8 @@ References
'security/ir.model.access.csv',
'wizard/wizard.xml',
'data/l10n_eu_service.service_tax_rate.csv',
'views/res_config_settings_views.xml'
'views/res_config_settings_views.xml',
],
'post_init_hook': 'l10n_eu_service_post_init',
'uninstall_hook': 'l10n_eu_service_uninstall',
}
@@ -1,5 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import chart_template
from . import eu_service_tax_rate
from . import eu_tax_map
from . import res_company
from . import res_config_settings
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class AccountChartTemplate(models.Model):
_inherit = 'account.chart.template'
def _load(self, sale_tax_rate, purchase_tax_rate, company):
rslt = super()._load(sale_tax_rate, purchase_tax_rate, company)
if company.account_fiscal_country_id in self.env.ref('base.europe').country_ids:
company._map_eu_taxes()
return rslt
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
from .eu_tax_map import EU_TAX_MAP
class Company(models.Model):
_inherit = 'res.company'
@api.model
def _map_all_eu_companies_taxes(self):
''' Identifies EU companies and calls the _map_eu_taxes function
'''
eu_countries = self.env.ref('base.europe').country_ids
companies = self.search([('account_fiscal_country_id', 'in', eu_countries.ids)])
companies._map_eu_taxes()
def _map_eu_taxes(self):
'''Creates or updates Fiscal Positions for each EU country excluding the company's account_fiscal_country_id
'''
eu_countries = self.env.ref('base.europe').country_ids
oss_tax_groups = self.env['ir.model.data'].search([
('module', '=', 'l10n_eu_service'),
('model', '=', 'account.tax.group')])
for company in self:
invoice_repartition_lines, refund_repartition_lines = company._get_repartition_lines_oss()
taxes = self.env['account.tax'].search([
('type_tax_use', '=', 'sale'),
('amount_type', '=', 'percent'),
('company_id', '=', company.id),
('country_id', '=', company.account_fiscal_country_id.id),
('tax_group_id', 'not in', oss_tax_groups.mapped('res_id'))])
for country in eu_countries - company.account_fiscal_country_id:
mapping = []
foreign_taxes = {}
fpos = self.env['account.fiscal.position'].search([
('country_id', '=', country.id),
('company_id', '=', company.id),
('auto_apply', '=', True),
('vat_required', '=', False)], limit=1)
if not fpos:
fpos = self.env['account.fiscal.position'].create({
'name': 'OSS B2C %s' % country.name,
'country_id': country.id,
'company_id': company.id,
'auto_apply': True,
})
for domestic_tax in taxes:
tax_amount = EU_TAX_MAP.get((company.account_fiscal_country_id.code, domestic_tax.amount, country.code), False)
if tax_amount and domestic_tax not in fpos.tax_ids.tax_src_id:
if not foreign_taxes.get(tax_amount, False):
tax_group_fid = 'oss_tax_group_%s' % str(tax_amount).replace('.', '_')
if not self.env['ir.model.data'].xmlid_to_object('l10n_eu_service.%s' % tax_group_fid):
self.env['ir.model.data'].create({
'name': tax_group_fid,
'module': 'l10n_eu_service',
'model': 'account.tax.group',
'res_id': self.env['account.tax.group'].create({'name': 'OSS %s%%' % tax_amount}).id,
'noupdate': True,
})
foreign_taxes[tax_amount] = self.env['account.tax'].create({
'name': '%(rate)s%% %(country)s %(label)s' % {'rate': tax_amount, 'country': country.code, 'label': country.vat_label},
'amount': tax_amount,
'invoice_repartition_line_ids': invoice_repartition_lines,
'refund_repartition_line_ids': refund_repartition_lines,
'type_tax_use': 'sale',
'description': "%s%%" % tax_amount,
'tax_group_id': self.env.ref('l10n_eu_service.%s' % tax_group_fid).id,
'sequence': 1000,
'company_id': company.id,
})
mapping.append((0, 0, {'tax_src_id': domestic_tax.id, 'tax_dest_id': foreign_taxes[tax_amount].id}))
if mapping:
fpos.write({
'tax_ids': mapping
})
def _get_repartition_lines_oss(self):
self.ensure_one()
defaults = self.env['account.tax'].with_company(self).default_get(['invoice_repartition_line_ids', 'refund_repartition_line_ids'])
oss_account = self._get_oss_account()
if oss_account:
defaults['invoice_repartition_line_ids'][1][2]['account_id'] = oss_account.id
defaults['refund_repartition_line_ids'][1][2]['account_id'] = oss_account.id
return defaults['invoice_repartition_line_ids'], defaults['refund_repartition_line_ids']
def _get_oss_account(self):
self.ensure_one()
if not self.env['ir.model.data'].xmlid_to_object('l10n_eu_service.oss_tax_account_company_%s' % self.id):
sales_tax_accounts = self.env['account.tax'].search([
('type_tax_use', '=', 'sale'),
('company_id', '=', self.id)
]).invoice_repartition_line_ids.mapped('account_id')
if not sales_tax_accounts:
return False
new_code = self.env['account.account']._search_new_account_code(self, len(sales_tax_accounts[0].code), sales_tax_accounts[0].code[:-2])
oss_account = self.env['account.account'].create({
'name': '%s OSS' % sales_tax_accounts[0].name,
'code': new_code,
'user_type_id': sales_tax_accounts[0].user_type_id.id,
'company_id': self.id,
})
self.env['ir.model.data'].create({
'name': 'oss_tax_account_company_%s' % self.id,
'module': 'l10n_eu_service',
'model': 'account.account',
'res_id': oss_account.id,
'noupdate': True,
})
return self.env.ref('l10n_eu_service.oss_tax_account_company_%s' % self.id)
@@ -9,6 +9,9 @@ class ResConfigSettings(models.TransientModel):
l10n_eu_services_eu_country = fields.Boolean('Is European country?', compute='_compute_l10n_eu_services_european_country')
def refresh_eu_tax_mapping(self):
self.env.companies._map_eu_taxes()
@api.depends('company_id')
def _compute_l10n_eu_services_european_country(self):
self.l10n_eu_services_eu_country = self.company_id.account_fiscal_country_id in self.env.ref('base.europe').country_ids
@@ -10,10 +10,11 @@
<field name="l10n_eu_services_eu_country" invisible="1"/>
<div class="content-group" attrs="{'invisible': [('module_l10n_eu_service', '=', False)]}">
<div class="mt8" attrs="{'invisible': [('l10n_eu_services_eu_country', '=', False)]}">
<button name="%(l10n_eu_service.action_eu_service)d" icon="fa-arrow-right" type="action" string="Add new countries" class="btn-link"/>
<button type="object" name="refresh_eu_tax_mapping" icon="fa-refresh" string="Refresh tax mapping" class="btn-link"/>
</div>
</div>
</div>
</field>
</record>
</odoo>
</odoo>
-3
View File
@@ -49,7 +49,4 @@
<field name="target">new</field>
</record>
<record id="open_l10n_eu_service" model="ir.actions.todo">
<field name="action_id" ref="action_eu_service"/>
</record>
</odoo>