diff --git a/.travis.yml b/.travis.yml index 5023064..29f2881 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,7 +32,7 @@ addons: env: global: - - VERSION="8.0" TESTS="0" LINT_CHECK="0" TRANSIFEX="0" + - VERSION="10.0" TESTS="0" LINT_CHECK="0" TRANSIFEX="0" - TRANSIFEX_USER='transbot@odoo-community.org' # This line contains the encrypted transifex password # To encrypt transifex password, install travis ruby utils with: diff --git a/intrastat_base/README.rst b/intrastat_base/README.rst new file mode 100644 index 0000000..65531ec --- /dev/null +++ b/intrastat_base/README.rst @@ -0,0 +1,79 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +============== +Intrastat Base +============== + +This module contains common functions for the intrastat reporting and +should be used in combination with the generic reporting module +*intrastat_product* and with the country-specific reporting modules such +as: + +- *l10n_fr_intrastat_service*: + the module for the *Déclaration Européenne des Services* (DES) for France +- *l10n_fr_intrastat_product*: + the module for the *Déclaration d'Echange de Biens* (DEB) for France +- *l10n_be_intrastat_product*: + the module for the Intrastat Declaration for Belgium. + + +Installation +============ + +WARNING: + +This module conflicts with the module *report_intrastat* from the official addons. +If you have already installed the module *report_intrastat*, +you should uninstall it first before installing this module. + +Usage +===== + +This module adds an intrastat property on countries and activates this property +on the 28 countries of the European Union. + +With this module, the country field on partners becomes a required field. + +It adds an option *Exclude invoice line from intrastat if this tax is present* +on taxes. + +It adds a tab *Intrastat* on the company form view. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/227/10.0 + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case +of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and +welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Alexis de Lattre, Akretion +* Luc De Meyer, Noviat + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/intrastat_base/__init__.py b/intrastat_base/__init__.py new file mode 100644 index 0000000..a0fdc10 --- /dev/null +++ b/intrastat_base/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models diff --git a/intrastat_base/__manifest__.py b/intrastat_base/__manifest__.py new file mode 100644 index 0000000..78dfa04 --- /dev/null +++ b/intrastat_base/__manifest__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# © 2011-2016 Akretion (http://www.akretion.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Intrastat Reporting Base', + 'version': '10.0.1.0.0', + 'category': 'Intrastat', + 'license': 'AGPL-3', + 'summary': 'Base module for Intrastat reporting', + 'author': 'Akretion,Odoo Community Association (OCA)', + 'website': 'http://www.akretion.com', + 'depends': ['base_vat'], + 'conflicts': ['report_intrastat'], + 'data': [ + 'data/country_data.xml', + 'views/product_template.xml', + 'views/res_partner.xml', + 'views/res_country.xml', + 'views/account_tax.xml', + 'views/res_company.xml', + 'views/intrastat.xml', + ], + 'demo': [ + 'demo/intrastat_demo.xml', + ], + 'installable': True, +} diff --git a/intrastat_base/data/country_data.xml b/intrastat_base/data/country_data.xml new file mode 100644 index 0000000..3c75a3a --- /dev/null +++ b/intrastat_base/data/country_data.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/intrastat_base/demo/intrastat_demo.xml b/intrastat_base/demo/intrastat_demo.xml new file mode 100644 index 0000000..f53d397 --- /dev/null +++ b/intrastat_base/demo/intrastat_demo.xml @@ -0,0 +1,27 @@ + + + + + + + FR58441019213 + + + + BE0884025633 + True + + + + Shipping costs + SHIP + service + + 30 + True + + + diff --git a/intrastat_base/i18n/intrastat_base.pot b/intrastat_base/i18n/intrastat_base.pot new file mode 100644 index 0000000..fdc6996 --- /dev/null +++ b/intrastat_base/i18n/intrastat_base.pot @@ -0,0 +1,182 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * intrastat_base +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-05-27 18:43+0000\n" +"PO-Revision-Date: 2015-05-27 18:43+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: intrastat_base +#: help:product.template,is_accessory_cost:0 +msgid "Activate this option for shipping costs, packaging costs and all services related to the sale of products. This option is used for Intrastat reports." +msgstr "" + +#. module: intrastat_base +#: code:addons/intrastat_base/intrastat_common.py:165 +#, python-format +msgid "Cannot delete the declaration %s because it is in Done state" +msgstr "" + +#. module: intrastat_base +#: view:res.company:intrastat_base.view_company_form +msgid "Common Intrastat Settings" +msgstr "" + +#. module: intrastat_base +#: model:ir.model,name:intrastat_base.model_report_intrastat_common +msgid "Common functions for intrastat reports for products " +msgstr "" + +#. module: intrastat_base +#: model:ir.model,name:intrastat_base.model_res_company +msgid "Companies" +msgstr "" + +#. module: intrastat_base +#: code:addons/intrastat_base/intrastat_common.py:68 +#, python-format +msgid "Company not yet set on intrastat report." +msgstr "" + +#. module: intrastat_base +#: model:ir.model,name:intrastat_base.model_res_country +msgid "Country" +msgstr "" + +#. module: intrastat_base +#: view:res.country:intrastat_base.view_country_search +#: field:res.country,intrastat:0 +msgid "EU Country" +msgstr "" + +#. module: intrastat_base +#: field:product.template,exclude_from_intrastat:0 +msgid "Exclude from Intrastat reports" +msgstr "" + +#. module: intrastat_base +#: field:account.tax,exclude_from_intrastat_if_present:0 +msgid "Exclude invoice line from intrastat if this tax is present" +msgstr "" + +#. module: intrastat_base +#: field:report.intrastat.common,id:0 +msgid "ID" +msgstr "" + +#. module: intrastat_base +#: help:product.template,exclude_from_intrastat:0 +msgid "If set to True, the product or service will not be taken into account for Intrastat Product or Service reports. So you should leave this field to False unless you have a very good reason." +msgstr "" + +#. module: intrastat_base +#: help:account.tax,exclude_from_intrastat_if_present:0 +msgid "If this tax is present on an invoice line, this invoice line will be skipped when generating Intrastat Product or Service lines from invoices." +msgstr "" + +#. module: intrastat_base +#: view:product.template:intrastat_base.product_template_form_view +msgid "Intrastat Properties" +msgstr "" + +#. module: intrastat_base +#: model:ir.ui.menu,name:intrastat_base.menu_intrastat_base_root +msgid "Intrastat Reporting" +msgstr "" + +#. module: intrastat_base +#: view:res.company:intrastat_base.view_company_form +msgid "Intrastat Settings" +msgstr "" + +#. module: intrastat_base +#: field:product.template,is_accessory_cost:0 +msgid "Is accessory cost" +msgstr "" + +#. module: intrastat_base +#: help:res.company,intrastat_remind_user_ids:0 +msgid "List of OpenERP users who will receive a notification to remind them about the Intrastat declaration." +msgstr "" + +#. module: intrastat_base +#: code:addons/intrastat_base/company.py:55 +#, python-format +msgid "Missing e-mail address on user '%s'." +msgstr "" + +#. module: intrastat_base +#: model:ir.model,name:intrastat_base.model_product_template +msgid "Product Template" +msgstr "" + +#. module: intrastat_base +#: view:res.country:intrastat_base.view_country_search +msgid "Search Countries" +msgstr "" + +#. module: intrastat_base +#: help:res.country,intrastat:0 +msgid "Set to True for all European Union countries." +msgstr "" + +#. module: intrastat_base +#: model:product.template,name:intrastat_base.shipping_costs_exclude_product_template +msgid "Shipping costs" +msgstr "" + +#. module: intrastat_base +#: model:ir.model,name:intrastat_base.model_account_tax +msgid "Tax" +msgstr "" + +#. module: intrastat_base +#: code:addons/intrastat_base/intrastat_common.py:84 +#, python-format +msgid "The VAT number is not set for the partner '%s'." +msgstr "" + +#. module: intrastat_base +#: code:addons/intrastat_base/intrastat_common.py:76 +#, python-format +msgid "The company currency must be 'EUR', but is currently '%s'." +msgstr "" + +#. module: intrastat_base +#: code:addons/intrastat_base/intrastat_common.py:72 +#, python-format +msgid "The country is not set on the company '%s'." +msgstr "" + +#. module: intrastat_base +#: code:addons/intrastat_base/intrastat_common.py:105 +#, python-format +msgid "The generated XML file is not valid against the official XML Schema Definition. The generated XML file and the full error have been written in the server logs. Here is the error, which may give you an idea on the cause of the problem : %s." +msgstr "" + +#. module: intrastat_base +#: code:addons/intrastat_base/product.py:51 +#, python-format +msgid "The option 'Is accessory cost?' should only be activated on 'Service' products. You have activated this option for the product '%s' which is of type '%s'" +msgstr "" + +#. module: intrastat_base +#: code:addons/intrastat_base/intrastat_common.py:62 +#, python-format +msgid "The start date must be the first day of the month" +msgstr "" + +#. module: intrastat_base +#: field:res.company,intrastat_remind_user_ids:0 +msgid "Users Receiving the Intrastat Reminder" +msgstr "" + diff --git a/intrastat_base/models/__init__.py b/intrastat_base/models/__init__.py new file mode 100644 index 0000000..a571f6a --- /dev/null +++ b/intrastat_base/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +from . import res_country +from . import product_template +from . import account_tax +from . import res_company +from . import intrastat_common diff --git a/intrastat_base/models/account_tax.py b/intrastat_base/models/account_tax.py new file mode 100644 index 0000000..f9997fc --- /dev/null +++ b/intrastat_base/models/account_tax.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# © 2011-2016 Akretion (http://www.akretion.com). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields + + +class AccountTax(models.Model): + _inherit = "account.tax" + + exclude_from_intrastat_if_present = fields.Boolean( + string='Exclude invoice line from intrastat if this tax is present', + help="If this tax is present on an invoice line, this invoice " + "line will be skipped when generating Intrastat Product or " + "Service lines from invoices.") diff --git a/intrastat_base/models/intrastat_common.py b/intrastat_base/models/intrastat_common.py new file mode 100644 index 0000000..de3b04b --- /dev/null +++ b/intrastat_base/models/intrastat_common.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- +# © 2010-2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields, api, tools, _ +from odoo.exceptions import UserError +import logging + +logger = logging.getLogger(__name__) + + +class IntrastatCommon(models.AbstractModel): + _name = "intrastat.common" + _description = "Common functions for intrastat reports for products " + "and services" + + @api.multi + @api.depends('declaration_line_ids.amount_company_currency') + def _compute_numbers(self): + for this in self: + total_amount = 0 # it is an integer + num_lines = 0 + for line in this.declaration_line_ids: + total_amount += line.amount_company_currency + num_lines += 1 + this.num_decl_lines = num_lines + this.total_amount = total_amount + + @api.multi + def _check_generate_lines(self): + """Check wether all requirements are met for generating lines.""" + for this in self: + if not this.company_id: + raise UserError(_("Company not yet set on intrastat report.")) + company = this.company_id + if not company.country_id: + raise UserError( + _("The country is not set on the company '%s'.") + % company.name) + if company.currency_id.name != 'EUR': + raise UserError( + _("The company currency must be 'EUR', but is currently " + "'%s'.") + % company.currency_id.name) + return True + + @api.multi + def _check_generate_xml(self): + for this in self: + if not this.company_id.partner_id.vat: + raise UserError( + _("The VAT number is not set for the partner '%s'.") + % this.company_id.partner_id.name) + return True + + @api.model + def _check_xml_schema(self, xml_string, xsd_file): + '''Validate the XML file against the XSD''' + from lxml import etree + from StringIO import StringIO + xsd_etree_obj = etree.parse( + tools.file_open(xsd_file)) + official_schema = etree.XMLSchema(xsd_etree_obj) + try: + t = etree.parse(StringIO(xml_string)) + official_schema.assertValid(t) + except Exception, e: + # if the validation of the XSD fails, we arrive here + logger = logging.getLogger(__name__) + logger.warning( + "The XML file is invalid against the XML Schema Definition") + logger.warning(xml_string) + logger.warning(e) + raise UserError( + _("The generated XML file is not valid against the official " + "XML Schema Definition. The generated XML file and the " + "full error have been written in the server logs. " + "Here is the error, which may give you an idea on the " + "cause of the problem : %s.") + % str(e)) + return True + + @api.multi + def _attach_xml_file(self, xml_string, declaration_name): + '''Attach the XML file to the report_intrastat_product/service + object''' + self.ensure_one() + import base64 + filename = '%s_%s.xml' % (self.year_month, declaration_name) + attach = self.env['ir.attachment'].create({ + 'name': filename, + 'res_id': self.id, + 'res_model': self._name, + 'datas': base64.encodestring(xml_string), + 'datas_fname': filename}) + return attach.id + + @api.multi + def _unlink_attachments(self): + atts = self.env['ir.attachment'].search( + [('res_model', '=', self._name), + ('res_id', '=', self.id)]) + atts.unlink() + + @api.model + def _open_attach_view(self, attach_id, title='XML file'): + '''Returns an action which opens the form view of the + corresponding attachement''' + action = { + 'name': title, + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'ir.attachment', + 'type': 'ir.actions.act_window', + 'nodestroy': True, + 'target': 'current', + 'res_id': attach_id, + } + return action + + @api.multi + def _generate_xml(self): + """ + Inherit this method in the localization module + to generate the INTRASTAT Declaration XML file + + Returns: + string with XML data + + Call the _check_xml_schema() method + before returning the XML string. + """ + return False + + @api.multi + def send_reminder_email(self, mail_template_xmlid): + mail_template = self.env.ref(mail_template_xmlid) + for this in self: + if this.company_id.intrastat_remind_user_ids: + mail_template.send_mail(this.id) + logger.info( + 'Intrastat Reminder email has been sent (XMLID: %s).' + % mail_template_xmlid) + else: + logger.warning( + 'The list of users receiving the Intrastat Reminder is ' + 'empty on company %s' % this.company_id.name) + return True + + @api.multi + def unlink(self): + for intrastat in self: + if intrastat.state == 'done': + raise UserError( + _('Cannot delete the declaration %s ' + 'because it is in Done state') % self.year_month) + return super(IntrastatCommon, self).unlink() + + +class IntrastatResultView(models.TransientModel): + """ + Transient Model to display Intrastat Report results + """ + _name = 'intrastat.result.view' + + note = fields.Text( + string='Notes', readonly=True, + default=lambda self: self._context.get('note')) diff --git a/intrastat_base/models/product_template.py b/intrastat_base/models/product_template.py new file mode 100644 index 0000000..71cb987 --- /dev/null +++ b/intrastat_base/models/product_template.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# © 2010-2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + is_accessory_cost = fields.Boolean( + string='Is accessory cost', + help="Activate this option for shipping costs, packaging " + "costs and all services related to the sale of products. " + "This option is used for Intrastat reports.") + + @api.multi + @api.constrains('type', 'is_accessory_cost') + def _check_accessory_cost(self): + for this in self: + if this.is_accessory_cost and this.type != 'service': + raise ValidationError( + _("The option 'Is accessory cost?' should only be " + "activated on 'Service' products. You have activated " + "this option for the product '%s' which is of type " + "'%s'") % + (this.name, this.type)) diff --git a/intrastat_base/models/res_company.py b/intrastat_base/models/res_company.py new file mode 100644 index 0000000..bec99ac --- /dev/null +++ b/intrastat_base/models/res_company.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# © 2013-2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + + +class ResCompany(models.Model): + _inherit = "res.company" + + intrastat_remind_user_ids = fields.Many2many( + 'res.users', column1='company_id', column2='user_id', + string="Users Receiving the Intrastat Reminder", + help="List of Odoo users who will receive a notification to " + "remind them about the Intrastat declaration.") + intrastat_email_list = fields.Char( + compute='_compute_intrastat_email_list', + string='List of emails of Users Receiving the Intrastat Reminder') + + @api.multi + @api.depends( + 'intrastat_remind_user_ids', 'intrastat_remind_user_ids.email') + def _compute_intrastat_email_list(self): + for this in self: + emails = [] + for user in this.intrastat_remind_user_ids: + if user.email: + emails.append(user.email) + this.intrastat_email_list = ','.join(emails) + + @api.multi + @api.constrains('intrastat_remind_user_ids') + def _check_intrastat_remind_users(self): + for this in self: + for user in this.intrastat_remind_user_ids: + if not user.email: + raise ValidationError( + _("Missing e-mail address on user '%s'.") % + (user.name)) diff --git a/intrastat_base/models/res_country.py b/intrastat_base/models/res_country.py new file mode 100644 index 0000000..19f34f2 --- /dev/null +++ b/intrastat_base/models/res_country.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# © 2011-2014 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields + + +class ResCountry(models.Model): + _inherit = 'res.country' + + intrastat = fields.Boolean( + string='EU Country', + help="Set to True for all European Union countries.") diff --git a/intrastat_base/views/account_tax.xml b/intrastat_base/views/account_tax.xml new file mode 100644 index 0000000..a1e72cd --- /dev/null +++ b/intrastat_base/views/account_tax.xml @@ -0,0 +1,21 @@ + + + + + + + + intrastat.base.tax + account.tax + + + + + + + + + diff --git a/intrastat_base/views/intrastat.xml b/intrastat_base/views/intrastat.xml new file mode 100644 index 0000000..b7cd9d6 --- /dev/null +++ b/intrastat_base/views/intrastat.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + intrastat.result_view_form + intrastat.result.view + +
+ + + +
+
+
+
+
+ +
diff --git a/intrastat_base/views/product_template.xml b/intrastat_base/views/product_template.xml new file mode 100644 index 0000000..6440407 --- /dev/null +++ b/intrastat_base/views/product_template.xml @@ -0,0 +1,28 @@ + + + + + + + + intrastat.base.product.template.form + product.template + + + + + + + + + + + + + diff --git a/intrastat_base/views/res_company.xml b/intrastat_base/views/res_company.xml new file mode 100644 index 0000000..c3d1f30 --- /dev/null +++ b/intrastat_base/views/res_company.xml @@ -0,0 +1,26 @@ + + + + + + + + intrastat.company.form + res.company + + + + + + + + + + + + + + diff --git a/intrastat_base/views/res_country.xml b/intrastat_base/views/res_country.xml new file mode 100644 index 0000000..e9dec05 --- /dev/null +++ b/intrastat_base/views/res_country.xml @@ -0,0 +1,48 @@ + + + + + + + + + intrastat.base.country.tree + res.country + + + + + + + + + + intrastat.base.country.form + res.country + + + + + + + + + + + intrastat.base.country.search + res.country + + + + + + + + + + + diff --git a/intrastat_base/views/res_partner.xml b/intrastat_base/views/res_partner.xml new file mode 100644 index 0000000..7a9dfeb --- /dev/null +++ b/intrastat_base/views/res_partner.xml @@ -0,0 +1,25 @@ + + + + + + + + res.partner + + + + 1 + + + 1 + + + + + diff --git a/intrastat_product/README.rst b/intrastat_product/README.rst new file mode 100644 index 0000000..ecaf88d --- /dev/null +++ b/intrastat_product/README.rst @@ -0,0 +1,104 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +================= +Intrastat Product +================= + +This module contains common objects and fields for the Intrastat Product reporting, such as the *H.S. codes* (if you are not familiar with H.S. codes, read `Wikipedia `) and the *country of origin* on the products. + +It should be used in combination with country-specific Intrastat Product reporting modules +such as: + +- *l10n_fr_intrastat_product*: + the module for the *Déclaration d'Echange de Biens* (DEB) for France +- *l10n_be_intrastat_product*: + the module for the Intrastat Product Declaration for Belgium + +These country-specific modules can be found in the OCA localization for those countries. + +Installation +============ + +WARNING: + +This module conflicts with the module *report_intrastat* from the official addons. +If you have already installed the module *report_intrastat*, +you should uninstall it before installing this module. + +Usage +===== + +This module is used in combination with the country-specific +localization module(s). + +Coding guidelines for localization module: +------------------------------------------ + +We recommend to start by copying an existing module, e.g. l10n_be_intrastat_product +and adapt the code for the specific needs of your country. + +* Declaration Object + + Create a new class as follows: + + .. code-block:: python + + class L10nCcIntrastatProductDeclaration(models.Model): + _name = 'l10n.cc.intrastat.product.declaration' + _description = "Intrastat Product Declaration for YourCountry" + _inherit = ['intrastat.product.declaration', 'mail.thread'] + + whereby cc = your country code + +* Computation & Declaration Lines + + Create also new objects inheriting from the Computation and Declaration Line Objects + so that you can add methods or customise the methods from the base modules (make a PR when + the customization or new method is required for multiple countries). + + Adapt also the parent_id fields of the newly created objects + (cf. l10n_be_intrastat_product as example). + +* XML Files: Menu, Action, Views + + Cf. l10n_be_istrastat_product as example, replace "be" by your Country Code. + + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/227/8.0 + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case +of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed +and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Alexis de Lattre, Akretion +* Luc De Meyer, Noviat + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/intrastat_product/__init__.py b/intrastat_product/__init__.py new file mode 100644 index 0000000..cde864b --- /dev/null +++ b/intrastat_product/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models diff --git a/intrastat_product/__manifest__.py b/intrastat_product/__manifest__.py new file mode 100644 index 0000000..5baef25 --- /dev/null +++ b/intrastat_product/__manifest__.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Intrastat Product module for Odoo +# Copyright (C) 2011-2015 Akretion (http://www.akretion.com) +# Copyright (C) 2009-2015 Noviat (http://www.noviat.com) +# @author Alexis de Lattre +# @author Luc de Meyer +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +{ + 'name': 'Intrastat Product', + 'version': '8.0.1.4.1', + 'category': 'Intrastat', + 'license': 'AGPL-3', + 'summary': 'Base module for Intrastat Product', + 'author': 'Akretion, Noviat, Odoo Community Association (OCA)', + 'depends': [ + 'intrastat_base', + 'product_harmonized_system', + 'stock_picking_invoice_link', + 'sale_stock', + 'purchase', + ], + 'conflicts': ['report_intrastat'], + 'data': [ + 'views/hs_code.xml', + 'views/intrastat_region.xml', + 'views/intrastat_unit.xml', + 'views/intrastat_transaction.xml', + 'views/intrastat_transport_mode.xml', + 'views/intrastat_product_declaration.xml', + 'views/res_company.xml', + 'views/account_invoice.xml', + 'views/stock_picking.xml', + 'views/stock_warehouse.xml', + 'security/intrastat_security.xml', + 'security/ir.model.access.csv', + 'data/intrastat_transport_mode.xml', + 'data/intrastat_unit.xml', + ], + 'demo': ['demo/intrastat_demo.xml'], + 'installable': False, +} diff --git a/intrastat_product/data/intrastat_transport_mode.xml b/intrastat_product/data/intrastat_transport_mode.xml new file mode 100644 index 0000000..4b4d245 --- /dev/null +++ b/intrastat_product/data/intrastat_transport_mode.xml @@ -0,0 +1,53 @@ + + + + + + 1 + Sea + Sea Transport (including wagons, motor vehicles, trailers, semi-trailers and lighters on board of a ship) + + + 2 + Rail + Railway transport (including lorries on railway wagons) + + + 3 + Road + Road Transport + + + 4 + Air + Air Transport + + + 5 + Post + Postal consignments + + + 7 + Fixed installations + Fixed transport installations (e.g. pipelines, high-tension cables) + + + + 8 + Inland waterway + Inland waterway transport + + + 9 + Own propulsion + Own propulsion (imported or exported means of transport crossing the border under their own power, e.g. aircraft, lorries, boats, etc.) + + + + + + + + + diff --git a/intrastat_product/data/intrastat_unit.xml b/intrastat_product/data/intrastat_unit.xml new file mode 100644 index 0000000..a6fbaff --- /dev/null +++ b/intrastat_product/data/intrastat_unit.xml @@ -0,0 +1,125 @@ + + + + + + + + c/k + Carats - 1 metric carat = 2 × 10exp(–4) kg + + + ce/el + Number of cells + + + ct/l + Carrying capacity in tonnes + + + g + Gram + + + + gi F/S + Gram of fissile isotopes + + + kg H2O2 + Kilogram of hydrogen peroxide + + + kg K2O + Kilogram of potassium oxide + + + kg KOH + Kilogram of potassium hydroxide (caustic potash) + + + kg met.am. + Kilogram of methylamines + + + kg N + Kilogram of nitrogen + + + kg NaOH + Kilogram of sodium hydroxide (caustic soda) + + + kg/net eda + Kilogram drained net weight + + + kg P2O5 + Kilogram of diphosphorus pentaoxide + + + kg 90 pct sdt + Kilogram of substance 90 % dry + + + kg U + Kilogram of uranium + + + 1000 kWh + Thousand kilowatt hours + + + l + Litre + + + + 1000 l + Thousand litres + + + l alc. 100 pct + Litre pure (100 %) alcohol + + + m + Metre + + + + m2 + Square metre + + + m3 + Cubic metre + + + 1000 m3 + Thousand cubic metres + + + pa + Number of pairs + + + items + Number of items + + + + 100 items + Hundred items + + + 1000 items + Thousand items + + + TJ + Terajoule (gross calorific value) + + + + diff --git a/intrastat_product/demo/intrastat_demo.xml b/intrastat_product/demo/intrastat_demo.xml new file mode 100644 index 0000000..cf9d66d --- /dev/null +++ b/intrastat_product/demo/intrastat_demo.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + extended + extended + + + + + + diff --git a/intrastat_product/i18n/intrastat_product.pot b/intrastat_product/i18n/intrastat_product.pot new file mode 100644 index 0000000..6b1f00f --- /dev/null +++ b/intrastat_product/i18n/intrastat_product.pot @@ -0,0 +1,186 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * intrastat_product +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-05-27 18:43+0000\n" +"PO-Revision-Date: 2015-05-27 18:43+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: intrastat_product +#: field:report.intrastat.code,active:0 +msgid "Active" +msgstr "" + +#. module: intrastat_product +#: help:product.category,intrastat_id:0 +msgid "Code from the Harmonised System. If this code is not set on the product itself, it will be read here, on the related product category." +msgstr "" + +#. module: intrastat_product +#: help:product.template,intrastat_id:0 +msgid "Code from the Harmonised System. Nomenclature is available from the World Customs Organisation, see http://www.wcoomd.org/. Some countries have made their own extensions to this nomenclature." +msgstr "" + +#. module: intrastat_product +#: help:report.intrastat.code,intrastat_code:0 +msgid "Code used for the Intrastat declaration. Must be part of the 'Combined Nomenclature' (CN), cf http://en.wikipedia.org/wiki/Combined_NomenclatureMust have 8 digits with sometimes a 9th digit." +msgstr "" + +#. module: intrastat_product +#: field:product.template,origin_country_id:0 +msgid "Country of Origin" +msgstr "" + +#. module: intrastat_product +#: help:product.template,origin_country_id:0 +msgid "Country of origin of the product i.e. product 'made in ____'. If you have different countries of origin depending on the supplier from which you purchased the product, leave this field empty and use the equivalent field on the 'product supplier info' form." +msgstr "" + +#. module: intrastat_product +#: field:report.intrastat.code,create_uid:0 +msgid "Created by" +msgstr "" + +#. module: intrastat_product +#: field:report.intrastat.code,create_date:0 +msgid "Created on" +msgstr "" + +#. module: intrastat_product +#: field:report.intrastat.code,description:0 +msgid "Description" +msgstr "" + +#. module: intrastat_product +#: field:report.intrastat.code,display_name:0 +msgid "Display Name" +msgstr "" + +#. module: intrastat_product +#: field:report.intrastat.code,intrastat_code:0 +msgid "European Intrastat Code" +msgstr "" + +#. module: intrastat_product +#: help:report.intrastat.code,name:0 +msgid "Full length Harmonized System code (digits only). Full list is available from the World Customs Organisation, see http://www.wcoomd.org" +msgstr "" + +#. module: intrastat_product +#: model:ir.model,name:intrastat_product.model_report_intrastat_code +msgid "H.S. Code" +msgstr "" + +#. module: intrastat_product +#: field:report.intrastat.code,name:0 +msgid "H.S. code" +msgstr "" + +#. module: intrastat_product +#: code:addons/intrastat_product/intrastat.py:73 +#, python-format +msgid "H.S. codes should only contain digits. It is not the case of H.S. code '%s'." +msgstr "" + +#. module: intrastat_product +#: field:report.intrastat.code,id:0 +msgid "ID" +msgstr "" + +#. module: intrastat_product +#: model:ir.actions.act_window,name:intrastat_product.product_intrastat_code_act +#: model:ir.ui.menu,name:intrastat_product.product_intrastat_code_menu +#: field:product.category,intrastat_id:0 +#: field:product.template,intrastat_id:0 +#: view:report.intrastat.code:intrastat_product.product_intrastat_code_form +msgid "Intrastat Code" +msgstr "" + +#. module: intrastat_product +#: view:report.intrastat.code:intrastat_product.product_intrastat_code_tree +msgid "Intrastat Codes" +msgstr "" + +#. module: intrastat_product +#: view:product.category:intrastat_product.product_category_form_view +msgid "Intrastat Properties" +msgstr "" + +#. module: intrastat_product +#: field:report.intrastat.code,write_uid:0 +msgid "Last Updated by" +msgstr "" + +#. module: intrastat_product +#: field:report.intrastat.code,write_date:0 +msgid "Last Updated on" +msgstr "" + +#. module: intrastat_product +#: view:report.intrastat.code:intrastat_product.product_intrastat_code_form +#: field:report.intrastat.code,product_categ_ids:0 +msgid "Product Categories" +msgstr "" + +#. module: intrastat_product +#: model:ir.model,name:intrastat_product.model_product_category +msgid "Product Category" +msgstr "" + +#. module: intrastat_product +#: model:ir.model,name:intrastat_product.model_product_template +msgid "Product Template" +msgstr "" + +#. module: intrastat_product +#: view:report.intrastat.code:intrastat_product.product_intrastat_code_form +#: field:report.intrastat.code,product_tmpl_ids:0 +msgid "Products" +msgstr "" + +#. module: intrastat_product +#: view:report.intrastat.code:intrastat_product.product_intrastat_code_search +msgid "Search Intrastat Codes" +msgstr "" + +#. module: intrastat_product +#: help:report.intrastat.code,intrastat_uom_id:0 +msgid "Select the unit of measure if one is required for this particular Intrastat Code (other than the weight in Kg). If no particular unit of measure is required, leave empty." +msgstr "" + +#. module: intrastat_product +#: help:report.intrastat.code,description:0 +msgid "Short text description of the H.S. category" +msgstr "" + +#. module: intrastat_product +#: code:addons/intrastat_product/intrastat.py:82 +#, python-format +msgid "The field Intrastat Code should contain 8 or 9 digits. It is not the case of Intrastat Code '%s'." +msgstr "" + +#. module: intrastat_product +#: code:addons/intrastat_product/intrastat.py:77 +#, python-format +msgid "The field Intrastat Code should only contain digits. It is not the case of Intrastat Code '%s'." +msgstr "" + +#. module: intrastat_product +#: sql_constraint:report.intrastat.code:0 +msgid "This H.S. code already exists in Odoo !" +msgstr "" + +#. module: intrastat_product +#: field:report.intrastat.code,intrastat_uom_id:0 +msgid "UoM for Intrastat Report" +msgstr "" + diff --git a/intrastat_product/models/__init__.py b/intrastat_product/models/__init__.py new file mode 100644 index 0000000..09f082d --- /dev/null +++ b/intrastat_product/models/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +from . import res_company +from . import account_invoice +from . import hs_code +from . import intrastat_product_declaration +from . import intrastat_region +from . import intrastat_transaction +from . import intrastat_transport_mode +from . import intrastat_unit +from . import sale_order +from . import stock_picking +from . import stock_warehouse diff --git a/intrastat_product/models/account_invoice.py b/intrastat_product/models/account_invoice.py new file mode 100644 index 0000000..b62d4db --- /dev/null +++ b/intrastat_product/models/account_invoice.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Intrastat Product module for Odoo +# Copyright (C) 2011-2016 Akretion (http://www.akretion.com) +# Copyright (C) 2009-2016 Noviat (http://www.noviat.com) +# @author Alexis de Lattre +# @author Luc de Meyer +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp import models, fields, api + + +class AccountInvoice(models.Model): + _inherit = 'account.invoice' + + incoterm_id = fields.Many2one( + 'stock.incoterms', string='Incoterm', + help="International Commercial Terms are a series of predefined " + "commercial terms used in international transactions.") + intrastat_transaction_id = fields.Many2one( + 'intrastat.transaction', string='Intrastat Transaction Type', + default=lambda self: self._default_intrastat_transaction_id(), + ondelete='restrict', + help="Intrastat nature of transaction") + intrastat_transport_id = fields.Many2one( + 'intrastat.transport_mode', string='Intrastat Transport Mode', + ondelete='restrict') + src_dest_country_id = fields.Many2one( + 'res.country', string='Origin/Destination Country', + ondelete='restrict') + src_dest_region_id = fields.Many2one( + 'intrastat.region', string='Origin/Destination Region', + default=lambda self: self._default_src_dest_region_id(), + help="Origin/Destination Region." + "\nThis field is used for the Intrastat Declaration.", + ondelete='restrict') + intrastat_country = fields.Boolean( + compute='_compute_intrastat_country', + store=True, string='Intrastat Country', readonly=True) + intrastat = fields.Char( + string='Intrastat Declaration', + related='company_id.intrastat', readonly=True) + + @api.multi + @api.depends('src_dest_country_id', 'partner_id.country_id') + def _compute_intrastat_country(self): + for inv in self: + country = inv.src_dest_country_id \ + or inv.partner_id.country_id + inv.intrastat_country = country.intrastat + + @api.model + def _default_intrastat_transaction_id(self): + company = self.env['res.company'] + company_id = company._company_default_get('account.invoice') + company = company.browse(company_id) + inv_type = self._context.get('type') + if inv_type == 'out_invoice': + return company.intrastat_transaction_out_invoice + elif inv_type == 'out_refund': + return company.intrastat_transaction_out_refund + elif inv_type == 'in_invoice': + return company.intrastat_transaction_in_invoice + elif inv_type == 'in_refund': + return company.intrastat_transaction_in_refund + else: + return self.env['intrastat.transaction'] + + @api.model + def _default_src_dest_region_id(self): + rco = self.env['res.company'] + company_id = rco._company_default_get('account.invoice') + company = rco.browse(company_id) + return company.intrastat_region_id + + @api.multi + def onchange_partner_id( + self, type, partner_id, date_invoice=False, + payment_term=False, partner_bank_id=False, company_id=False): + res = super(AccountInvoice, self).onchange_partner_id( + type, partner_id, date_invoice=date_invoice, + payment_term=payment_term, partner_bank_id=partner_bank_id, + company_id=company_id) + if partner_id: + partner = self.env['res.partner'].browse(partner_id) + res['value']['src_dest_country_id'] = partner.country_id.id + return res + + +class AccountInvoiceLine(models.Model): + _inherit = 'account.invoice.line' + + hs_code_id = fields.Many2one( + 'hs.code', string='Intrastat Code', ondelete='restrict') + + @api.multi + def product_id_change( + self, product, uom_id, qty=0, name='', type='out_invoice', + partner_id=False, fposition_id=False, price_unit=False, + currency_id=False, company_id=None): + res = super(AccountInvoiceLine, self).product_id_change( + product, uom_id, qty=qty, name=name, type=type, + partner_id=partner_id, fposition_id=fposition_id, + price_unit=price_unit, currency_id=currency_id, + company_id=company_id) + + if product: + product = self.env['product.product'].browse(product) + hs_code = product.product_tmpl_id.get_hs_code_recursively() + if hs_code: + res['value']['hs_code_id'] = hs_code.id + else: + res['value']['hs_code_id'] = False + return res diff --git a/intrastat_product/models/hs_code.py b/intrastat_product/models/hs_code.py new file mode 100644 index 0000000..a4ae30b --- /dev/null +++ b/intrastat_product/models/hs_code.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Intrastat Product module for Odoo +# Copyright (C) 2011-2015 Akretion (http://www.akretion.com) +# Copyright (C) 2009-2015 Noviat (http://www.noviat.com) +# @author Alexis de Lattre +# @author Luc de Meyer +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp import models, fields, api, _ +from openerp.exceptions import ValidationError + + +class HSCode(models.Model): + _inherit = "hs.code" + + intrastat_unit_id = fields.Many2one( + '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))) diff --git a/intrastat_product/models/intrastat_product_declaration.py b/intrastat_product/models/intrastat_product_declaration.py new file mode 100644 index 0000000..6c5e099 --- /dev/null +++ b/intrastat_product/models/intrastat_product_declaration.py @@ -0,0 +1,955 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Intrastat Product module for Odoo +# Copyright (C) 2011-2015 Akretion (http://www.akretion.com) +# Copyright (C) 2009-2015 Noviat (http://www.noviat.com) +# @author Alexis de Lattre +# @author Luc de Meyer +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp import models, fields, api, _ +from openerp.exceptions import RedirectWarning, ValidationError +from openerp.exceptions import Warning as UserError +import openerp.addons.decimal_precision as dp +from datetime import datetime, date +from dateutil.relativedelta import relativedelta +import logging +_logger = logging.getLogger(__name__) + + +class IntrastatProductDeclaration(models.Model): + _name = 'intrastat.product.declaration' + _description = "Intrastat Product Report Base Object" + _rec_name = 'year_month' + _inherit = ['mail.thread', 'intrastat.common'] + _order = 'year_month desc, type, revision' + _track = { + 'state': { + 'intrastat_product.declaration_done': + lambda self, cr, uid, obj, ctx=None: obj['state'] == 'done', + } + } + + @api.model + def _get_type(self): + res = [] + company = self.env.user.company_id + arrivals = company.intrastat_arrivals + dispatches = company.intrastat_dispatches + if arrivals != 'exempt': + res.append(('arrivals', _('Declaration for Arrivals'))) + if dispatches != 'exempt': + res.append(('dispatches', _('Declaration for Dispatches'))) + return res + + @api.model + def _get_reporting_level(self): + return [ + ('standard', _('Standard')), + ('extended', _('Extended'))] + + @api.onchange('type') + def _onchange_type(self): + if self.type == 'arrivals': + self.reporting_level = \ + self.company_id.intrastat_arrivals == 'extended' \ + and 'extended' or 'standard' + if self.type == 'dispatches': + self.reporting_level = \ + self.company_id.intrastat_dispatches == 'extended' \ + and 'extended' or 'standard' + + @api.model + def _get_company(self): + return self.env.user.company_id + + @api.model + def _get_year(self): + if datetime.now().month == 1: + return datetime.now().year - 1 + else: + return datetime.now().year + + @api.model + def _get_month(self): + if datetime.now().month == 1: + return 12 + else: + return datetime.now().month - 1 + + @api.model + def _get_action(self): + return [ + ('replace', 'Replace'), + ('append', 'Append'), + ('nihil', 'Nihil')] + + @api.model + def _get_default_action(self): + return 'replace' + + company_id = fields.Many2one( + 'res.company', string='Company', readonly=True, + default=lambda self: self.env['res.company']._company_default_get( + 'intrastat.product.declaration')) + company_country_code = fields.Char( + compute='_compute_company_country_code', + string='Company Country Code', readonly=True, store=True, + help="Used in views and methods of localization modules.") + year = fields.Integer( + string='Year', required=True, + default=_get_year) + month = fields.Selection([ + (1, '01'), + (2, '02'), + (3, '03'), + (4, '04'), + (5, '05'), + (6, '06'), + (7, '07'), + (8, '08'), + (9, '09'), + (10, '10'), + (11, '11'), + (12, '12') + ], string='Month', required=True, + default=_get_month) + year_month = fields.Char( + compute='_compute_year_month', string='Period', readonly=True, + track_visibility='onchange', store=True, + help="Year and month of the declaration.") + type = fields.Selection( + '_get_type', string='Type', required=True, + states={'done': [('readonly', True)]}, + track_visibility='onchange', help="Select the declaration type.") + action = fields.Selection( + '_get_action', + string='Action', required=True, + default=_get_default_action, + states={'done': [('readonly', True)]}, + track_visibility='onchange') + revision = fields.Integer( + string='Revision', default=1, + states={'done': [('readonly', True)]}, + help="Used to keep track of changes") + computation_line_ids = fields.One2many( + 'intrastat.product.computation.line', + 'parent_id', string='Intrastat Product Computation Lines', + states={'done': [('readonly', True)]}) + declaration_line_ids = fields.One2many( + 'intrastat.product.declaration.line', + 'parent_id', string='Intrastat Product Declaration Lines', + states={'done': [('readonly', True)]}) + num_decl_lines = fields.Integer( + compute='_compute_numbers', string='Number of Declaration Lines', + store=True, track_visibility='onchange') + total_amount = fields.Integer( + compute='_compute_numbers', string='Total Fiscal Amount', store=True, + help="Total fiscal amount in company currency of the declaration.") + currency_id = fields.Many2one( + 'res.currency', related='company_id.currency_id', readonly=True, + string='Currency') + state = fields.Selection([ + ('draft', 'Draft'), + ('done', 'Done'), + ], string='State', readonly=True, track_visibility='onchange', + copy=False, default='draft', + help="State of the declaration. When the state is set to 'Done', " + "the parameters become read-only.") + note = fields.Text( + string='Notes', + help="You can add some comments here if you want.") + reporting_level = fields.Selection( + '_get_reporting_level', + string='Reporting Level') + valid = fields.Boolean( + compute='_check_validity', + string='Valid') + + @api.model + @api.constrains('year') + def _check_year(self): + for this in self: + s = str(this.year) + if len(s) != 4 or s[0] != '2': + raise ValidationError(_("Invalid Year !")) + + _sql_constraints = [ + ('date_uniq', + 'unique(year_month, company_id, type, revision)', + "A declaration of the same type already exists for this month !" + "\nYou should update the existing declaration " + "or change the revision number of this one."), + ] + + @api.multi + @api.depends('company_id') + def _compute_company_country_code(self): + for this in self: + if this.company_id: + this.company_country_code = \ + this.company_id.country_id.code.lower() + + @api.multi + @api.depends('year', 'month') + def _compute_year_month(self): + for this in self: + if this.year and this.month: + this.year_month = '-'.join( + [str(this.year), format(this.month, '02')]) + + @api.multi + @api.depends('month') + def _check_validity(self): + """ TO DO: logic based upon computation lines """ + for this in self: + this.valid = True + + @api.multi + def copy(self, default=None): + self.ensure_one() + default = default or {} + default['revision'] = self.revision + 1 + return super(IntrastatProductDeclaration, self).copy(default) + + def _company_warning(self, msg): + action = self.env.ref('base.action_res_company_form') + raise RedirectWarning( + msg, action.id, + _('Go to company configuration screen')) + + def _get_partner_country(self, inv_line): + country = inv_line.invoice_id.src_dest_country_id \ + or inv_line.invoice_id.partner_id.country_id + if not country.intrastat: + country = False + elif country == self.company_id.country_id: + country = False + return country + + def _get_intrastat_transaction(self, inv_line): + invoice = inv_line.invoice_id + if invoice.intrastat_transaction_id: + return invoice.intrastat_transaction_id + else: + company = invoice.company_id + if invoice.type == 'out_invoice': + return company.intrastat_transaction_out_invoice + elif invoice.type == 'out_refund': + return company.intrastat_transaction_out_refund + elif invoice.type == 'in_invoice': + return company.intrastat_transaction_in_invoice + elif invoice.type == 'in_refund': + return company.intrastat_transaction_in_refund + + def _get_weight_and_supplunits(self, inv_line, hs_code): + line_qty = inv_line.quantity + product = inv_line.product_id + invoice = inv_line.invoice_id + intrastat_unit_id = hs_code.intrastat_unit_id + source_uom = inv_line.uos_id + weight_uom_categ = self._get_uom_refs('weight_uom_categ') + kg_uom = self._get_uom_refs('kg_uom') + pce_uom_categ = self._get_uom_refs('pce_uom_categ') + pce_uom = self._get_uom_refs('pce_uom') + weight = suppl_unit_qty = 0.0 + + if not source_uom: + note = "\n" + _( + "Missing unit of measure on the line with %d " + "product(s) '%s' on invoice '%s'." + ) % (line_qty, product.name_get()[0][1], invoice.number) + note += "\n" + _( + "Please adjust this line manually.") + self._note += note + return weight, suppl_unit_qty + + if intrastat_unit_id: + target_uom = intrastat_unit_id.uom_id + if not target_uom: + note = "\n" + _( + "Conversion from Intrastat Supplementary Unit '%s' to " + "Unit of Measure is not implemented yet." + ) % intrastat_unit_id.name + note += "\n" + _( + "Please correct the Intrastat Supplementary Unit " + "settings and regenerate the lines or adjust the lines " + "with Intrastat Code '%s' manually" + ) % hs_code.display_name + self._note += note + return weight, suppl_unit_qty + if target_uom.category_id == source_uom.category_id: + suppl_unit_qty = self.env['product.uom']._compute_qty_obj( + source_uom, line_qty, target_uom) + else: + note = "\n" + _( + "Conversion from unit of measure '%s' to '%s' " + "is not implemented yet." + ) % (source_uom.name, target_uom.name) + note += "\n" + _( + "Please correct the unit of measure settings and " + "regenerate the lines or adjust the impacted " + "lines manually") + self._note += note + return weight, suppl_unit_qty + + if source_uom == kg_uom: + weight = line_qty + elif source_uom.category_id == weight_uom_categ: + weight = self.env['product.uom']._compute_qty_obj( + source_uom, line_qty, kg_uom) + elif source_uom.category_id == pce_uom_categ: + if not product.weight_net: + note = "\n" + _( + "Missing net weight on product %s." + ) % product.name_get()[0][1] + note += "\n" + _( + "Please correct the product record and regenerate " + "the lines or adjust the impacted lines manually") + self._note += note + return weight, suppl_unit_qty + if source_uom == pce_uom: + weight = product.weight_net * line_qty + else: + # Here, I suppose that, on the product, the + # weight is per PCE and not per uom_id + weight = product.weight_net * \ + self.env['product.uom']._compute_qty_obj( + source_uom, line_qty, pce_uom) + else: + note = "\n" + _( + "Conversion from unit of measure '%s' to 'Kg' " + "is not implemented yet. It is needed for product '%s'." + ) % (source_uom.name, product.name_get()[0][1]) + note += "\n" + _( + "Please correct the unit of measure settings and " + "regenerate the lines or adjust the impacted lines " + "manually") + self._note += note + return weight, suppl_unit_qty + + return weight, suppl_unit_qty + + def _get_amount(self, inv_line): + invoice = inv_line.invoice_id + amount = invoice.currency_id.with_context( + date=invoice.date_invoice).compute( + inv_line.price_subtotal, + self.company_id.currency_id) + return amount + + def _get_region(self, inv_line): + """ + For supplier invoices/refunds: if the invoice line is linked + to a stock move, use the destination stock location ; + otherwise, get the PO (which is linked to a stock location) + and then get the warehouse. + It is important to take into account the following scenario: + I order a qty of 50 kg and my suppliers delivers and invoices 52 kg + (quite common in some industries where the order qty cannot be exact + due to some operational constraints) ; in this case, I have a qty of + 2 kg which is not linked to a PO, but it is linked to a stock move. + + For customer invoices/refunds: if the invoice line is linked to a + stock move, use the source stock location ; + otherwise, get the sale order, which is linked to the warehouse. + + If none found, get the company's default intrastat region. + + """ + region = False + inv_type = inv_line.invoice_id.type + if inv_line.move_line_ids: + if inv_type in ('in_invoice', 'out_refund'): + region = inv_line.move_line_ids[0].location_id.\ + get_intrastat_region() + else: + region = inv_line.move_line_ids[0].location_dest_id.\ + get_intrastat_region() + elif inv_type in ('in_invoice', 'in_refund'): + po_lines = self.env['purchase.order.line'].search( + [('invoice_lines', 'in', inv_line.id)]) + if po_lines: + po = po_lines.order_id + region = po.location_id.get_intrastat_region() + elif inv_line.invoice_id.type in ('out_invoice', 'out_refund'): + so_lines = self.env['sale.order.line'].search( + [('invoice_lines', 'in', inv_line.id)]) + if so_lines: + so = so_lines.order_id + region = so.warehouse_id.region_id + if not region: + if self.company_id.intrastat_region_id: + region = self.company_id.intrastat_region_id + return region + + def _get_transport(self, inv_line): + transport = inv_line.invoice_id.intrastat_transport_id \ + or self.company_id.intrastat_transport_id + if not transport: + msg = _( + "The default Intrastat Transport Mode " + "of the Company is not set, " + "please configure it first.") + self._company_warning(msg) + return transport + + def _get_incoterm(self, inv_line): + incoterm = inv_line.invoice_id.incoterm_id \ + or self.company_id.intrastat_incoterm_id + if not incoterm: + msg = _( + "The default Incoterm " + "of the Company is not set, " + "please configure it first.") + self._company_warning(msg) + return incoterm + + def _get_product_origin_country(self, inv_line): + return inv_line.product_id.origin_country_id + + def _update_computation_line_vals(self, inv_line, line_vals): + """ placeholder for localization modules """ + pass + + def _handle_invoice_accessory_cost( + self, invoice, lines_current_invoice, + total_inv_accessory_costs_cc, total_inv_product_cc, + total_inv_weight): + """ + Affect accessory costs pro-rata of the value + (or pro-rata of the weight if the goods of the invoice + have no value) + + This method allows to implement a different logic + in the localization modules. + E.g. in Belgium accessory cost should not be added. + """ + if total_inv_accessory_costs_cc: + if total_inv_product_cc: + # pro-rata of the value + for ac_line_vals in lines_current_invoice: + ac_line_vals['amount_accessory_cost_company_currency'] = ( + total_inv_accessory_costs_cc * + ac_line_vals['amount_company_currency'] / + total_inv_product_cc) + else: + # pro-rata of the weight + for ac_line_vals in lines_current_invoice: + ac_line_vals['amount_accessory_cost_company_currency'] = ( + total_inv_accessory_costs_cc * + ac_line_vals['weight'] / + total_inv_weight) + + def _prepare_invoice_domain(self): + """ + Complete this method in the localization module + with the country-specific logic for arrivals and dispatches. + Cf. l10n_be_intrastat_product_declaration for an example + """ + start_date = date(self.year, self.month, 1) + end_date = start_date + relativedelta(day=1, months=+1, days=-1) + domain = [ + ('date_invoice', '>=', start_date), + ('date_invoice', '<=', end_date), + ('state', 'in', ['open', 'paid']), + ('intrastat_country', '=', True), + ('company_id', '=', self.company_id.id)] + return domain + + def _is_product(self, invoice_line): + if ( + invoice_line.product_id and + invoice_line.product_id.type in ('product', 'consu')): + return True + else: + return False + + def _gather_invoices_init(self): + """ placeholder for localization modules """ + pass + + def _gather_invoices(self): + + lines = [] + accessory_costs = self.company_id.intrastat_accessory_costs + + self._gather_invoices_init() + domain = self._prepare_invoice_domain() + invoices = self.env['account.invoice'].search(domain) + + for invoice in invoices: + + lines_current_invoice = [] + total_inv_accessory_costs_cc = 0.0 # in company currency + total_inv_product_cc = 0.0 # in company currency + total_inv_weight = 0.0 + for inv_line in invoice.invoice_line: + + if ( + accessory_costs and + inv_line.product_id and + inv_line.product_id.is_accessory_cost): + acost = invoice.currency_id.with_context( + date=invoice.date_invoice).compute( + inv_line.price_subtotal, + self.company_id.currency_id) + total_inv_accessory_costs_cc += acost + + continue + + if not inv_line.quantity: + _logger.info( + 'Skipping invoice line %s qty %s ' + 'of invoice %s. Reason: qty = 0' + % (inv_line.name, inv_line.quantity, invoice.number)) + continue + + partner_country = self._get_partner_country(inv_line) + if not partner_country: + _logger.info( + 'Skipping invoice line %s qty %s ' + 'of invoice %s. Reason: no partner_country' + % (inv_line.name, inv_line.quantity, invoice.number)) + continue + + if any([ + tax.exclude_from_intrastat_if_present + for tax in inv_line.invoice_line_tax_id]): + _logger.info( + 'Skipping invoice line %s ' + 'qty %s of invoice %s. Reason: ' + 'tax.exclude_from_intrastat_if_present' + % (inv_line.name, inv_line.quantity, invoice.number)) + continue + + if inv_line.hs_code_id: + hs_code = inv_line.hs_code_id + elif inv_line.product_id and self._is_product(inv_line): + hs_code = inv_line.product_id.product_tmpl_id.\ + get_hs_code_recursively() + if not hs_code: + note = "\n" + _( + "Missing H.S. code on product %s. " + "This product is present in invoice %s.") % ( + inv_line.product_id.name_get()[0][1], + inv_line.invoice_id.number) + self._note += note + continue + else: + _logger.info( + 'Skipping invoice line %s qty %s' + 'of invoice %s. Reason: no product nor hs_code' + % (inv_line.name, inv_line.quantity, invoice.number)) + continue + + intrastat_transaction = \ + self._get_intrastat_transaction(inv_line) + + weight, suppl_unit_qty = self._get_weight_and_supplunits( + inv_line, hs_code) + total_inv_weight += weight + + amount_company_currency = self._get_amount(inv_line) + total_inv_product_cc += amount_company_currency + + product_origin_country = self._get_product_origin_country( + inv_line) + + region = self._get_region(inv_line) + + line_vals = { + 'parent_id': self.id, + 'invoice_line_id': inv_line.id, + 'src_dest_country_id': partner_country.id, + 'product_id': inv_line.product_id.id, + 'hs_code_id': hs_code.id, + 'weight': weight, + 'suppl_unit_qty': suppl_unit_qty, + 'amount_company_currency': amount_company_currency, + 'amount_accessory_cost_company_currency': 0.0, + 'transaction_id': intrastat_transaction.id, + 'product_origin_country_id': + product_origin_country.id or False, + 'region_id': region and region.id or False, + } + + # extended declaration + if self._extended: + transport = self._get_transport(inv_line) + line_vals.update({ + 'transport_id': transport.id, + }) + + self._update_computation_line_vals(inv_line, line_vals) + + if line_vals: + lines_current_invoice.append((line_vals)) + + self._handle_invoice_accessory_cost( + invoice, lines_current_invoice, + total_inv_accessory_costs_cc, total_inv_product_cc, + total_inv_weight) + + for line_vals in lines_current_invoice: + if ( + not line_vals['amount_company_currency'] and + not + line_vals['amount_accessory_cost_company_currency']): + inv_line = self.env['account.invoice.line'].browse( + line_vals['invoice_line_id']) + _logger.info( + 'Skipping invoice line %s qty %s ' + 'of invoice %s. Reason: price_subtotal = 0 ' + 'and accessory costs = 0' + % (inv_line.name, inv_line.quantity, + inv_line.invoice_id.number)) + continue + lines.append(line_vals) + + return lines + + def _get_uom_refs(self, ref): + uom_refs = { + 'weight_uom_categ': self.env.ref('product.product_uom_categ_kgm'), + 'kg_uom': self.env.ref('product.product_uom_kgm'), + 'pce_uom_categ': self.env.ref('product.product_uom_categ_unit'), + 'pce_uom': self.env.ref('product.product_uom_unit') + } + return uom_refs[ref] + + @api.multi + def action_gather(self): + self.ensure_one() + self.message_post(_("Generate Lines from Invoices")) + self._check_generate_lines() + self._note = '' + if ( + self.type == 'arrivals' and + self.company_id.intrastat_arrivals == 'extended') or ( + self.type == 'dispatches' and + self.company_id.intrastat_dispatches == 'extended'): + self._extended = True + else: + self._extended = False + + self.computation_line_ids.unlink() + self.declaration_line_ids.unlink() + lines = self._gather_invoices() + + if not lines: + self.action = 'nihil' + note = "\n" + \ + _("No records found for the selected period !") + '\n' + \ + _("The Declaration Action has been set to 'nihil'.") + self._note += note + else: + self.write({'computation_line_ids': [(0, 0, x) for x in lines]}) + + if self._note: + note_header = '\n\n>>> ' + fields.Datetime.to_string( + fields.Datetime.context_timestamp(self, datetime.now())) + '\n' + self.note = note_header + self._note + (self.note or '') + result_view = self.env.ref( + 'intrastat_base.intrastat_result_view_form') + return { + 'name': _("Generate lines from invoices: results"), + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'intrastat.result.view', + 'view_id': result_view.id, + 'target': 'new', + 'context': dict(self._context, note=self._note), + 'type': 'ir.actions.act_window', + } + + return True + + @api.model + def _group_line_hashcode_fields(self, computation_line): + return { + 'country': computation_line.src_dest_country_id.id or False, + 'hs_code_id': computation_line.hs_code_id.id or False, + 'intrastat_unit': computation_line.intrastat_unit_id.id or False, + 'transaction': computation_line.transaction_id.id or False, + 'transport': computation_line.transport_id.id or False, + 'region': computation_line.region_id.id or False, + 'product_origin_country': + computation_line.product_origin_country_id.id or False, + } + + def group_line_hashcode(self, computation_line): + hc_fields = self._group_line_hashcode_fields(computation_line) + hashcode = '-'.join([unicode(f) for f in hc_fields.itervalues()]) + return hashcode + + @api.multi + def generate_declaration(self): + """ generate declaration lines """ + self.ensure_one() + assert self.valid, 'Computation lines are not valid' + self.message_post(_("Generate Declaration Lines")) + # Delete existing declaration lines + self.declaration_line_ids.unlink() + # Regenerate declaration lines from computation lines + dl_group = {} + for cl in self.computation_line_ids: + hashcode = self.group_line_hashcode(cl) + if hashcode in dl_group: + dl_group[hashcode].append(cl) + else: + dl_group[hashcode] = [cl] + ipdl = self.declaration_line_ids + for cl_lines in dl_group.values(): + vals = ipdl._prepare_declaration_line(cl_lines) + declaration_line = ipdl.create(vals) + for cl in cl_lines: + cl.write({'declaration_line_id': declaration_line.id}) + return True + + @api.multi + def generate_xml(self): + """ generate the INTRASTAT Declaration XML file """ + self.ensure_one() + self.message_post(_("Generate XML Declaration File")) + self._check_generate_xml() + self._unlink_attachments() + xml_string = self._generate_xml() + if xml_string: + attach_id = self._attach_xml_file( + xml_string, '%s_%s' % (self.type, self.revision)) + return self._open_attach_view(attach_id) + else: + raise UserError( + _("No XML File has been generated.")) + + @api.multi + def done(self): + self.write({'state': 'done'}) + + @api.multi + def back2draft(self): + self.write({'state': 'draft'}) + + +class IntrastatProductComputationLine(models.Model): + _name = 'intrastat.product.computation.line' + _description = "Intrastat Product Computataion Lines" + + parent_id = fields.Many2one( + 'intrastat.product.declaration', + string='Intrastat Product Declaration', + ondelete='cascade', readonly=True) + company_id = fields.Many2one( + 'res.company', related='parent_id.company_id', + string="Company", readonly=True) + company_currency_id = fields.Many2one( + 'res.currency', related='company_id.currency_id', + string="Company currency", readonly=True) + type = fields.Selection( + related='parent_id.type', + string='Type', + readonly=True) + reporting_level = fields.Selection( + related='parent_id.reporting_level', + string='Reporting Level', + readonly=True) + valid = fields.Boolean( + compute='_check_validity', + string='Valid') + invoice_line_id = fields.Many2one( + 'account.invoice.line', string='Invoice Line', readonly=True) + invoice_id = fields.Many2one( + 'account.invoice', related='invoice_line_id.invoice_id', + string='Invoice', readonly=True) + declaration_line_id = fields.Many2one( + 'intrastat.product.declaration.line', + string='Declaration Line', readonly=True) + src_dest_country_id = fields.Many2one( + 'res.country', string='Country', + help="Country of Origin/Destination", + domain=[('intrastat', '=', True)]) + product_id = fields.Many2one( + 'product.product', related='invoice_line_id.product_id', + string='Product', readonly=True) + hs_code_id = fields.Many2one( + 'hs.code', string='Intrastat Code') + intrastat_unit_id = fields.Many2one( + 'intrastat.unit', related='hs_code_id.intrastat_unit_id', + string='Suppl. Unit', readonly=True, + help="Intrastat Supplementary Unit") + weight = fields.Float( + string='Weight', + digits=dp.get_precision('Stock Weight'), help="Net weight in Kg") + suppl_unit_qty = fields.Float( + string='Suppl. Unit Qty', + digits=dp.get_precision('Product Unit of Measure'), + help="Supplementary Units Quantity") + amount_company_currency = fields.Float( + string='Fiscal Value', + digits=dp.get_precision('Account'), required=True, + help="Amount in company currency to write in the declaration. " + "Amount in company currency = amount in invoice currency " + "converted to company currency with the rate of the invoice date.") + amount_accessory_cost_company_currency = fields.Float( + string='Accessory Costs', + digits=dp.get_precision('Account'), + help="Amount in company currency of the accessory costs related to " + "this invoice line (by default, these accessory costs are computed " + "at the pro-rata of the amount of each invoice line.") + transaction_id = fields.Many2one( + 'intrastat.transaction', + string='Intrastat Transaction') + region_id = fields.Many2one( + 'intrastat.region', string='Intrastat Region') + # extended declaration + incoterm_id = fields.Many2one( + 'stock.incoterms', string='Incoterm') + transport_id = fields.Many2one( + 'intrastat.transport_mode', + string='Transport Mode') + product_origin_country_id = fields.Many2one( + 'res.country', string='Country of Origin of the Product', + help="Country of origin of the product i.e. product 'made in ____'") + + @api.multi + @api.depends('transport_id') + def _check_validity(self): + """ TO DO: logic based upon fields """ + for this in self: + this.valid = True + + @api.onchange('product_id') + def _onchange_product(self): + self.weight = 0.0 + self.suppl_unit_qty = 0.0 + self.intrastat_code_id = False + self.intrastat_unit_id = False + if self.product_id: + self.intrastat_code_id = self.product_id.intrastat_id + self.intrastat_unit_id =\ + self.product_id.intrastat_id.intrastat_unit_id + if not self.intrastat_unit_id: + self.weight = self.product_id.weight_net + + +class IntrastatProductDeclarationLine(models.Model): + _name = 'intrastat.product.declaration.line' + _description = "Intrastat Product Declaration Lines" + + parent_id = fields.Many2one( + 'intrastat.product.declaration', + string='Intrastat Product Declaration', + ondelete='cascade', readonly=True) + company_id = fields.Many2one( + 'res.company', related='parent_id.company_id', + string="Company", readonly=True) + company_currency_id = fields.Many2one( + 'res.currency', related='company_id.currency_id', + string="Company currency", readonly=True) + type = fields.Selection( + related='parent_id.type', + string='Type', + readonly=True) + reporting_level = fields.Selection( + related='parent_id.reporting_level', + string='Reporting Level', + readonly=True) + computation_line_ids = fields.One2many( + 'intrastat.product.computation.line', 'declaration_line_id', + string='Computation Lines', readonly=True) + src_dest_country_id = fields.Many2one( + '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( + 'intrastat.unit', related='hs_code_id.intrastat_unit_id', + string='Suppl. Unit', readonly=True, + help="Intrastat Supplementary Unit") + weight = fields.Integer( + string='Weight', help="Net weight in Kg") + suppl_unit_qty = fields.Integer( + string='Suppl. Unit Qty', + help="Supplementary Units Quantity") + amount_company_currency = fields.Integer( + string='Fiscal Value', + help="Amount in company currency to write in the declaration. " + "Amount in company currency = amount in invoice currency " + "converted to company currency with the rate of the invoice date.") + transaction_id = fields.Many2one( + 'intrastat.transaction', + string='Intrastat Transaction') + region_id = fields.Many2one( + 'intrastat.region', string='Intrastat Region') + # extended declaration + incoterm_id = fields.Many2one( + 'stock.incoterms', string='Incoterm') + transport_id = fields.Many2one( + 'intrastat.transport_mode', + string='Transport Mode') + product_origin_country_id = fields.Many2one( + 'res.country', string='Country of Origin of the Product', + help="Country of origin of the product i.e. product 'made in ____'") + + @api.model + def _prepare_grouped_fields(self, computation_line, fields_to_sum): + vals = { + 'src_dest_country_id': computation_line.src_dest_country_id.id, + 'intrastat_unit_id': computation_line.intrastat_unit_id.id, + 'hs_code_id': computation_line.hs_code_id.id, + 'transaction_id': computation_line.transaction_id.id, + 'transport_id': computation_line.transport_id.id, + 'region_id': computation_line.region_id.id, + 'parent_id': computation_line.parent_id.id, + 'product_origin_country_id': + computation_line.product_origin_country_id.id, + 'amount_company_currency': 0.0, + } + for field in fields_to_sum: + vals[field] = 0.0 + return vals + + def _fields_to_sum(self): + fields_to_sum = [ + 'weight', + 'suppl_unit_qty', + ] + return fields_to_sum + + @api.model + def _prepare_declaration_line(self, computation_lines): + fields_to_sum = self._fields_to_sum() + vals = self._prepare_grouped_fields( + computation_lines[0], fields_to_sum) + for computation_line in computation_lines: + for field in fields_to_sum: + vals[field] += computation_line[field] + vals['amount_company_currency'] += ( + computation_line['amount_company_currency'] + + computation_line['amount_accessory_cost_company_currency']) + # round, otherwise odoo with truncate (6.7 -> 6... instead of 7 !) + for field in fields_to_sum: + vals[field] = int(round(vals[field])) + if not vals['weight']: + vals['weight'] = 1 + vals['amount_company_currency'] = int(round( + vals['amount_company_currency'])) + return vals diff --git a/intrastat_product/models/intrastat_region.py b/intrastat_product/models/intrastat_region.py new file mode 100644 index 0000000..9dddfdf --- /dev/null +++ b/intrastat_product/models/intrastat_region.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Odoo, Open Source Management Solution +# +# Copyright (c) 2009-2015 Noviat nv/sa (www.noviat.com). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp import models, fields + + +class IntrastatRegion(models.Model): + _name = 'intrastat.region' + _description = "Intrastat Region" + + code = fields.Char(string='Code', required=True) + country_id = fields.Many2one( + 'res.country', string='Country', required=True) + name = fields.Char(string='Name', translate=True) + description = fields.Char(string='Description') + company_id = fields.Many2one( + 'res.company', string='Company', + default=lambda self: self.env['res.company']._company_default_get( + 'intrastat.region')) + + _sql_constraints = [ + ('intrastat_region_code_unique', + 'UNIQUE(code, country_id)', + 'Code must be unique.')] diff --git a/intrastat_product/models/intrastat_transaction.py b/intrastat_product/models/intrastat_transaction.py new file mode 100644 index 0000000..ea9002e --- /dev/null +++ b/intrastat_product/models/intrastat_transaction.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Intrastat Product module for Odoo +# Copyright (C) 2011-2015 Akretion (http://www.akretion.com) +# Copyright (C) 2009-2015 Noviat (http://www.noviat.com) +# @author Alexis de Lattre +# @author Luc de Meyer +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp import models, fields, api + + +class IntrastatTransaction(models.Model): + _name = 'intrastat.transaction' + _description = "Intrastat Transaction" + _order = 'code' + _rec_name = 'display_name' + + code = fields.Char(string='Code', required=True) + description = fields.Text(string='Description') + display_name = fields.Char( + compute='_compute_display_name', string="Display Name", readonly=True, + store=True) + company_id = fields.Many2one( + 'res.company', string='Company', + default=lambda self: self.env['res.company']._company_default_get( + 'intrastat.transaction')) + + @api.multi + @api.depends('code', 'description') + def _compute_display_name(self): + for this in self: + display_name = this.code + if this.description: + display_name += ' ' + this.description + this.display_name = len(display_name) > 55 \ + and display_name[:55] + '...' \ + or display_name + + _sql_constraints = [( + 'intrastat_transaction_code_unique', + 'UNIQUE(code, company_id)', + 'Code must be unique.')] diff --git a/intrastat_product/models/intrastat_transport_mode.py b/intrastat_product/models/intrastat_transport_mode.py new file mode 100644 index 0000000..484c84d --- /dev/null +++ b/intrastat_product/models/intrastat_transport_mode.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Intrastat Product module for Odoo +# Copyright (C) 2011-2015 Akretion (http://www.akretion.com) +# Copyright (C) 2009-2015 Noviat (http://www.noviat.com) +# @author Alexis de Lattre +# @author Luc de Meyer +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp import models, fields, api + + +class IntrastatTransportMode(models.Model): + _name = 'intrastat.transport_mode' + _description = "Intrastat Transport Mode" + _rec_name = 'display_name' + _order = 'code' + + display_name = fields.Char( + string='Display Name', compute='_display_name', store=True, + readonly=True) + code = fields.Char(string='Code', required=True) + name = fields.Char(string='Name', required=True, translate=True) + description = fields.Char(string='Description', translate=True) + + @api.multi + @api.depends('name', 'code') + def _display_name(self): + for this in self: + this.display_name = '%s. %s' % (this.code, this.name) + + _sql_constraints = [( + 'intrastat_transport_code_unique', + 'UNIQUE(code)', + 'Code must be unique.')] diff --git a/intrastat_product/models/intrastat_unit.py b/intrastat_product/models/intrastat_unit.py new file mode 100644 index 0000000..7122610 --- /dev/null +++ b/intrastat_product/models/intrastat_unit.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Intrastat Product module for Odoo +# Copyright (C) 2011-2015 Akretion (http://www.akretion.com) +# Copyright (C) 2009-2015 Noviat (http://www.noviat.com) +# @author Alexis de Lattre +# @author Luc de Meyer +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp import models, fields + + +class IntrastatUnit(models.Model): + _name = 'intrastat.unit' + _description = 'Intrastat Supplementary Units' + + name = fields.Char( + string='Name', required=True) + description = fields.Char( + string='Description', required=True) + uom_id = fields.Many2one( + 'product.uom', string='Regular UoM', + help="Select the regular Unit of Measure of Odoo that corresponds " + "to this Intrastat Supplementary Unit.") + active = fields.Boolean( + string='Active', default=True) diff --git a/intrastat_product/models/res_company.py b/intrastat_product/models/res_company.py new file mode 100644 index 0000000..2995486 --- /dev/null +++ b/intrastat_product/models/res_company.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Intrastat Product module for Odoo +# Copyright (C) 2011-2015 Akretion (http://www.akretion.com) +# Copyright (C) 2009-2015 Noviat (http://www.noviat.com) +# @author Alexis de Lattre +# @author Luc de Meyer +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp import models, fields, api + + +class ResCompany(models.Model): + _inherit = 'res.company' + + intrastat_incoterm_id = fields.Many2one( + 'stock.incoterms', + string='Default Incoterm for Intrastat', + help="International Commercial Terms are a series of " + "predefined commercial terms used in international " + "transactions.") + intrastat_arrivals = fields.Selection( + '_intrastat_arrivals', string='Arrivals', + default='extended', required=True) + intrastat_dispatches = fields.Selection( + '_intrastat_arrivals', string='Dispatches', + default='extended', required=True) + intrastat_transport_id = fields.Many2one( + 'intrastat.transport_mode', + string='Default Transport Mode', ondelete='restrict') + intrastat = fields.Char( + string='Intrastat Declaration', store=True, readonly=True, + compute='_compute_intrastat') + intrastat_region_id = fields.Many2one( + 'intrastat.region', + string='Default Intrastat Region') + intrastat_transaction_out_invoice = fields.Many2one( + 'intrastat.transaction', + string='Default Intrastat Transaction For Customer Invoice') + intrastat_transaction_out_refund = fields.Many2one( + 'intrastat.transaction', + string='Default Intrastat Transaction for Customer Refunds') + intrastat_transaction_in_invoice = fields.Many2one( + 'intrastat.transaction', + string='Default Intrastat Transaction For Supplier Invoices') + intrastat_transaction_in_refund = fields.Many2one( + 'intrastat.transaction', + string='Default Intrastat Transaction For Supplier Refunds') + intrastat_accessory_costs = fields.Boolean( + string='Include Accessory Costs in Fiscal Value of Product') + + @api.model + def _intrastat_arrivals(self): + return [ + ('exempt', 'Exempt'), + ('standard', 'Standard'), + ('extended', 'Extended')] + + @api.model + def _intrastat_dispatches(self): + return [ + ('exempt', 'Exempt'), + ('standard', 'Standard'), + ('extended', 'Extended')] + + @api.multi + @api.depends('intrastat_arrivals', 'intrastat_dispatches') + def _compute_intrastat(self): + for this in self: + if this.intrastat_arrivals == 'exempt' \ + and this.intrastat_dispatches == 'exempt': + this.intrastat = 'exempt' + elif this.intrastat_arrivals == 'extended' \ + or this.intrastat_dispatches == 'extended': + this.intrastat = 'extended' + else: + this.intrastat = 'standard' diff --git a/intrastat_product/models/sale_order.py b/intrastat_product/models/sale_order.py new file mode 100644 index 0000000..0818460 --- /dev/null +++ b/intrastat_product/models/sale_order.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Intrastat Product module for Odoo +# Copyright (C) 2010-2015 Akretion (http://www.akretion.com) +# @author Alexis de Lattre +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp import models, api + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + @api.model + def _prepare_invoice(self, order, lines): + '''Copy destination country to invoice''' + invoice_vals = super(SaleOrder, self)._prepare_invoice( + order, lines) + invoice_vals['src_dest_country_id'] = \ + order.partner_shipping_id.country_id.id or False + invoice_vals['incoterm_id'] = order.incoterm.id or False + return invoice_vals diff --git a/intrastat_product/models/stock_picking.py b/intrastat_product/models/stock_picking.py new file mode 100644 index 0000000..f46705f --- /dev/null +++ b/intrastat_product/models/stock_picking.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Intrastat Product module for Odoo +# Copyright (C) 2011-2015 Akretion (http://www.akretion.com) +# Copyright (C) 2009-2015 Noviat (http://www.noviat.com) +# @author Alexis de Lattre +# @author Luc de Meyer +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp import models, fields, api + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + intrastat_transport_id = fields.Many2one( + 'intrastat.transport_mode', string='Transport Mode', + help="This information is used in Intrastat reports") + intrastat = fields.Char(related='company_id.intrastat') + + @api.model + def _create_invoice_from_picking(self, picking, vals): + """ Copy data from picking to invoice. """ + vals['intrastat_transport_id'] = picking.intrastat_transport_id.id + if picking.partner_id and picking.partner_id.country_id: + vals['src_dest_country_id'] = picking.partner_id.country_id.id + region = picking.location_id.get_intrastat_region() + if region: + vals['src_dest_region_id'] = region.id + return super(StockPicking, self)._create_invoice_from_picking( + picking, vals) diff --git a/intrastat_product/models/stock_warehouse.py b/intrastat_product/models/stock_warehouse.py new file mode 100644 index 0000000..90509b8 --- /dev/null +++ b/intrastat_product/models/stock_warehouse.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Odoo, Open Source Management Solution +# +# Copyright (c) 2009-2015 Noviat nv/sa (www.noviat.com). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp import models, fields, api + + +class StockWarehouse(models.Model): + _inherit = 'stock.warehouse' + + region_id = fields.Many2one( + 'intrastat.region', + string='Intrastat region') + + +class StockLocation(models.Model): + _inherit = 'stock.location' + + @api.multi + def get_intrastat_region(self): + self.ensure_one() + locations = self.search( + [('parent_left', '<=', self.parent_left), + ('parent_right', '>=', self.parent_right)]) + warehouses = self.env['stock.warehouse'].search([ + ('lot_stock_id', 'in', [x.id for x in locations]), + ('region_id', '!=', False)]) + if warehouses: + return warehouses[0].region_id + return None diff --git a/intrastat_product/security/intrastat_security.xml b/intrastat_product/security/intrastat_security.xml new file mode 100644 index 0000000..e483225 --- /dev/null +++ b/intrastat_product/security/intrastat_security.xml @@ -0,0 +1,18 @@ + + + + + + Intrastat Transaction Company rule + + ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])] + + + + Intrastat Region Company rule + + ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])] + + + + diff --git a/intrastat_product/security/ir.model.access.csv b/intrastat_product/security/ir.model.access.csv new file mode 100644 index 0000000..bd1ca7c --- /dev/null +++ b/intrastat_product/security/ir.model.access.csv @@ -0,0 +1,13 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_intrastat_unit_read,Read access on Intrastat Supplementary Units to everybody,model_intrastat_unit,,1,0,0,0 +access_intrastat_unit_full,Full access on Intrastat Supplementary Units to Finance manager,model_intrastat_unit,account.group_account_manager,1,1,1,1 +access_intrastat_transaction_read,Read access on Intrastat Transaction Types to everybody,model_intrastat_transaction,,1,0,0,0 +access_intrastat_transaction_full,Full access on Intrastat Transaction Types to Finance manager,model_intrastat_transaction,account.group_account_manager,1,1,1,1 +access_intrastat_transport_mode_read,Read access on Intrastat Transport Modes to everybody,model_intrastat_transport_mode,,1,0,0,0 +access_intrastat_transport_mode_full,Full access on Intrastat Transport Modes to Finance manager,model_intrastat_transport_mode,account.group_account_manager,1,1,1,1 +access_intrastat_region_read,Read access on Intrastat Regions,model_intrastat_region,,1,0,0,0 +access_intrastat_region_full,Full access on Intrastat Regions,model_intrastat_region,account.group_account_manager,1,1,1,1 +access_hs_code_financial_mgr_full,Full access on H.S. Code to financial mgr,product_harmonized_system.model_hs_code,account.group_account_manager,1,1,1,1 +access_intrastat_product_declaration,Full access on Intrastat Product Declarations to Accountant,model_intrastat_product_declaration,account.group_account_user,1,1,1,1 +access_intrastat_product_computation_line,Full access on Intrastat Product Computation Lines to Accountant,model_intrastat_product_computation_line,account.group_account_user,1,1,1,1 +access_intrastat_product_declaration_line,Full access on Intrastat Product Declaration Lines to Accountant,model_intrastat_product_declaration_line,account.group_account_user,1,1,1,1 diff --git a/intrastat_product/views/account_invoice.xml b/intrastat_product/views/account_invoice.xml new file mode 100644 index 0000000..26332cd --- /dev/null +++ b/intrastat_product/views/account_invoice.xml @@ -0,0 +1,54 @@ + + + + + + intrastat.invoice.form + account.invoice + + + + + + + + + + + + + + + + + + + + intrastat.invoice.supplier.form + account.invoice + + + + + + + + + + + + + + + + + + + + diff --git a/intrastat_product/views/hs_code.xml b/intrastat_product/views/hs_code.xml new file mode 100644 index 0000000..f6e54f2 --- /dev/null +++ b/intrastat_product/views/hs_code.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + intrastat.hs.code.tree + hs.code + + + + + + + + + + + intrastat.hs.code.form + hs.code + + + + + + + + + + diff --git a/intrastat_product/views/intrastat_product_declaration.xml b/intrastat_product/views/intrastat_product_declaration.xml new file mode 100644 index 0000000..7ddc9bd --- /dev/null +++ b/intrastat_product/views/intrastat_product_declaration.xml @@ -0,0 +1,263 @@ + + + + + + intrastat.product.declaration.form + intrastat.product.declaration + +
+
+
+ +
+

+

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+
+ + + + Intrastat Product Declaration Validated + intrastat.product.declaration + + Intrastat Product Declaration Validated + + + + intrastat.product.declaration.tree + intrastat.product.declaration + + + + + + + + + + + + + + + intrastat.product.declaration.search + intrastat.product.declaration + + + + + + + + + + + + + + + + intrastat.product.declaration.graph + intrastat.product.declaration + + + + + + + + + + + + + intrastat.product.computation.line.form + intrastat.product.computation.line + +
+ + + + + + + + + + + + + +
+
+
+ + + intrastat.product.computation.line.tree + intrastat.product.computation.line + + + + + + + + + + + + + + + + + + + + + + + intrastat.product.declaration.line.form + intrastat.product.declaration.line + +
+ + + + + + + + + + + +
+
+
+ + + intrastat.product.declaration.line.tree + intrastat.product.declaration.line + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/intrastat_product/views/intrastat_region.xml b/intrastat_product/views/intrastat_region.xml new file mode 100644 index 0000000..e4580df --- /dev/null +++ b/intrastat_product/views/intrastat_region.xml @@ -0,0 +1,46 @@ + + + + + + intrastat.region.form + intrastat.region + +
+ + + + + + + +
+
+
+ + + intrastat.region.tree + intrastat.region + + + + + + + + + + + + + Intrastat Regions + intrastat.region + tree,form + + + + +
+
diff --git a/intrastat_product/views/intrastat_transaction.xml b/intrastat_product/views/intrastat_transaction.xml new file mode 100644 index 0000000..7f80b6b --- /dev/null +++ b/intrastat_product/views/intrastat_transaction.xml @@ -0,0 +1,68 @@ + + + + + + + + + + intrastat.transaction_form + intrastat.transaction + +
+ + + + + +
+
+
+ + + intrastat.transaction_tree + intrastat.transaction + + + + + + + + + + + intrastat.transaction.mode.search + intrastat.transaction + + + + + + + + + + + + Transaction Types + intrastat.transaction + tree,form + + + + +
+
diff --git a/intrastat_product/views/intrastat_transport_mode.xml b/intrastat_product/views/intrastat_transport_mode.xml new file mode 100644 index 0000000..94d51be --- /dev/null +++ b/intrastat_product/views/intrastat_transport_mode.xml @@ -0,0 +1,64 @@ + + + + + + + + + + intrastat.transport.mode.form + intrastat.transport_mode + +
+ + + + + +
+
+
+ + + intrastat.transport.mode.tree + intrastat.transport_mode + + + + + + + + + + + intrastat.transport.mode.search + intrastat.transport_mode + + + + + + + + + Transport Modes + intrastat.transport_mode + tree,form + + + + +
+
diff --git a/intrastat_product/views/intrastat_unit.xml b/intrastat_product/views/intrastat_unit.xml new file mode 100644 index 0000000..2bfa2da --- /dev/null +++ b/intrastat_product/views/intrastat_unit.xml @@ -0,0 +1,70 @@ + + + + + + + + + + intrastat.unit.form + intrastat.unit + +
+ + + + + + +
+
+
+ + + intrastat.unit.tree + intrastat.unit + + + + + + + + + + + intrastat.unit.search + intrastat.unit + + + + + + + + + + + + + Supplementary Units + intrastat.unit + tree,form + + + + +
+
diff --git a/intrastat_product/views/res_company.xml b/intrastat_product/views/res_company.xml new file mode 100644 index 0000000..2d61383 --- /dev/null +++ b/intrastat_product/views/res_company.xml @@ -0,0 +1,28 @@ + + + + + + intrastat.company.form + res.company + + + + + + + + + + + + + + + + + + + + diff --git a/intrastat_product/views/stock_picking.xml b/intrastat_product/views/stock_picking.xml new file mode 100644 index 0000000..80ce61e --- /dev/null +++ b/intrastat_product/views/stock_picking.xml @@ -0,0 +1,28 @@ + + + + + + + + + + intrastat.product.picking.form + stock.picking + + + + + + + + + + + diff --git a/intrastat_product/views/stock_warehouse.xml b/intrastat_product/views/stock_warehouse.xml new file mode 100644 index 0000000..163cf0f --- /dev/null +++ b/intrastat_product/views/stock_warehouse.xml @@ -0,0 +1,17 @@ + + + + + + intrastat.stock.warehouse.form + stock.warehouse + + + + + + + + + + diff --git a/oca_dependencies.txt b/oca_dependencies.txt index ac0117d..ec25d0d 100644 --- a/oca_dependencies.txt +++ b/oca_dependencies.txt @@ -13,3 +13,4 @@ # # To provide both the URL and a branch, use: # sale-workflow https://github.com/OCA/sale-workflow branchname +stock-logistics-workflow diff --git a/product_harmonized_system/README.rst b/product_harmonized_system/README.rst new file mode 100644 index 0000000..76168dd --- /dev/null +++ b/product_harmonized_system/README.rst @@ -0,0 +1,68 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + + +============================================ +Harmonized System Codes (and National Codes) +============================================ + +This module contains the objects for Harmonised System Codes (H.S. codes). The full nomenclature is available from the `World Customs Organisation `. These code are usually required on the Proforma invoices that are attached to the packages that are shipped abroad. + +This module also handle the local/national extensions to the H.S. codes. The import of the full nomenclature is not provided by this module ; it should be provided by localization modules. + +You will also be able to configure the country of origin of a product, which is often required on the proforma invoice for the customs. + +This module should be usefull for all companies that export physical goods abroad. This module is also used by the Intrastat modules for the European Union, cf the *intrastat_product* module. + + +Installation +============ + +This module is NOT compatible with the *report_intrastat* module from the official addons. + + +Usage +===== + +To create H.S. codes, go to the menu *Sales > Configuration > Product Categories and Attributes > H.S. Codes*. + +Then you will be able to set the H.S. code on an product (under the *Information* tab) or on a product category. On the product form, you will also be able to set the *Country of Origin* of a product (for example, if the product is *made in China*, select *China* as *Country of Origin*). + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/227/10.0 + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case +of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and +welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Alexis de Lattre, Akretion +* Luc De Meyer, Noviat + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/product_harmonized_system/__init__.py b/product_harmonized_system/__init__.py new file mode 100644 index 0000000..cde864b --- /dev/null +++ b/product_harmonized_system/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models diff --git a/product_harmonized_system/__manifest__.py b/product_harmonized_system/__manifest__.py new file mode 100644 index 0000000..3935d33 --- /dev/null +++ b/product_harmonized_system/__manifest__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# © 2011-2016 Akretion (http://www.akretion.com) +# © 2009-2016 Noviat (http://www.noviat.com) +# @author Alexis de Lattre +# @author Luc de Meyer +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Product Harmonized System Codes', + 'version': '10.0.1.0.0', + 'category': 'Reporting', + 'license': 'AGPL-3', + 'summary': 'Base module for Product Import/Export reports', + 'author': 'Akretion, Noviat, Odoo Community Association (OCA)', + 'depends': ['product'], + 'conflicts': ['report_intrastat'], + 'data': [ + 'security/product_hs_security.xml', + 'security/ir.model.access.csv', + 'views/hs_code.xml', + 'views/product_category.xml', + 'views/product_template.xml', + ], + 'demo': [ + 'demo/product_demo.xml', + ], + 'installable': True, +} diff --git a/product_harmonized_system/demo/product_demo.xml b/product_harmonized_system/demo/product_demo.xml new file mode 100644 index 0000000..dbc475f --- /dev/null +++ b/product_harmonized_system/demo/product_demo.xml @@ -0,0 +1,81 @@ + + + + + + + + 84715000 + Automatic data-processing machines (computers) + + + + 84717050 + Storage units + + + + 85340090 + Printed circuits + + + + + + 8.7 + + + + + + 1.1 + + + + + + 8.2 + + + + + + 0.01 + + + + + + 0.67 + + + + + + 0.75 + + + + + + 1.05 + + + + + + 0.3 + + + + + + 3.3 + + + + diff --git a/product_harmonized_system/i18n/fr.po b/product_harmonized_system/i18n/fr.po new file mode 100644 index 0000000..f8c57a1 --- /dev/null +++ b/product_harmonized_system/i18n/fr.po @@ -0,0 +1,156 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_harmonized_system +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-07-16 13:11+0000\n" +"PO-Revision-Date: 2015-07-16 13:11+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: product_harmonized_system +#: field:hs.code,active:0 +msgid "Active" +msgstr "Actif" + +#. module: product_harmonized_system +#: model:hs.code,description:product_harmonized_system.84715000 +msgid "Automatic data-processing machines (computers)" +msgstr "" + +#. module: product_harmonized_system +#: help:hs.code,local_code:0 +msgid "Code used for the national Import/Export declaration. e.g. Intrastat for the European Union" +msgstr "Code utilisé pour la déclaration nationale d'import/export, par exemple la DEB pour la France" + +#. module: product_harmonized_system +#: field:hs.code,company_id:0 +msgid "Company" +msgstr "Société" + +#. module: product_harmonized_system +#: field:product.template,origin_country_id:0 +msgid "Country of Origin" +msgstr "Pays d'origine" + +#. module: product_harmonized_system +#: help:product.template,origin_country_id:0 +msgid "Country of origin of the product i.e. product 'made in ____'. If you have different countries of origin depending on the supplier from which you purchased the product, leave this field empty and use the equivalent field on the 'product supplier info' form." +msgstr "" + +#. module: product_harmonized_system +#: field:hs.code,create_uid:0 +msgid "Created by" +msgstr "Créé par" + +#. module: product_harmonized_system +#: field:hs.code,create_date:0 +msgid "Created on" +msgstr "Créé le" + +#. module: product_harmonized_system +#: field:hs.code,description:0 +msgid "Description" +msgstr "Description" + +#. module: product_harmonized_system +#: field:hs.code,display_name:0 +msgid "Display Name" +msgstr "Nom affiché" + +#. module: product_harmonized_system +#: model:ir.model,name:product_harmonized_system.model_hs_code +#: field:product.category,hs_code_id:0 +#: field:product.template,hs_code_id:0 +msgid "H.S. Code" +msgstr "Code S.H." + +#. module: product_harmonized_system +#: view:hs.code:product_harmonized_system.hs_code_form +msgid "HS Code" +msgstr "Code S.H." + +#. module: product_harmonized_system +#: view:hs.code:product_harmonized_system.hs_code_tree +#: model:ir.actions.act_window,name:product_harmonized_system.hs_code_act +#: model:ir.ui.menu,name:product_harmonized_system.hs_code_menu +msgid "HS Codes" +msgstr "Codes S.H." + +#. module: product_harmonized_system +#: help:product.category,hs_code_id:0 +msgid "Harmonised System Code. If this code is not set on the product itself, it will be read here, on the related product category." +msgstr "" + +#. module: product_harmonized_system +#: help:product.template,hs_code_id:0 +msgid "Harmonised System Code. Nomenclature is available from the World Customs Organisation, see http://www.wcoomd.org/. You can leave this field empty and configure the H.S. code on the product category." +msgstr "" + +#. module: product_harmonized_system +#: field:hs.code,id:0 +msgid "ID" +msgstr "ID" + +#. module: product_harmonized_system +#: view:product.category:product_harmonized_system.product_category_form_view +msgid "Import/Export Properties" +msgstr "Propriétés pour l'import/export" + +#. module: product_harmonized_system +#: field:hs.code,write_uid:0 +msgid "Last Updated by" +msgstr "Dernière modification par" + +#. module: product_harmonized_system +#: field:hs.code,write_date:0 +msgid "Last Updated on" +msgstr "Dernière modification le" + +#. module: product_harmonized_system +#: field:hs.code,local_code:0 +msgid "Local Code" +msgstr "Code local" + +#. module: product_harmonized_system +#: model:hs.code,description:product_harmonized_system.85340090 +msgid "Printed circuits" +msgstr "" + +#. module: product_harmonized_system +#: model:ir.model,name:product_harmonized_system.model_product_category +msgid "Product Category" +msgstr "Catégorie d'articles" + +#. module: product_harmonized_system +#: model:ir.model,name:product_harmonized_system.model_product_template +msgid "Product Template" +msgstr "Modèle d'article" + +#. module: product_harmonized_system +#: view:hs.code:product_harmonized_system.hs_code_search +msgid "Search HS Codes" +msgstr "Recherche dans les codes S.H." + +#. module: product_harmonized_system +#: help:hs.code,description:0 +msgid "Short text description of the H.S. category" +msgstr "Courte description de la catégorie H.S." + +#. module: product_harmonized_system +#: model:hs.code,description:product_harmonized_system.84717050 +msgid "Storage units" +msgstr "" + +#. module: product_harmonized_system +#: sql_constraint:hs.code:0 +msgid "This code already exists for this company !" +msgstr "Ce code existe déjà pour cette société !" + diff --git a/product_harmonized_system/i18n/product_harmonized_system.pot b/product_harmonized_system/i18n/product_harmonized_system.pot new file mode 100644 index 0000000..b1389e9 --- /dev/null +++ b/product_harmonized_system/i18n/product_harmonized_system.pot @@ -0,0 +1,156 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_harmonized_system +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-07-16 13:10+0000\n" +"PO-Revision-Date: 2015-07-16 13:10+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: product_harmonized_system +#: field:hs.code,active:0 +msgid "Active" +msgstr "" + +#. module: product_harmonized_system +#: model:hs.code,description:product_harmonized_system.84715000 +msgid "Automatic data-processing machines (computers)" +msgstr "" + +#. module: product_harmonized_system +#: help:hs.code,local_code:0 +msgid "Code used for the national Import/Export declaration. e.g. Intrastat for the European Union" +msgstr "" + +#. module: product_harmonized_system +#: field:hs.code,company_id:0 +msgid "Company" +msgstr "" + +#. module: product_harmonized_system +#: field:product.template,origin_country_id:0 +msgid "Country of Origin" +msgstr "" + +#. module: product_harmonized_system +#: help:product.template,origin_country_id:0 +msgid "Country of origin of the product i.e. product 'made in ____'. If you have different countries of origin depending on the supplier from which you purchased the product, leave this field empty and use the equivalent field on the 'product supplier info' form." +msgstr "" + +#. module: product_harmonized_system +#: field:hs.code,create_uid:0 +msgid "Created by" +msgstr "" + +#. module: product_harmonized_system +#: field:hs.code,create_date:0 +msgid "Created on" +msgstr "" + +#. module: product_harmonized_system +#: field:hs.code,description:0 +msgid "Description" +msgstr "" + +#. module: product_harmonized_system +#: field:hs.code,display_name:0 +msgid "Display Name" +msgstr "" + +#. module: product_harmonized_system +#: model:ir.model,name:product_harmonized_system.model_hs_code +#: field:product.category,hs_code_id:0 +#: field:product.template,hs_code_id:0 +msgid "H.S. Code" +msgstr "" + +#. module: product_harmonized_system +#: view:hs.code:product_harmonized_system.hs_code_form +msgid "HS Code" +msgstr "" + +#. module: product_harmonized_system +#: view:hs.code:product_harmonized_system.hs_code_tree +#: model:ir.actions.act_window,name:product_harmonized_system.hs_code_act +#: model:ir.ui.menu,name:product_harmonized_system.hs_code_menu +msgid "HS Codes" +msgstr "" + +#. module: product_harmonized_system +#: help:product.category,hs_code_id:0 +msgid "Harmonised System Code. If this code is not set on the product itself, it will be read here, on the related product category." +msgstr "" + +#. module: product_harmonized_system +#: help:product.template,hs_code_id:0 +msgid "Harmonised System Code. Nomenclature is available from the World Customs Organisation, see http://www.wcoomd.org/. You can leave this field empty and configure the H.S. code on the product category." +msgstr "" + +#. module: product_harmonized_system +#: field:hs.code,id:0 +msgid "ID" +msgstr "" + +#. module: product_harmonized_system +#: view:product.category:product_harmonized_system.product_category_form_view +msgid "Import/Export Properties" +msgstr "" + +#. module: product_harmonized_system +#: field:hs.code,write_uid:0 +msgid "Last Updated by" +msgstr "" + +#. module: product_harmonized_system +#: field:hs.code,write_date:0 +msgid "Last Updated on" +msgstr "" + +#. module: product_harmonized_system +#: field:hs.code,local_code:0 +msgid "Local Code" +msgstr "" + +#. module: product_harmonized_system +#: model:hs.code,description:product_harmonized_system.85340090 +msgid "Printed circuits" +msgstr "" + +#. module: product_harmonized_system +#: model:ir.model,name:product_harmonized_system.model_product_category +msgid "Product Category" +msgstr "" + +#. module: product_harmonized_system +#: model:ir.model,name:product_harmonized_system.model_product_template +msgid "Product Template" +msgstr "" + +#. module: product_harmonized_system +#: view:hs.code:product_harmonized_system.hs_code_search +msgid "Search HS Codes" +msgstr "" + +#. module: product_harmonized_system +#: help:hs.code,description:0 +msgid "Short text description of the H.S. category" +msgstr "" + +#. module: product_harmonized_system +#: model:hs.code,description:product_harmonized_system.84717050 +msgid "Storage units" +msgstr "" + +#. module: product_harmonized_system +#: sql_constraint:hs.code:0 +msgid "This code already exists for this company !" +msgstr "" + diff --git a/product_harmonized_system/models/__init__.py b/product_harmonized_system/models/__init__.py new file mode 100644 index 0000000..2d9638e --- /dev/null +++ b/product_harmonized_system/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import hs_code +from . import product_category +from . import product_template diff --git a/product_harmonized_system/models/hs_code.py b/product_harmonized_system/models/hs_code.py new file mode 100644 index 0000000..580c957 --- /dev/null +++ b/product_harmonized_system/models/hs_code.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# © 2011-2016 Akretion (http://www.akretion.com) +# © 2009-2016 Noviat (http://www.noviat.com) +# @author Alexis de Lattre +# @author Luc de Meyer +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields, api + + +class HSCode(models.Model): + _name = "hs.code" + _description = "H.S. Code" + _order = "local_code" + _rec_name = "display_name" + + hs_code = fields.Char( + string='H.S. Code', compute='_compute_hs_code', readonly=True, + help="Harmonized System code (6 digits). Full list is " + "available from the World Customs Organisation, see " + "http://www.wcoomd.org") + description = fields.Char( + 'Description', translate=True, + help="Short text description of the H.S. category") + display_name = fields.Char( + compute='_compute_display_name_field', string="Display Name", + store=True, readonly=True) + local_code = fields.Char( + string='Local Code', required=True, + help="Code used for the national Import/Export declaration. " + "The national code starts with the 6 digits of the H.S. and often " + "has a few additional digits to extend the H.S. code.") + active = fields.Boolean(default=True) + company_id = fields.Many2one( + 'res.company', string='Company', readonly=True, required=True, + default=lambda self: self.env['res.company']._company_default_get( + 'hs.code')) + product_categ_ids = fields.One2many( + 'product.category', 'hs_code_id', string='Product Categories') + product_tmpl_ids = fields.One2many( + 'product.template', 'hs_code_id', string='Products') + + @api.multi + @api.depends('local_code') + def _compute_hs_code(self): + for this in self: + this.hs_code = this.local_code and this.local_code[:6] + + @api.multi + @api.depends('local_code', 'description') + def _compute_display_name_field(self): + for this in self: + display_name = this.local_code + if this.description: + display_name += ' ' + this.description + this.display_name = len(display_name) > 55 \ + and display_name[:55] + '...' \ + or display_name + + _sql_constraints = [ + ('local_code_company_uniq', 'unique(local_code, company_id)', + 'This code already exists for this company !'), + ] + + @api.model + def create(self, vals): + if vals.get('local_code'): + vals['local_code'] = vals['local_code'].replace(' ', '') + return super(HSCode, self).create(vals) + + @api.multi + def write(self, vals): + if vals.get('local_code'): + vals['local_code'] = vals['local_code'].replace(' ', '') + return super(HSCode, self).write(vals) diff --git a/product_harmonized_system/models/product_category.py b/product_harmonized_system/models/product_category.py new file mode 100644 index 0000000..6f6233e --- /dev/null +++ b/product_harmonized_system/models/product_category.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# © 2011-2016 Akretion (http://www.akretion.com) +# © 2009-2016 Noviat (http://www.noviat.com) +# @author Alexis de Lattre +# @author Luc de Meyer +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields, api + + +class ProductCategory(models.Model): + _inherit = "product.category" + + hs_code_id = fields.Many2one( + 'hs.code', string='H.S. Code', + company_dependent=True, ondelete='restrict', + help="Harmonised System Code. If this code is not " + "set on the product itself, it will be read here, on the " + "related product category.") + + @api.multi + def get_hs_code_recursively(self): + self.ensure_one() + if self.hs_code_id: + res = self.hs_code_id + elif self.parent_id: + res = self.parent_id.get_hs_code_recursively() + else: + res = None + return res diff --git a/product_harmonized_system/models/product_template.py b/product_harmonized_system/models/product_template.py new file mode 100644 index 0000000..8893f39 --- /dev/null +++ b/product_harmonized_system/models/product_template.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# © 2011-2016 Akretion (http://www.akretion.com) +# © 2009-2016 Noviat (http://www.noviat.com) +# @author Alexis de Lattre +# @author Luc de Meyer +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields, api + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + hs_code_id = fields.Many2one( + 'hs.code', string='H.S. Code', + company_dependent=True, ondelete='restrict', + help="Harmonised System Code. Nomenclature is " + "available from the World Customs Organisation, see " + "http://www.wcoomd.org/. You can leave this field empty " + "and configure the H.S. code on the product category.") + origin_country_id = fields.Many2one( + 'res.country', string='Country of Origin', + help="Country of origin of the product i.e. product " + "'made in ____'.") + + @api.multi + def get_hs_code_recursively(self): + self.ensure_one() + if self.hs_code_id: + res = self.hs_code_id + elif self.categ_id: + res = self.categ_id.get_hs_code_recursively() + else: + res = None + return res diff --git a/product_harmonized_system/security/ir.model.access.csv b/product_harmonized_system/security/ir.model.access.csv new file mode 100644 index 0000000..e22ebdd --- /dev/null +++ b/product_harmonized_system/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_hs_code_group_system,Full access on hs.code to Settings group,model_hs_code,base.group_system,1,1,1,1 +access_hs_code_read,Read access on hs.code to everybody,model_hs_code,,1,0,0,0 diff --git a/product_harmonized_system/security/product_hs_security.xml b/product_harmonized_system/security/product_hs_security.xml new file mode 100644 index 0000000..c7da6fb --- /dev/null +++ b/product_harmonized_system/security/product_hs_security.xml @@ -0,0 +1,10 @@ + + + + + HS Code Company rule + + ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])] + + + diff --git a/product_harmonized_system/views/hs_code.xml b/product_harmonized_system/views/hs_code.xml new file mode 100644 index 0000000..fac22af --- /dev/null +++ b/product_harmonized_system/views/hs_code.xml @@ -0,0 +1,72 @@ + + + + + + + + hs.code.search + hs.code + + + + + + + + + + hs.code.tree + hs.code + + + + + + + + + + + + + hs.code.form + hs.code + +
+ + + + + + + + + + + + + +
+
+
+ + + + H.S. Codes + hs.code + tree,form + + + + + +
diff --git a/product_harmonized_system/views/product_category.xml b/product_harmonized_system/views/product_category.xml new file mode 100644 index 0000000..ac75e13 --- /dev/null +++ b/product_harmonized_system/views/product_category.xml @@ -0,0 +1,24 @@ + + + + + + + + hs_code.product.category.form + product.category + + + + + + + + + + + diff --git a/product_harmonized_system/views/product_template.xml b/product_harmonized_system/views/product_template.xml new file mode 100644 index 0000000..cc0ebcb --- /dev/null +++ b/product_harmonized_system/views/product_template.xml @@ -0,0 +1,23 @@ + + + + + + + + hs_code.product.template.form + product.template + + + + + + + + + +