[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:
@@ -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>
|
||||
|
||||
@@ -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');")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user