From b0acb42f0ba18b8cf50377baf5509880d480a499 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Tue, 12 Dec 2017 10:55:18 +0100 Subject: [PATCH 01/12] [FIX] stock: avoid infinite or longer than necessary loops In some cases the MRP scheduler can remain blocked on an infinite loop consuming a lot of CPU. The reason comes from the loop 'Check done procurements' If a new procument is created from another process (such like MTO orders) during this loop, the process never ends. The only way to reproduce is in debug mode: 1. Put a breakpoint on line 'Check done procurements' 2. Launch the MRP scheduler manually and stay paused on the breakpoint 3. In parrallel, create and confirm a sale order with a MTO route, this will trigger the creation of a new procurement order in state 'Running' 4. Continue debugging step by step the scheduler and you will notice that the loop never ends. Closes #21565 opw-787651 --- addons/procurement/models/procurement.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/addons/procurement/models/procurement.py b/addons/procurement/models/procurement.py index dd8d1fdeece..b00ace616df 100644 --- a/addons/procurement/models/procurement.py +++ b/addons/procurement/models/procurement.py @@ -282,19 +282,19 @@ class ProcurementOrder(models.Model): ProcurementSudo = self.env['procurement.order'].sudo() # Run confirmed procurements procurements = ProcurementSudo.search([('state', '=', 'confirmed')] + (company_id and [('company_id', '=', company_id)] or [])) + run_procurements = [] while procurements: procurements.run(autocommit=use_new_cursor) + run_procurements.extend(procurements.ids) if use_new_cursor: self.env.cr.commit() - procurements = ProcurementSudo.search([('id', 'not in', procurements.ids), ('state', '=', 'confirmed')] + (company_id and [('company_id', '=', company_id)] or [])) + procurements = ProcurementSudo.search([('id', 'not in', run_procurements), ('state', '=', 'confirmed')] + (company_id and [('company_id', '=', company_id)] or [])) # Check done procurements procurements = ProcurementSudo.search([('state', '=', 'running')] + (company_id and [('company_id', '=', company_id)] or [])) - while procurements: - procurements.check(autocommit=use_new_cursor) - if use_new_cursor: - self.env.cr.commit() - procurements = ProcurementSudo.search([('id', 'not in', procurements.ids), ('state', '=', 'running')] + (company_id and [('company_id', '=', company_id)] or [])) + procurements.check(autocommit=use_new_cursor) + if use_new_cursor: + self.env.cr.commit() finally: if use_new_cursor: From 2a9e6e6a3b4b9593af96cf0495c8feaa970aaeb3 Mon Sep 17 00:00:00 2001 From: "Adrien Peiffer (ACSONE)" Date: Tue, 28 Nov 2017 15:37:56 +0100 Subject: [PATCH 02/12] [FIX] res_partner: bad sudo. Using sudo on super call can turn into infinite call in odoo. opw:785415 --- odoo/addons/base/res/res_partner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odoo/addons/base/res/res_partner.py b/odoo/addons/base/res/res_partner.py index 8e91a0b3ebd..9184a9ec68a 100644 --- a/odoo/addons/base/res/res_partner.py +++ b/odoo/addons/base/res/res_partner.py @@ -509,7 +509,7 @@ class Partner(models.Model, FormatAddress): result = True # To write in SUPERUSER on field is_company and avoid access rights problems. if 'is_company' in vals and self.user_has_groups('base.group_partner_manager') and not self.env.uid == SUPERUSER_ID: - result = super(Partner, self).sudo().write({'is_company': vals.get('is_company')}) + result = super(Partner, self.sudo()).write({'is_company': vals.get('is_company')}) del vals['is_company'] result = result and super(Partner, self).write(vals) for partner in self: From 30d0861f0a9c9079c3c2222ca6d1b2a2e33469c2 Mon Sep 17 00:00:00 2001 From: Wolfgang Taferner Date: Tue, 5 Dec 2017 09:35:42 +0100 Subject: [PATCH 03/12] [FIX] base: ir.sequence better fallback Zero is an non-acceptable value for sequences, so the fallback should be one instead. Fixes #21267 --- openerp/addons/base/ir/ir_sequence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_sequence.py b/openerp/addons/base/ir/ir_sequence.py index 25728399b19..61997e23348 100644 --- a/openerp/addons/base/ir/ir_sequence.py +++ b/openerp/addons/base/ir/ir_sequence.py @@ -105,7 +105,7 @@ class ir_sequence(models.Model): def _set_number_next_actual(self): for record in self: - record.write({'number_next': record.number_next_actual or 0}) + record.write({'number_next': record.number_next_actual or 1}) name = fields.Char(required=True) code = fields.Char('Sequence Code') From b3be0b75764d6a0c45097f72cf9d38a459969646 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Thu, 7 Dec 2017 17:13:05 +0100 Subject: [PATCH 04/12] [FIX] stock: don't propagate cancel when chained move is done When you have e.g. an SO with a product MTO generating a manufacturing order, and you put the delivery order to done, before the MO is done, you can not cancel the MO anymore, because the cancel method checks if the move is not done. As the user stays blocked in this case, it is better to just not try to cancel the destination move by not propagating the cancel when the destination move is done. opw-786571 --- addons/stock/models/stock_move.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index b9c52991a84..719bf08ea1a 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -669,7 +669,8 @@ class StockMove(models.Model): self.ensure_one() if self.move_dest_id: if self.propagate: - self.move_dest_id.action_cancel() + if self.move_dest_id.state not in ('done', 'cancel'): + self.move_dest_id.action_cancel() elif self.move_dest_id.state == 'waiting': # If waiting, the chain will be broken and we are not sure if we can still wait for it (=> could take from stock instead) self.move_dest_id.write({'state': 'confirmed'}) From 22f8660e2211fd84f84f530cfed29a594a228a02 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Mon, 11 Dec 2017 16:47:53 +0100 Subject: [PATCH 05/12] [FIX] stock: don't propagate cancel when chained move is done When you have e.g. an SO with a product MTO generating a manufacturing order, and you put the delivery order to done, before the MO is done, you can not cancel the MO anymore, because the cancel method checks if the move is not done (and raises an error). As the user stays blocked in this case, it is better to just not try to cancel the destination move by not propagating the cancel when the destination move is done. opw-786571 --- addons/stock/stock.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/stock/stock.py b/addons/stock/stock.py index d95e34eca3b..45fe1e380d9 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -2532,7 +2532,8 @@ class stock_move(osv.osv): else: if move.move_dest_id: if move.propagate: - self.action_cancel(cr, uid, [move.move_dest_id.id], context=context) + if move.move_dest_id.state not in ('done', 'cancel'): + self.action_cancel(cr, uid, [move.move_dest_id.id], context=context) elif move.move_dest_id.state == 'waiting': #If waiting, the chain will be broken and we are not sure if we can still wait for it (=> could take from stock instead) self.write(cr, uid, [move.move_dest_id.id], {'state': 'confirmed'}, context=context) From d847a664157738fe896dc5636977489f2da2a7ff Mon Sep 17 00:00:00 2001 From: auyeung112002 Date: Tue, 28 Nov 2017 12:05:29 +0800 Subject: [PATCH 06/12] [FIX] web: Make search view dropdown menu scrollable --- addons/web/static/src/less/search_view.less | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/addons/web/static/src/less/search_view.less b/addons/web/static/src/less/search_view.less index a34905cb929..c50cd58be83 100644 --- a/addons/web/static/src/less/search_view.less +++ b/addons/web/static/src/less/search_view.less @@ -139,6 +139,9 @@ // Filters .o_filters_menu { .o-selected-li; + + max-height: ~"calc(100vh - 125px)"; + overflow: auto; .o_filter_condition { .o-search-options-dropdown-custom-li; @@ -175,6 +178,9 @@ // Group By .o_group_by_menu { .o-selected-li; + + max-height: ~"calc(100vh - 125px)"; + overflow: auto; .divider {display: none;} // hide the divider by default (no group) .o_add_group { // add button @@ -186,6 +192,9 @@ // Favorites .o_favorites_menu { .o-selected-li; + + max-height: ~"calc(100vh - 125px)"; + overflow: auto; .divider {display: none;} // hide the divider by default (no favorite) .o-searchview-custom-private, .o-searchview-custom-public { From b92aaf1e660d67c6b02974c9f059ca9026168e73 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Tue, 12 Dec 2017 16:20:02 +0100 Subject: [PATCH 07/12] [FIX] mail: display email of partner in the tags --- addons/mail/static/src/js/many2many_tags_email.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/addons/mail/static/src/js/many2many_tags_email.js b/addons/mail/static/src/js/many2many_tags_email.js index 4b273b456e1..dc1a405444c 100644 --- a/addons/mail/static/src/js/many2many_tags_email.js +++ b/addons/mail/static/src/js/many2many_tags_email.js @@ -82,6 +82,21 @@ var FieldMany2ManyTagsEmail = form_relational.FieldMany2ManyTags.extend({ }); }); }, + + /** + * Override for res.partner + * name_get is dynamic (based on context) while display_name is static + * (stored) + */ + get_render_data: function(ids){ + this.dataset.cancel_read(); + return this.dataset.name_get(ids).then(function (names) { + return _.map(names, function(name) { + return {id: name[0], display_name: name[1]}; + }); + }); + }, + }); From 1022e4fb25c71c7b653f81912d2afb2214a4f279 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Wed, 13 Dec 2017 15:17:22 +0100 Subject: [PATCH 08/12] [MERGE] forward port branch 9.0 up to 22f8660e221 Works better when you add files... --- odoo/addons/base/ir/ir_sequence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/odoo/addons/base/ir/ir_sequence.py b/odoo/addons/base/ir/ir_sequence.py index 5431a33eac2..140ff7e01bd 100644 --- a/odoo/addons/base/ir/ir_sequence.py +++ b/odoo/addons/base/ir/ir_sequence.py @@ -96,7 +96,7 @@ class IrSequence(models.Model): def _set_number_next_actual(self): for seq in self: - seq.write({'number_next': seq.number_next_actual or 0}) + seq.write({'number_next': seq.number_next_actual or 1}) name = fields.Char(required=True) code = fields.Char(string='Sequence Code') @@ -309,7 +309,7 @@ class IrSequenceDateRange(models.Model): def _set_number_next_actual(self): for seq in self: - seq.write({'number_next': seq.number_next_actual or 0}) + seq.write({'number_next': seq.number_next_actual or 1}) date_from = fields.Date(string='From', required=True) date_to = fields.Date(string='To', required=True) From 3a13eada0ee522b8ac919bbac424519dcf639ac1 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Wed, 13 Dec 2017 17:06:57 +0100 Subject: [PATCH 09/12] [FIX] web: readonly required text fields should not be highlighted The bug is also present in enterprise but the required style of enterprise makes the bug invisible. The bug has also been naturally fixed in 11.0 thanks to the views and design refactoring. Thanks @auyeung112002 for the original fix. --- addons/web/static/src/less/form_view_extra.less | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/less/form_view_extra.less b/addons/web/static/src/less/form_view_extra.less index 300724e2819..3377634611a 100644 --- a/addons/web/static/src/less/form_view_extra.less +++ b/addons/web/static/src/less/form_view_extra.less @@ -187,11 +187,17 @@ } // All required form fields + .o-input-required-style() { + background-color: #D2D2FF; + } .o_form_required { - &.o_form_input, .o_form_input, &.o_form_textarea, .o_form_textarea { - background-color: #D2D2FF; + &.o_form_input, .o_form_input, textarea.o_form_textarea { + .o-input-required-style(); } } + textarea.o_form_textarea.o_form_required { + .o-input-required-style(); + } .o_form_field.o_form_field_many2one .o_external_button { padding: 0; From b75927f9e36283ac540970c067577171889c2733 Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Wed, 13 Dec 2017 14:08:14 +0100 Subject: [PATCH 10/12] [FIX] l10n_fr_certification: adapting to the new specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Main resource document to be found at https://www.economie.gouv.fr/files/files/directions_services/dgfip/controle_fiscal/actualites_reponses/logiciels_de_caisse.pdf This document states that only business operations with partners not being subject to VAT should be inalterable and secure. We adpat the module by differentiating between b2c and b2b journals and apply hashing on the former's moves Protection against account_cancel is not necessary anymore By default, this module considers a sales journal being a b2c, thus activating hashing on its move. Since we can't afford to lose information, when a b2c journal contains hashed moves, its b2c characteristic cannot be changed [ADD] l10n_fr_pos_cert: module to adapt the POS to the recent French regulation CGI art. 286, I. 3° bis. The module adds following features: - Inalterability: deactivation of all the ways to cancel or modify key data of POS orders, invoices and journal entries - Security: chaining algorithm to verify the inalterability - Access to download the mandatory Certificate of Conformity delivered by Odoo SA (only for Odoo Enterprise users) [ADD] l10n_fr_sale_closing: module to store automatically the total of daily/monthly/annualy sales. (both of the period and cumulative). This fulfills another requirement brought by the recent French regulation CGI art. 286, I. 3° bis related to the storage of data Was PR #20581. Was task 29922 --- addons/account/models/res_config.py | 2 - addons/l10n_fr_certification/__init__.py | 22 +- addons/l10n_fr_certification/__openerp__.py | 18 +- .../data/account_move.xml | 16 +- .../i18n/l10n_fr_certification.pot | 155 +++++++++++++ .../l10n_fr_certification/models/account.py | 89 +++++-- .../models/res_company.py | 51 ++-- .../static/description/icon.png | Bin 0 -> 562 bytes .../l10n_fr_certification/views/no_cancel.xml | 36 --- .../views/res_config.xml | 18 ++ addons/l10n_fr_pos_cert/__init__.py | 17 ++ addons/l10n_fr_pos_cert/__openerp__.py | 33 +++ .../data/pos_inalterability.xml | 17 ++ .../i18n/l10n_fr_pos_cert.pot | 166 +++++++++++++ addons/l10n_fr_pos_cert/models/__init__.py | 3 + .../models/account_bank_statement.py | 21 ++ addons/l10n_fr_pos_cert/models/pos.py | 194 ++++++++++++++++ addons/l10n_fr_pos_cert/models/res_company.py | 26 +++ .../static/description/icon.png | Bin 0 -> 562 bytes .../l10n_fr_pos_cert/views/account_views.xml | 26 +++ addons/l10n_fr_sale_closing/__init__.py | 14 ++ addons/l10n_fr_sale_closing/__openerp__.py | 28 +++ .../data/account_sale_closure_cron.xml | 35 +++ .../i18n/l10n_fr_sale_closing.pot | 219 ++++++++++++++++++ .../l10n_fr_sale_closing/models/__init__.py | 2 + .../models/account_closing.py | 158 +++++++++++++ .../models/res_company.py | 29 +++ .../security/account_closing_intercompany.xml | 8 + .../security/ir.model.access.csv | 2 + .../static/description/icon.png | Bin 0 -> 562 bytes .../views/account_sale_closure.xml | 68 ++++++ 31 files changed, 1373 insertions(+), 100 deletions(-) create mode 100644 addons/l10n_fr_certification/i18n/l10n_fr_certification.pot create mode 100644 addons/l10n_fr_certification/static/description/icon.png delete mode 100644 addons/l10n_fr_certification/views/no_cancel.xml create mode 100644 addons/l10n_fr_certification/views/res_config.xml create mode 100644 addons/l10n_fr_pos_cert/__init__.py create mode 100644 addons/l10n_fr_pos_cert/__openerp__.py create mode 100644 addons/l10n_fr_pos_cert/data/pos_inalterability.xml create mode 100644 addons/l10n_fr_pos_cert/i18n/l10n_fr_pos_cert.pot create mode 100644 addons/l10n_fr_pos_cert/models/__init__.py create mode 100644 addons/l10n_fr_pos_cert/models/account_bank_statement.py create mode 100644 addons/l10n_fr_pos_cert/models/pos.py create mode 100644 addons/l10n_fr_pos_cert/models/res_company.py create mode 100644 addons/l10n_fr_pos_cert/static/description/icon.png create mode 100644 addons/l10n_fr_pos_cert/views/account_views.xml create mode 100644 addons/l10n_fr_sale_closing/__init__.py create mode 100644 addons/l10n_fr_sale_closing/__openerp__.py create mode 100644 addons/l10n_fr_sale_closing/data/account_sale_closure_cron.xml create mode 100644 addons/l10n_fr_sale_closing/i18n/l10n_fr_sale_closing.pot create mode 100644 addons/l10n_fr_sale_closing/models/__init__.py create mode 100644 addons/l10n_fr_sale_closing/models/account_closing.py create mode 100644 addons/l10n_fr_sale_closing/models/res_company.py create mode 100644 addons/l10n_fr_sale_closing/security/account_closing_intercompany.xml create mode 100644 addons/l10n_fr_sale_closing/security/ir.model.access.csv create mode 100644 addons/l10n_fr_sale_closing/static/description/icon.png create mode 100644 addons/l10n_fr_sale_closing/views/account_sale_closure.xml diff --git a/addons/account/models/res_config.py b/addons/account/models/res_config.py index 1f1ba9e7434..070f4423b16 100644 --- a/addons/account/models/res_config.py +++ b/addons/account/models/res_config.py @@ -137,13 +137,11 @@ class AccountConfigSettings(models.TransientModel): help='Get your bank statements from your bank and import them in Odoo in the .OFX format.\n' 'This installs the module account_bank_statement_import_ofx.') - @api.model def _default_has_default_company(self): count = self.env['res.company'].search_count([]) return bool(count == 1) - @api.onchange('company_id') def onchange_company_id(self): # update related fields diff --git a/addons/l10n_fr_certification/__init__.py b/addons/l10n_fr_certification/__init__.py index 24644871f1e..60027beb24f 100644 --- a/addons/l10n_fr_certification/__init__.py +++ b/addons/l10n_fr_certification/__init__.py @@ -3,31 +3,19 @@ import models from openerp import api, SUPERUSER_ID +from models.res_company import UNALTERABLE_COUNTRIES + def _setup_inalterability(cr, registry): env = api.Environment(cr, SUPERUSER_ID, {}) # enable ping for this module env['publisher_warranty.contract'].update_notification(cron_mode=True) - # make sure account_cancel is not usable at the same time as l10n_fr - # FORWARD PORT NOTICE - # In master as of March 2017, RCO-ODOO coded an exclusive field on modules to flag incompatibility - - wanted_states = ['installed', 'to upgrade', 'to install'] - account_cancel_module = env['ir.module.module'].search([('name', '=', 'account_cancel')], limit=1) - - if account_cancel_module and account_cancel_module.state in wanted_states: - views_xml_id = env['ir.model.data'].search([('module', '=', 'account_cancel'), ('model', '=', 'ir.ui.view')]) - ir_views = env['ir.ui.view'].browse([v.res_id for v in views_xml_id]) - for cancel_view in ir_views: - cancel_view.write({'active': False}) - - fr_companies = env['res.company'].search([('partner_id.country_id.code', '=', 'FR')]) + fr_companies = env['res.company'].search([('partner_id.country_id.code', 'in', UNALTERABLE_COUNTRIES)]) if fr_companies: # create the securisation sequence per company - fr_companies._create_secure_sequence() + fr_companies._create_secure_sequence(['l10n_fr_secure_sequence_id']) #reset the update_posted field on journals journals = env['account.journal'].search([('company_id', 'in', fr_companies.ids)]) - for journal in journals: - journal.write({'update_posted': False}) + journals.write({'update_posted': False}) diff --git a/addons/l10n_fr_certification/__openerp__.py b/addons/l10n_fr_certification/__openerp__.py index 7e2ea327d54..426eabf66ef 100644 --- a/addons/l10n_fr_certification/__openerp__.py +++ b/addons/l10n_fr_certification/__openerp__.py @@ -2,18 +2,30 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. { - 'name': 'France - Accounting - Certified CGI 286 I-3 bis', + 'name': 'France - VAT Anti-Fraud Certification (CGI 286 I-3 bis)', 'version': '1.0', 'category': 'Localization', 'description': """ +This add-on brings the technical requirements of the French regulation CGI art. 286, I. 3° bis that stipulates certain criteria concerning the inalterability, security, storage and archiving of data related to sales to private individuals (B2C). +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +The module adds following features: + + Inalterability: deactivation of all the ways to cancel or modify key data, invoices and journal entries + + Security: chaining algorithm to verify the inalterability + + Storage: automatic sales closings with computation of both period and cumulative totals (daily, monthly, annually) + + Access to download the mandatory Certificate of Conformity delivered by Odoo SA (only for Odoo Enterprise users) """, 'depends': ['l10n_fr'], 'installable': True, 'auto_install': False, - 'application': True, + 'application': False, 'data': [ - 'views/no_cancel.xml', 'data/account_move.xml', + 'views/res_config.xml', ], 'post_init_hook': '_setup_inalterability', } diff --git a/addons/l10n_fr_certification/data/account_move.xml b/addons/l10n_fr_certification/data/account_move.xml index 95adc8d8c2e..2794e6364f9 100644 --- a/addons/l10n_fr_certification/data/account_move.xml +++ b/addons/l10n_fr_certification/data/account_move.xml @@ -1,6 +1,6 @@ - Intégrité des données comptables + Data Inalterability Check ir.actions.server @@ -8,6 +8,18 @@ - + + Get the certification now! + ir.actions.server + + +db_uuid = env['ir.config_parameter'].sudo().get_param('database.uuid') +action = {"type": "ir.actions.act_url", + "url": "https://www.odoo.com/my/contract/french-certification/db/" + db_uuid, + "target": "new",} + + + + diff --git a/addons/l10n_fr_certification/i18n/l10n_fr_certification.pot b/addons/l10n_fr_certification/i18n/l10n_fr_certification.pot new file mode 100644 index 00000000000..346bc685e8b --- /dev/null +++ b/addons/l10n_fr_certification/i18n/l10n_fr_certification.pot @@ -0,0 +1,155 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * l10n_fr_certification +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 9.0c\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-12-06 16:03+0000\n" +"PO-Revision-Date: 2017-12-06 16:03+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: l10n_fr_certification +#: code:addons/l10n_fr_certification/models/account.py:108 +#, python-format +msgid "(ref.: %s)" +msgstr "" + +#. module: l10n_fr_certification +#: code:addons/l10n_fr_certification/models/account.py:79 +#, python-format +msgid "According to the French law, you cannot modify a journal entry in order for its posted data to be updated or deleted. Unauthorized field: %s." +msgstr "" + +#. module: l10n_fr_certification +#: code:addons/l10n_fr_certification/models/account.py:165 +#: code:addons/l10n_fr_certification/models/account.py:185 +#: code:addons/l10n_fr_certification/models/account.py:194 +#, python-format +msgid "According to the French law, you cannot modify a journal in order for its posted data to be updated or deleted. Unauthorized field: %s." +msgstr "" + +#. module: l10n_fr_certification +#: code:addons/l10n_fr_certification/models/account.py:154 +#, python-format +msgid "According to the French law, you cannot modify a journal item in order for its posted data to be updated or deleted. Unauthorized field: %s." +msgstr "" + +#. module: l10n_fr_certification +#: model:ir.model,name:l10n_fr_certification.model_account_move +msgid "Account Entry" +msgstr "" + +#. module: l10n_fr_certification +#: code:addons/l10n_fr_certification/models/account.py:34 +#, python-format +msgid "An error occured when computing the inalterability. Impossible to get the unique previous posted journal entry." +msgstr "" + +#. module: l10n_fr_certification +#: model:ir.model,name:l10n_fr_certification.model_res_company +msgid "Companies" +msgstr "" + +#. module: l10n_fr_certification +#: code:addons/l10n_fr_certification/models/account.py:123 +#, python-format +msgid "Corrupted data on journal entry with id %s." +msgstr "" + +#. module: l10n_fr_certification +#: model:ir.ui.view,arch_db:l10n_fr_certification.view_general_fr_inalterability_inherit_config +msgid "Data Inalterability" +msgstr "" + +#. module: l10n_fr_certification +#: model:ir.actions.server,name:l10n_fr_certification.action_check_hash_integrity +#: model:ir.ui.menu,name:l10n_fr_certification.menu_check_move_integrity +#: model:ir.ui.menu,name:l10n_fr_certification.menu_check_move_integrity_reporting +#: model:ir.ui.view,arch_db:l10n_fr_certification.view_general_fr_inalterability_inherit_config +msgid "Data Inalterability Check" +msgstr "" + +#. module: l10n_fr_certification +#: model:ir.ui.menu,name:l10n_fr_certification.inalterability_fr_statements_menu +msgid "French Statements" +msgstr "" + +#. module: l10n_fr_certification +#: model:ir.actions.server,name:l10n_fr_certification.odoo_french_certification +#: model:ir.ui.view,arch_db:l10n_fr_certification.view_general_fr_inalterability_inherit_config +msgid "Get the certification now!" +msgstr "" + +#. module: l10n_fr_certification +#: model:ir.model.fields,field_description:l10n_fr_certification.field_account_move_l10n_fr_hash +msgid "Inalterability Hash" +msgstr "" + +#. module: l10n_fr_certification +#: model:ir.model.fields,field_description:l10n_fr_certification.field_account_move_l10n_fr_secure_sequence_number +msgid "Inalteralbility No Gap Sequence #" +msgstr "" + +#. module: l10n_fr_certification +#: code:addons/l10n_fr_certification/models/account.py:175 +#, python-format +msgid "It is not permitted to disable the data inalterability in this journal (%s) since journal entries have already been protected." +msgstr "" + +#. module: l10n_fr_certification +#: model:ir.model,name:l10n_fr_certification.model_account_journal +msgid "Journal" +msgstr "" + +#. module: l10n_fr_certification +#: model:ir.model,name:l10n_fr_certification.model_account_move_line +msgid "Journal Item" +msgstr "" + +#. module: l10n_fr_certification +#: model:ir.model.fields,field_description:l10n_fr_certification.field_account_move_l10n_fr_string_to_hash +msgid "L10n fr string to hash" +msgstr "" + +#. module: l10n_fr_certification +#: model:ir.model.fields,field_description:l10n_fr_certification.field_res_company_l10n_fr_secure_sequence_id +msgid "Sequence to use to ensure the securisation of data" +msgstr "" + +#. module: l10n_fr_certification +#: code:addons/l10n_fr_certification/models/account.py:136 +#, python-format +msgid "Successful test !\n" +"\n" +" The journal entries are guaranteed to be in their original and inalterable state\n" +" From: %(start_move_name)s %(start_move_ref)s\n" +" To: %(end_move_name)s %(end_move_ref)s\n" +"\n" +" For this report to be legally meaningful, please download your certification from your customer account on Odoo.com (Only for Odoo Enterprise users)." +msgstr "" + +#. module: l10n_fr_certification +#: code:addons/l10n_fr_certification/models/account.py:118 +#, python-format +msgid "There isn't any journal entry flagged for data inalterability yet for the company %s. This mechanism only runs for journal entries generated after the installation of the module France - Certification CGI 286 I-3 bis." +msgstr "" + +#. module: l10n_fr_certification +#: code:addons/l10n_fr_certification/models/account.py:99 +#, python-format +msgid "You cannot modify a posted journal entry. This ensures its inalterability." +msgstr "" + +#. module: l10n_fr_certification +#: code:addons/l10n_fr_certification/models/account.py:82 +#, python-format +msgid "You cannot overwrite the values ensuring the inalterability of the accounting." +msgstr "" + diff --git a/addons/l10n_fr_certification/models/account.py b/addons/l10n_fr_certification/models/account.py index 07d5fc04e41..d7aeda0bb48 100644 --- a/addons/l10n_fr_certification/models/account.py +++ b/addons/l10n_fr_certification/models/account.py @@ -6,7 +6,6 @@ from openerp import models, api, fields from openerp.tools.translate import _ from openerp.exceptions import UserError -ERR_MSG = _("According to the french law, you cannot modify a %s in order for its posted data to be updated or deleted. Field: %s") #forbidden fields MOVE_FIELDS = ['date', 'journal_id', 'company_id'] @@ -16,8 +15,10 @@ LINE_FIELDS = ['debit', 'credit', 'account_id', 'partner_id'] class AccountMove(models.Model): _inherit = "account.move" - l10n_fr_secure_sequence_number = fields.Integer(readonly=True, copy=False) - l10n_fr_hash = fields.Char(readonly=True, copy=False) + # TO DO in master : refactor hashing algo to go into a mixin + + l10n_fr_secure_sequence_number = fields.Integer(string="Inalteralbility No Gap Sequence #", readonly=True, copy=False) + l10n_fr_hash = fields.Char(string="Inalterability Hash", readonly=True, copy=False) l10n_fr_string_to_hash = fields.Char(compute='_compute_string_to_hash', readonly=True, store=False) def _get_new_hash(self, secure_seq_number): @@ -26,10 +27,11 @@ class AccountMove(models.Model): #get the only one exact previous move in the securisation sequence prev_move = self.search([('state', '=', 'posted'), ('company_id', '=', self.company_id.id), + ('l10n_fr_secure_sequence_number', '!=', 0), ('l10n_fr_secure_sequence_number', '=', int(secure_seq_number) - 1)]) if prev_move and len(prev_move) != 1: raise UserError( - _('Error occured when computing the hash. Impossible to get the unique previous posted move')) + _('An error occured when computing the inalterability. Impossible to get the unique previous posted journal entry.')) #build and return the hash return self._compute_hash(prev_move.l10n_fr_hash if prev_move else '') @@ -67,33 +69,34 @@ class AccountMove(models.Model): def write(self, vals): has_been_posted = False for move in self: - if move.company_id.country_id.code == 'FR': + if move.company_id._is_accounting_unalterable(): # write the hash and the secure_sequence_number when posting an account.move if vals.get('state') == 'posted': has_been_posted = True # restrict the operation in case we are trying to write a forbidden field if (move.state == "posted" and set(vals).intersection(MOVE_FIELDS)): - raise UserError(ERR_MSG % (self._name, ', '.join(MOVE_FIELDS))) + raise UserError(_("According to the French law, you cannot modify a journal entry in order for its posted data to be updated or deleted. Unauthorized field: %s.") % ', '.join(MOVE_FIELDS)) # restrict the operation in case we are trying to overwrite existing hash if (move.l10n_fr_hash and 'l10n_fr_hash' in vals) or (move.l10n_fr_secure_sequence_number and 'l10n_fr_secure_sequence_number' in vals): raise UserError(_('You cannot overwrite the values ensuring the inalterability of the accounting.')) res = super(AccountMove, self).write(vals) # write the hash and the secure_sequence_number when posting an account.move if has_been_posted: - for move in self.filtered(lambda m: m.company_id.country_id.code == 'FR' and - not (m.l10n_fr_secure_sequence_number or m.l10n_fr_hash)): + for move in self.filtered(lambda m: m.company_id._is_accounting_unalterable() and + not (m.l10n_fr_secure_sequence_number or m.l10n_fr_hash)): new_number = move.company_id.l10n_fr_secure_sequence_id.next_by_id() vals_hashing = {'l10n_fr_secure_sequence_number': new_number, 'l10n_fr_hash': move._get_new_hash(new_number)} res |= super(AccountMove, move).write(vals_hashing) return res + @api.multi def button_cancel(self): #by-pass the normal behavior/message that tells people can cancel a posted journal entry #if the journal allows it. - if self.company_id.country_id.code == 'FR': - raise UserError(_('You cannot modify a posted entry of a journal.')) + if self.company_id._is_accounting_unalterable(): + raise UserError(_('You cannot modify a posted journal entry. This ensures its inalterability.')) super(AccountMove, self).button_cancel() @api.model @@ -101,29 +104,43 @@ class AccountMove(models.Model): """Checks that all posted moves have still the same data as when they were posted and raises an error with the result. """ + def build_move_info(move): + entry_reference = _('(ref.: %s)') + move_reference_string = move.ref and entry_reference % move.ref or '' + return [move.name, move_reference_string] + moves = self.search([('state', '=', 'posted'), ('company_id', '=', company_id), ('l10n_fr_secure_sequence_number', '!=', 0)], order="l10n_fr_secure_sequence_number ASC") if not moves: - raise UserError(_('Warning: impossible to find any move with a hash.')) + raise UserError(_('There isn\'t any journal entry flagged for data inalterability yet for the company %s. This mechanism only runs for journal entries generated after the installation of the module France - Certification CGI 286 I-3 bis.') % self.env.user.company_id.name) previous_hash = '' start_move_info = [] for move in moves: if move.l10n_fr_hash != move._compute_hash(previous_hash=previous_hash): - raise UserError(_('Corrupted Data on move %s.') % move.id) + raise UserError(_('Corrupted data on journal entry with id %s.') % move.id) if not previous_hash: #save the date and sequence number of the first move hashed - start_move_info = [move.date, move.l10n_fr_secure_sequence_number] + start_move_info = build_move_info(move) previous_hash = move.l10n_fr_hash - end_move_info = [move.date, move.l10n_fr_secure_sequence_number] - raise UserError(_('''Successfully checked the integrity of account moves. + end_move_info = build_move_info(move) - The account moves are guaranteed to be in their original and inalterable state - - since: %s (Sequence Number: %s) - - to: %s (Sequence Number: %s)''' - ) % (start_move_info[0], start_move_info[1], end_move_info[0], end_move_info[1])) + report_dict = {'start_move_name': start_move_info[0], + 'start_move_ref': start_move_info[1], + 'end_move_name': end_move_info[0], + 'end_move_ref': end_move_info[1]} + + # Raise on success + raise UserError(_('''Successful test ! + + The journal entries are guaranteed to be in their original and inalterable state + From: %(start_move_name)s %(start_move_ref)s + To: %(end_move_name)s %(end_move_ref)s + + For this report to be legally meaningful, please download your certification from your customer account on Odoo.com (Only for Odoo Enterprise users).''' + ) % report_dict) class AccountMoveLine(models.Model): @@ -133,24 +150,46 @@ class AccountMoveLine(models.Model): def write(self, vals): # restrict the operation in case we are trying to write a forbidden field if set(vals).intersection(LINE_FIELDS): - if any(l.company_id.country_id.code == 'FR' and l.move_id.state == 'posted' for l in self): - raise UserError(ERR_MSG % (self._name, ', '.join(LINE_FIELDS))) + if any(l.company_id._is_accounting_unalterable() and l.move_id.state == 'posted' for l in self): + raise UserError(_("According to the French law, you cannot modify a journal item in order for its posted data to be updated or deleted. Unauthorized field: %s.") % ', '.join(LINE_FIELDS)) return super(AccountMoveLine, self).write(vals) class AccountJournal(models.Model): _inherit = "account.journal" + @api.onchange('update_posted') + def _onchange_update_posted(self): + if self.update_posted and self.company_id._is_accounting_unalterable(): + field_string = self._fields['update_posted'].get_description(self.env)['string'] + raise UserError(_("According to the French law, you cannot modify a journal in order for its posted data to be updated or deleted. Unauthorized field: %s.") % field_string) + + @api.multi + def _is_journal_alterable(self): + self.ensure_one() + critical_domain = [('journal_id', '=', self.id), + '|', ('l10n_fr_hash', '!=', False), + '&', ('l10n_fr_secure_sequence_number', '!=', False), + ('l10n_fr_secure_sequence_number', '!=', 0)] + if self.env['account.move'].search(critical_domain): + raise UserError(_('It is not permitted to disable the data inalterability in this journal (%s) since journal entries have already been protected.') % (self.name, )) + return True + @api.multi def write(self, vals): # restrict the operation in case we are trying to write a forbidden field - if self.company_id.country_id.code == 'FR' and vals.get('update_posted'): - raise UserError(ERR_MSG % (self._name, 'update_posted')) + for journal in self: + if journal.company_id._is_accounting_unalterable(): + if vals.get('update_posted'): + field_string = journal._fields['update_posted'].get_description(self.env)['string'] + raise UserError(_("According to the French law, you cannot modify a journal in order for its posted data to be updated or deleted. Unauthorized field: %s.") % field_string) return super(AccountJournal, self).write(vals) @api.model def create(self, vals): # restrict the operation in case we are trying to set a forbidden field - if self.company_id.country_id.code == 'FR' and vals.get('update_posted'): - raise UserError(ERR_MSG % (self._name, 'update_posted')) + if self.company_id._is_accounting_unalterable(): + if vals.get('update_posted'): + field_string = self._fields['update_posted'].get_description(self.env)['string'] + raise UserError(_("According to the French law, you cannot modify a journal in order for its posted data to be updated or deleted. Unauthorized field: %s.") % field_string) return super(AccountJournal, self).create(vals) diff --git a/addons/l10n_fr_certification/models/res_company.py b/addons/l10n_fr_certification/models/res_company.py index 66a2a633164..91e00081914 100644 --- a/addons/l10n_fr_certification/models/res_company.py +++ b/addons/l10n_fr_certification/models/res_company.py @@ -3,40 +3,61 @@ from openerp import fields, models, api +UNALTERABLE_COUNTRIES = ['FR', 'MF', 'MQ', 'NC', 'PF', 'RE', 'GF', 'GP', 'TF'] + + class ResCompany(models.Model): _inherit = 'res.company' + # To do in master : refactor to set sequences more generic + l10n_fr_secure_sequence_id = fields.Many2one('ir.sequence', 'Sequence to use to ensure the securisation of data', readonly=True) @api.model def create(self, vals): company = super(ResCompany, self).create(vals) #when creating a new french company, create the securisation sequence as well - if company.country_id == self.env.ref('base.fr'): - company._create_secure_sequence() + if company._is_accounting_unalterable(): + sequence_fields = ['l10n_fr_secure_sequence_id'] + company._create_secure_sequence(sequence_fields) return company @api.multi def write(self, vals): res = super(ResCompany, self).write(vals) #if country changed to fr, create the securisation sequence - if vals.get('country_id') and vals.get('country_id') == self.env.ref('base.fr').id: - self.filtered(lambda c: not c.l10n_fr_secure_sequence_id)._create_secure_sequence() + for company in self: + if company._is_accounting_unalterable(): + sequence_fields = ['l10n_fr_secure_sequence_id'] + company._create_secure_sequence(sequence_fields) return res - def _create_secure_sequence(self): + def _create_secure_sequence(self, sequence_fields): """This function creates a no_gap sequence on each companies in self that will ensure a unique number is given to all posted account.move in such a way that we can always find the previous move of a journal entry. """ for company in self: - vals = { - 'name': 'French Securisation of account_move_line - ' + company.name, - 'code': 'FRSECUR', - 'implementation': 'no_gap', - 'prefix': '', - 'suffix': '', - 'padding': 0, - 'company_id': company.id} - seq = self.env['ir.sequence'].create(vals) - company.write({'l10n_fr_secure_sequence_id': seq.id}) + vals_write = {} + for seq_field in sequence_fields: + if not company[seq_field]: + vals = { + 'name': 'French Securisation of ' + seq_field + ' - ' + company.name, + 'code': 'FRSECUR', + 'implementation': 'no_gap', + 'prefix': '', + 'suffix': '', + 'padding': 0, + 'company_id': company.id} + seq = self.env['ir.sequence'].create(vals) + vals_write[seq_field] = seq.id + if vals_write: + company.write(vals_write) + + def _is_vat_french(self): + return self.vat and self.vat.startswith('FR') and len(self.vat) == 13 + + def _is_accounting_unalterable(self): + if not self.vat and not self.country_id: + return False + return self.country_id and self.country_id.code in UNALTERABLE_COUNTRIES or self._is_vat_french() diff --git a/addons/l10n_fr_certification/static/description/icon.png b/addons/l10n_fr_certification/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..bbcaa4fcce7321598e556ae85fef65b0ed74fbdb GIT binary patch literal 562 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSY)RhkE>8(%y`dEVi!oTtiUlHNH>77!9&AY3=E8Bo-U3d6}R4AGW2y$lyJDXp1;N53Y(Th zU{h1xR)-+gGZxC3a-|pg>StB=HCf-01nPx?2Rmh-z5QKZdc^s9t(|>evE}~r+hoJ8 z-`W2D^Y_wOug||f^ZN9wS?$}dGOkH&uwXtSsc?>=kgdbmVGiRX9)a|sCIs^~ke+v! zw^xXlV*|_dC?HR@#5JNMC9x#cD!C{XNHG{07#issnCKc9g&3Mx85>&}nQ9vtSs56t zpT<#(q9HdwB{QuOO@o1{m60Jt!@i29DL@U9ARB`7(@M${i&7cN%ggmL^RkPR6AM!H X@{7`Ezq647Dq`?-^>bP0l+XkK28gaM literal 0 HcmV?d00001 diff --git a/addons/l10n_fr_certification/views/no_cancel.xml b/addons/l10n_fr_certification/views/no_cancel.xml deleted file mode 100644 index d732364b189..00000000000 --- a/addons/l10n_fr_certification/views/no_cancel.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - bank.statement.draft.form.inherit - account.bank.statement - - - - 1 - - - - - - invoice.form.cancel.inherit - account.invoice - - - - 1 - - - - - - account.move.form.nocancel.inherit - account.move - - - - 1 - - - - - - \ No newline at end of file diff --git a/addons/l10n_fr_certification/views/res_config.xml b/addons/l10n_fr_certification/views/res_config.xml new file mode 100644 index 00000000000..36f66f00df2 --- /dev/null +++ b/addons/l10n_fr_certification/views/res_config.xml @@ -0,0 +1,18 @@ + + + Inalterability settings + base.config.settings + + + + + + + + + \ No newline at end of file diff --git a/addons/l10n_fr_pos_cert/__init__.py b/addons/l10n_fr_pos_cert/__init__.py new file mode 100644 index 00000000000..f95ac6e7067 --- /dev/null +++ b/addons/l10n_fr_pos_cert/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import models +from openerp import api, SUPERUSER_ID +from ..l10n_fr_certification.models.res_company import UNALTERABLE_COUNTRIES + + +def _setup_inalterability(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + # enable ping for this module + env['publisher_warranty.contract'].update_notification(cron_mode=True) + + fr_companies = env['res.company'].search([('partner_id.country_id.code', 'in', UNALTERABLE_COUNTRIES)]) + if fr_companies: + # create the securisation sequence per company + fr_companies._create_secure_sequence(['l10n_fr_pos_cert_sequence_id']) diff --git a/addons/l10n_fr_pos_cert/__openerp__.py b/addons/l10n_fr_pos_cert/__openerp__.py new file mode 100644 index 00000000000..fd9ae3459e8 --- /dev/null +++ b/addons/l10n_fr_pos_cert/__openerp__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'France - VAT Anti-Fraud Certification for Point of Sale (CGI 286 I-3 bis)', + 'version': '1.0', + 'category': 'Localization', + 'description': """ +This add-on brings the technical requirements of the French regulation CGI art. 286, I. 3° bis that stipulates certain criteria concerning the inalterability, security, storage and archiving of data related to sales to private individuals (B2C). +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +Install it if you use the Point of Sale app to sell to individuals. + +The module adds following features: + + Inalterability: deactivation of all the ways to cancel or modify key data of POS orders, invoices and journal entries + + Security: chaining algorithm to verify the inalterability + + Storage: automatic sales closings with computation of both period and cumulative totals (daily, monthly, annually) + + Access to download the mandatory Certificate of Conformity delivered by Odoo SA (only for Odoo Enterprise users) +""", + 'depends': ['l10n_fr_certification', 'point_of_sale'], + 'installable': True, + 'auto_install': False, + 'application': False, + 'data': [ + 'data/pos_inalterability.xml', + 'views/account_views.xml', + ], + 'post_init_hook': '_setup_inalterability', +} diff --git a/addons/l10n_fr_pos_cert/data/pos_inalterability.xml b/addons/l10n_fr_pos_cert/data/pos_inalterability.xml new file mode 100644 index 00000000000..ace4c3de24d --- /dev/null +++ b/addons/l10n_fr_pos_cert/data/pos_inalterability.xml @@ -0,0 +1,17 @@ + + + + + + + + Pos Orders Inalterability Check + + ir.actions.server + +action = env['pos.order']._check_hash_integrity(env.user.company_id.id) + + + + + \ No newline at end of file diff --git a/addons/l10n_fr_pos_cert/i18n/l10n_fr_pos_cert.pot b/addons/l10n_fr_pos_cert/i18n/l10n_fr_pos_cert.pot new file mode 100644 index 00000000000..4e1507eb9f0 --- /dev/null +++ b/addons/l10n_fr_pos_cert/i18n/l10n_fr_pos_cert.pot @@ -0,0 +1,166 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * l10n_fr_pos_cert +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 9.0c\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-12-06 16:08+0000\n" +"PO-Revision-Date: 2017-12-06 16:08+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: l10n_fr_pos_cert +#: code:addons/l10n_fr_pos_cert/models/pos.py:145 +#, python-format +msgid "(Receipt ref.: %s)" +msgstr "" + +#. module: l10n_fr_pos_cert +#: code:addons/l10n_fr_pos_cert/models/pos.py:57 +#, python-format +msgid "According to the French law, you cannot modify a %s. Forbidden fields: %s." +msgstr "" + +#. module: l10n_fr_pos_cert +#: code:addons/l10n_fr_pos_cert/models/pos.py:193 +#, python-format +msgid "According to the French law, you cannot modify a point of sale order line. Forbidden fields: %s." +msgstr "" + +#. module: l10n_fr_pos_cert +#: code:addons/l10n_fr_pos_cert/models/pos.py:124 +#, python-format +msgid "According to the French law, you cannot modify a point of sale order. Forbidden fields: %s." +msgstr "" + +#. module: l10n_fr_pos_cert +#: code:addons/l10n_fr_pos_cert/models/pos.py:77 +#, python-format +msgid "An error occured when computing the inalterability. Impossible to get the unique previous posted point of sale order." +msgstr "" + +#. module: l10n_fr_pos_cert +#: model:ir.model,name:l10n_fr_pos_cert.model_account_bank_statement +msgid "Bank Statement" +msgstr "" + +#. module: l10n_fr_pos_cert +#: model:ir.model,name:l10n_fr_pos_cert.model_account_bank_statement_line +msgid "Bank Statement Line" +msgstr "" + +#. module: l10n_fr_pos_cert +#: model:ir.model,name:l10n_fr_pos_cert.model_res_company +msgid "Companies" +msgstr "" + +#. module: l10n_fr_pos_cert +#: code:addons/l10n_fr_pos_cert/models/pos.py:160 +#, python-format +msgid "Corrupted data on point of sale order with id %s." +msgstr "" + +#. module: l10n_fr_pos_cert +#: model:ir.ui.menu,name:l10n_fr_pos_cert.pos_fr_statements_menu +msgid "French Statements" +msgstr "" + +#. module: l10n_fr_pos_cert +#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order_l10n_fr_hash +msgid "Inalteralbility Hash" +msgstr "" + +#. module: l10n_fr_pos_cert +#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order_l10n_fr_secure_sequence_number +msgid "Inalteralbility No Gap Sequence #" +msgstr "" + +#. module: l10n_fr_pos_cert +#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_res_company_l10n_fr_pos_cert_sequence_id +msgid "L10n fr pos cert sequence id" +msgstr "" + +#. module: l10n_fr_pos_cert +#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order_l10n_fr_string_to_hash +msgid "L10n fr string to hash" +msgstr "" + +#. module: l10n_fr_pos_cert +#: model:ir.model,name:l10n_fr_pos_cert.model_pos_order_line +msgid "Lines of Point of Sale" +msgstr "" + +#. module: l10n_fr_pos_cert +#: model:ir.model,name:l10n_fr_pos_cert.model_pos_order +msgid "Point of Sale" +msgstr "" + +#. module: l10n_fr_pos_cert +#: model:ir.actions.server,name:l10n_fr_pos_cert.action_check_hash_integrity_pos_orders +#: model:ir.ui.menu,name:l10n_fr_pos_cert.menu_check_pos_order_integrity +msgid "Pos Orders Inalterability Check" +msgstr "" + +#. module: l10n_fr_pos_cert +#: model:ir.ui.menu,name:l10n_fr_pos_cert.menu_account_closing +msgid "Sales Closings" +msgstr "" + +#. module: l10n_fr_pos_cert +#: code:addons/l10n_fr_pos_cert/models/pos.py:175 +#, python-format +msgid "Successful test !\n" +"\n" +" The point of sale orders are guaranteed to be in their original and inalterable state\n" +" From: %(start_order_name)s %(start_order_ref)s recorded on %(start_order_date)s\n" +" To: %(end_order_name)s %(end_order_ref)s recorded on %(end_order_date)s\n" +"\n" +" For this report to be legally meaningful, please download your certification from your customer account on Odoo.com (Only for Odoo Enterprise users)." +msgstr "" + +#. module: l10n_fr_pos_cert +#: code:addons/l10n_fr_pos_cert/models/pos.py:155 +#, python-format +msgid "There isn't any order flagged for data inalterability yet for the company %s. This mechanism only runs for point of sale orders generated after the installation of the module France - Certification CGI 286 I-3 bis. - POS" +msgstr "" + +#. module: l10n_fr_pos_cert +#: code:addons/l10n_fr_pos_cert/models/pos.py:45 +#, python-format +msgid "This session has been opened another day. To comply with the French law, you should close sessions on a daily basis. Please close session %s and open a new one." +msgstr "" + +#. module: l10n_fr_pos_cert +#: code:addons/l10n_fr_pos_cert/models/account_bank_statement.py:11 +#, python-format +msgid "You cannot modify anything on a bank statement (name: %s) that was created by point of sale operations." +msgstr "" + +#. module: l10n_fr_pos_cert +#: code:addons/l10n_fr_pos_cert/models/account_bank_statement.py:20 +#, python-format +msgid "You cannot modify anything on a bank statement line (name: %s) that was created by point of sale operations." +msgstr "" + +#. module: l10n_fr_pos_cert +#: code:addons/l10n_fr_pos_cert/models/pos.py:127 +#, python-format +msgid "You cannot overwrite the values ensuring the inalterability of the point of sale." +msgstr "" + +#. module: l10n_fr_pos_cert +#: model:ir.model,name:l10n_fr_pos_cert.model_pos_config +msgid "pos.config" +msgstr "" + +#. module: l10n_fr_pos_cert +#: model:ir.model,name:l10n_fr_pos_cert.model_pos_session +msgid "pos.session" +msgstr "" + diff --git a/addons/l10n_fr_pos_cert/models/__init__.py b/addons/l10n_fr_pos_cert/models/__init__.py new file mode 100644 index 00000000000..e56d2fcd33c --- /dev/null +++ b/addons/l10n_fr_pos_cert/models/__init__.py @@ -0,0 +1,3 @@ +import account_bank_statement +import res_company +import pos \ No newline at end of file diff --git a/addons/l10n_fr_pos_cert/models/account_bank_statement.py b/addons/l10n_fr_pos_cert/models/account_bank_statement.py new file mode 100644 index 00000000000..28afbf0d549 --- /dev/null +++ b/addons/l10n_fr_pos_cert/models/account_bank_statement.py @@ -0,0 +1,21 @@ +from openerp import models, api +from openerp.tools.translate import _ +from openerp.exceptions import UserError + + +class AccountBankStatement(models.Model): + _inherit = 'account.bank.statement' + + def unlink(self): + for statement in self.filtered(lambda s: s.company_id._is_accounting_unalterable() and s.journal_id.journal_user): + raise UserError(_('You cannot modify anything on a bank statement (name: %s) that was created by point of sale operations.') % (statement.name,)) + return super(AccountBankStatement, self).unlink() + + +class AccountBankStatementLine(models.Model): + _inherit = 'account.bank.statement.line' + + def unlink(self): + for line in self.filtered(lambda s: s.company_id._is_accounting_unalterable() and s.journal_id.journal_user): + raise UserError(_('You cannot modify anything on a bank statement line (name: %s) that was created by point of sale operations.') % (line.name,)) + return super(AccountBankStatementLine, self).unlink() diff --git a/addons/l10n_fr_pos_cert/models/pos.py b/addons/l10n_fr_pos_cert/models/pos.py new file mode 100644 index 00000000000..d6d88604e8c --- /dev/null +++ b/addons/l10n_fr_pos_cert/models/pos.py @@ -0,0 +1,194 @@ +from datetime import datetime +from hashlib import sha256 +from json import dumps +import pytz + +from openerp import models, api, fields +from openerp.fields import Datetime +from openerp.tools.translate import _ +from openerp.exceptions import UserError + + +def ctx_tz(record, field): + res_lang = None + ctx = record._context + tz_name = pytz.timezone(ctx.get('tz') or record.env.user.tz) + timestamp = Datetime.from_string(record[field]) + if ctx.get('lang'): + res_lang = record.env['res.lang'].search([('code', '=', ctx['lang'])], limit=1) + if res_lang: + timestamp = pytz.utc.localize(timestamp, is_dst=False) + return datetime.strftime(timestamp.astimezone(tz_name), res_lang.date_format + ' ' + res_lang.time_format) + return Datetime.context_timestamp(record, timestamp) + + +class pos_config(models.Model): + _inherit = 'pos.config' + + @api.multi + def open_ui(self): + for config in self.filtered(lambda c: c.company_id._is_accounting_unalterable()): + if config.current_session_id: + config.current_session_id._check_session_timing() + return super(pos_config, self).open_ui() + + +class pos_session(models.Model): + _inherit = 'pos.session' + + @api.multi + def _check_session_timing(self): + self.ensure_one() + date_today = datetime.utcnow() + session_start = Datetime.from_string(self.start_at) + if not date_today - datetime.timedelta(hours=24) <= session_start: + raise UserError(_("This session has been opened another day. To comply with the French law, you should close sessions on a daily basis. Please close session %s and open a new one.") % self.name) + return True + + @api.multi + def open_frontend_cb(self): + for session in self.filtered(lambda s: s.config_id.company_id._is_accounting_unalterable()): + session._check_session_timing() + return super(pos_session, self).open_frontend_cb() + + +ORDER_FIELDS = ['date_order', 'user_id', 'lines', 'statement_ids', 'pricelist_id', 'partner_id', 'session_id', 'pos_reference', 'sale_journal', 'fiscal_position_id'] +LINE_FIELDS = ['notice', 'product_id', 'qty', 'price_unit', 'discount', 'tax_ids', 'tax_ids_after_fiscal_position'] +ERR_MSG = _('According to the French law, you cannot modify a %s. Forbidden fields: %s.') + + +class pos_order(models.Model): + _inherit = 'pos.order' + + l10n_fr_hash = fields.Char(string="Inalteralbility Hash", readonly=True, copy=False) + l10n_fr_secure_sequence_number = fields.Integer(string="Inalteralbility No Gap Sequence #", readonly=True, copy=False) + l10n_fr_string_to_hash = fields.Char(compute='_compute_string_to_hash', readonly=True, store=False) + + def _get_new_hash(self, secure_seq_number): + """ Returns the hash to write on pos orders when they get posted""" + self.ensure_one() + #get the only one exact previous order in the securisation sequence + prev_order = self.search([('state', 'in', ['paid', 'done', 'invoiced']), + ('company_id', '=', self.company_id.id), + ('l10n_fr_secure_sequence_number', '!=', 0), + ('l10n_fr_secure_sequence_number', '=', int(secure_seq_number) - 1)]) + if prev_order and len(prev_order) != 1: + raise UserError( + _('An error occured when computing the inalterability. Impossible to get the unique previous posted point of sale order.')) + + #build and return the hash + return self._compute_hash(prev_order.l10n_fr_hash if prev_order else '') + + def _compute_hash(self, previous_hash): + """ Computes the hash of the browse_record given as self, based on the hash + of the previous record in the company's securisation sequence given as parameter""" + self.ensure_one() + hash_string = sha256(previous_hash + self.l10n_fr_string_to_hash) + return hash_string.hexdigest() + + def _compute_string_to_hash(self): + def _getattrstring(obj, field_str): + field_value = obj[field_str] + if obj._fields[field_str].type == 'many2one': + field_value = field_value.id + if obj._fields[field_str].type in ['many2many', 'one2many']: + field_value = field_value.ids + return str(field_value) + + for order in self: + values = {} + for field in ORDER_FIELDS: + values[field] = _getattrstring(order, field) + + for line in order.lines: + for field in LINE_FIELDS: + k = 'line_%d_%s' % (line.id, field) + values[k] = _getattrstring(line, field) + #make the json serialization canonical + # (https://tools.ietf.org/html/draft-staykov-hu-json-canonical-form-00) + order.l10n_fr_string_to_hash = dumps(values, sort_keys=True, encoding="utf-8", + ensure_ascii=True, indent=None, + separators=(',',':')) + + @api.multi + def write(self, vals): + has_been_posted = False + for order in self: + if order.company_id._is_accounting_unalterable(): + # write the hash and the secure_sequence_number when posting or invoicing an pos.order + if vals.get('state') in ['paid', 'done', 'invoiced']: + has_been_posted = True + + # restrict the operation in case we are trying to write a forbidden field + if (order.state in ['paid', 'done', 'invoiced'] and set(vals).intersection(ORDER_FIELDS)): + raise UserError(_('According to the French law, you cannot modify a point of sale order. Forbidden fields: %s.') % ', '.join(ORDER_FIELDS)) + # restrict the operation in case we are trying to overwrite existing hash + if (order.l10n_fr_hash and 'l10n_fr_hash' in vals) or (order.l10n_fr_secure_sequence_number and 'l10n_fr_secure_sequence_number' in vals): + raise UserError(_('You cannot overwrite the values ensuring the inalterability of the point of sale.')) + res = super(pos_order, self).write(vals) + # write the hash and the secure_sequence_number when posting or invoicing a pos order + if has_been_posted: + for order in self.filtered(lambda o: o.company_id._is_accounting_unalterable() and + not (o.l10n_fr_secure_sequence_number or o.l10n_fr_hash)): + new_number = order.company_id.l10n_fr_pos_cert_sequence_id.next_by_id() + vals_hashing = {'l10n_fr_secure_sequence_number': new_number, + 'l10n_fr_hash': order._get_new_hash(new_number)} + res |= super(pos_order, order).write(vals_hashing) + return res + + @api.model + def _check_hash_integrity(self, company_id): + """Checks that all posted or invoiced pos orders have still the same data as when they were posted + and raises an error with the result. + """ + def build_order_info(order): + entry_reference = _('(Receipt ref.: %s)') + order_reference_string = order.pos_reference and entry_reference % order.pos_reference or '' + return [ctx_tz(order, 'date_order'), order.l10n_fr_secure_sequence_number, order.name, order_reference_string, ctx_tz(order, 'write_date')] + + orders = self.search([('state', 'in', ['paid', 'done', 'invoiced']), + ('company_id', '=', company_id), + ('l10n_fr_secure_sequence_number', '!=', 0)], + order="l10n_fr_secure_sequence_number ASC") + + if not orders: + raise UserError(_('There isn\'t any order flagged for data inalterability yet for the company %s. This mechanism only runs for point of sale orders generated after the installation of the module France - Certification CGI 286 I-3 bis. - POS') % self.env.user.company_id.name) + previous_hash = '' + start_order_info = [] + for order in orders: + if order.l10n_fr_hash != order._compute_hash(previous_hash=previous_hash): + raise UserError(_('Corrupted data on point of sale order with id %s.') % order.id) + previous_hash = order.l10n_fr_hash + + orders_sorted_date = orders.sorted(lambda o: o.date_order) + start_order_info = build_order_info(orders_sorted_date[0]) + end_order_info = build_order_info(orders_sorted_date[-1]) + + report_dict = {'start_order_name': start_order_info[2], + 'start_order_ref': start_order_info[3], + 'start_order_date': start_order_info[0], + 'end_order_name': end_order_info[2], + 'end_order_ref': end_order_info[3], + 'end_order_date': end_order_info[0]} + + # Raise on success + raise UserError(_('''Successful test ! + + The point of sale orders are guaranteed to be in their original and inalterable state + From: %(start_order_name)s %(start_order_ref)s recorded on %(start_order_date)s + To: %(end_order_name)s %(end_order_ref)s recorded on %(end_order_date)s + + For this report to be legally meaningful, please download your certification from your customer account on Odoo.com (Only for Odoo Enterprise users).''' + ) % report_dict) + + +class PosOrderLine(models.Model): + _inherit = "pos.order.line" + + @api.multi + def write(self, vals): + # restrict the operation in case we are trying to write a forbidden field + if set(vals).intersection(LINE_FIELDS): + if any(l.company_id._is_accounting_unalterable() and l.order_id.state in ['done', 'invoiced'] for l in self): + raise UserError(_('According to the French law, you cannot modify a point of sale order line. Forbidden fields: %s.') % ', '.join(LINE_FIELDS)) + return super(PosOrderLine, self).write(vals) diff --git a/addons/l10n_fr_pos_cert/models/res_company.py b/addons/l10n_fr_pos_cert/models/res_company.py new file mode 100644 index 00000000000..28e13c0d1d2 --- /dev/null +++ b/addons/l10n_fr_pos_cert/models/res_company.py @@ -0,0 +1,26 @@ +from openerp import models, api, fields + + +class ResCompany(models.Model): + _inherit = 'res.company' + + l10n_fr_pos_cert_sequence_id = fields.Many2one('ir.sequence') + + @api.model + def create(self, vals): + company = super(ResCompany, self).create(vals) + #when creating a new french company, create the securisation sequence as well + if company._is_accounting_unalterable(): + sequence_fields = ['l10n_fr_pos_cert_sequence_id'] + company._create_secure_sequence(sequence_fields) + return company + + @api.multi + def write(self, vals): + res = super(ResCompany, self).write(vals) + #if country changed to fr, create the securisation sequence + for company in self: + if company._is_accounting_unalterable(): + sequence_fields = ['l10n_fr_pos_cert_sequence_id'] + company._create_secure_sequence(sequence_fields) + return res diff --git a/addons/l10n_fr_pos_cert/static/description/icon.png b/addons/l10n_fr_pos_cert/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..bbcaa4fcce7321598e556ae85fef65b0ed74fbdb GIT binary patch literal 562 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSY)RhkE>8(%y`dEVi!oTtiUlHNH>77!9&AY3=E8Bo-U3d6}R4AGW2y$lyJDXp1;N53Y(Th zU{h1xR)-+gGZxC3a-|pg>StB=HCf-01nPx?2Rmh-z5QKZdc^s9t(|>evE}~r+hoJ8 z-`W2D^Y_wOug||f^ZN9wS?$}dGOkH&uwXtSsc?>=kgdbmVGiRX9)a|sCIs^~ke+v! zw^xXlV*|_dC?HR@#5JNMC9x#cD!C{XNHG{07#issnCKc9g&3Mx85>&}nQ9vtSs56t zpT<#(q9HdwB{QuOO@o1{m60Jt!@i29DL@U9ARB`7(@M${i&7cN%ggmL^RkPR6AM!H X@{7`Ezq647Dq`?-^>bP0l+XkK28gaM literal 0 HcmV?d00001 diff --git a/addons/l10n_fr_pos_cert/views/account_views.xml b/addons/l10n_fr_pos_cert/views/account_views.xml new file mode 100644 index 00000000000..bfe6100061f --- /dev/null +++ b/addons/l10n_fr_pos_cert/views/account_views.xml @@ -0,0 +1,26 @@ + + + account.bank.statement.form + account.bank.statement + + extension + + + + + + {'readonly': [('pos_session_id', '!=', False)]} + + + {'readonly': [('pos_session_id', '!=', False)]} + + + {'readonly': [('pos_session_id', '!=', False)]} + + + {'readonly': [('pos_session_id', '!=', False)]} + + + + + \ No newline at end of file diff --git a/addons/l10n_fr_sale_closing/__init__.py b/addons/l10n_fr_sale_closing/__init__.py new file mode 100644 index 00000000000..ca9ea5e3dd8 --- /dev/null +++ b/addons/l10n_fr_sale_closing/__init__.py @@ -0,0 +1,14 @@ +import models +from openerp import api, SUPERUSER_ID +from ..l10n_fr_certification.models.res_company import UNALTERABLE_COUNTRIES + + +def _setup_inalterability(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + # enable ping for this module + env['publisher_warranty.contract'].update_notification(cron_mode=True) + + fr_companies = env['res.company'].search([('partner_id.country_id.code', 'in', UNALTERABLE_COUNTRIES)]) + if fr_companies: + # create the securisation sequence per company + fr_companies._create_secure_sequence(['l10n_fr_closing_sequence_id']) diff --git a/addons/l10n_fr_sale_closing/__openerp__.py b/addons/l10n_fr_sale_closing/__openerp__.py new file mode 100644 index 00000000000..83fcad8f626 --- /dev/null +++ b/addons/l10n_fr_sale_closing/__openerp__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'France - VAT Anti-Fraud Certification (CGI 286 I-3 bis) - Sale Closings', + 'version': '1.0', + 'category': 'Localization', + 'description': """ +This module adds support for automatic sales closings with computation of both period and cumulative totals (daily, monthly, annually) +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +The module adds following features: + + Storage: automatic sales closings with computation of both period and cumulative totals (daily, monthly, annually) + +""", + 'depends': ['l10n_fr_certification'], + 'installable': True, + 'auto_install': True, + 'application': False, + 'data': [ + 'views/account_sale_closure.xml', + 'data/account_sale_closure_cron.xml', + 'security/ir.model.access.csv', + 'security/account_closing_intercompany.xml', + ], + 'post_init_hook': '_setup_inalterability', +} diff --git a/addons/l10n_fr_sale_closing/data/account_sale_closure_cron.xml b/addons/l10n_fr_sale_closing/data/account_sale_closure_cron.xml new file mode 100644 index 00000000000..edc1b1566ec --- /dev/null +++ b/addons/l10n_fr_sale_closing/data/account_sale_closure_cron.xml @@ -0,0 +1,35 @@ + + + Generate Daily Sales Closing + 1 + days + -1 + + + + + + + + Generate Monthly Sales Closing + 1 + months + -1 + + + + + + + + Generate Annual Sales Closing + 12 + months + -1 + + + + + + + diff --git a/addons/l10n_fr_sale_closing/i18n/l10n_fr_sale_closing.pot b/addons/l10n_fr_sale_closing/i18n/l10n_fr_sale_closing.pot new file mode 100644 index 00000000000..38ec555a380 --- /dev/null +++ b/addons/l10n_fr_sale_closing/i18n/l10n_fr_sale_closing.pot @@ -0,0 +1,219 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * l10n_fr_sale_closing +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 9.0c\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-12-06 16:03+0000\n" +"PO-Revision-Date: 2017-12-06 16:03+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: l10n_fr_sale_closing +#: model:ir.ui.view,arch_db:l10n_fr_sale_closing.form_view_account_sale_closing +msgid "Account Closing" +msgstr "" + +#. module: l10n_fr_sale_closing +#: selection:account.sale.closing,frequency:0 +msgid "Annual" +msgstr "" + +#. module: l10n_fr_sale_closing +#: code:addons/l10n_fr_sale_closing/models/account_closing.py:134 +#, python-format +msgid "Annual Closing" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_date_closing_stop +msgid "Closing Date" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_frequency +msgid "Closing Type" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model,name:l10n_fr_sale_closing.model_res_company +msgid "Companies" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_company_id +msgid "Company" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_create_uid +msgid "Created by" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_create_date +msgid "Created on" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_total_beginning +msgid "Cumulative Grand Total" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_currency_id +msgid "Currency" +msgstr "" + +#. module: l10n_fr_sale_closing +#: selection:account.sale.closing,frequency:0 +msgid "Daily" +msgstr "" + +#. module: l10n_fr_sale_closing +#: code:addons/l10n_fr_sale_closing/models/account_closing.py:125 +#, python-format +msgid "Daily Closing" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,help:l10n_fr_sale_closing.field_account_sale_closing_date_closing_start +msgid "Date from which the total interval is computed" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,help:l10n_fr_sale_closing.field_account_sale_closing_date_closing_stop +msgid "Date to which the values are computed" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_display_name +msgid "Display Name" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,help:l10n_fr_sale_closing.field_account_sale_closing_name +msgid "Frequency and unique sequence number" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_id +msgid "ID" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,help:l10n_fr_sale_closing.field_account_sale_closing_last_move_id +msgid "Last Journal entry included in the grand total" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing___last_update +msgid "Last Modified on" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_write_uid +msgid "Last Updated by" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_write_date +msgid "Last Updated on" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_last_move_id +msgid "Last journal entry" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_last_move_hash +msgid "Last journal entry's inalteralbility hash" +msgstr "" + +#. module: l10n_fr_sale_closing +#: selection:account.sale.closing,frequency:0 +msgid "Monthly" +msgstr "" + +#. module: l10n_fr_sale_closing +#: code:addons/l10n_fr_sale_closing/models/account_closing.py:130 +#, python-format +msgid "Monthly Closing" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_name +msgid "Name" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_total_interval +msgid "Period Total" +msgstr "" + +#. module: l10n_fr_sale_closing +#: code:addons/l10n_fr_sale_closing/models/account_closing.py:142 +#: code:addons/l10n_fr_sale_closing/models/account_closing.py:146 +#, python-format +msgid "Sale Closings are not meant to be written or deleted under any circumstances." +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.actions.act_window,name:l10n_fr_sale_closing.action_list_view_account_sale_closing +#: model:ir.ui.menu,name:l10n_fr_sale_closing.menu_account_closing +#: model:ir.ui.menu,name:l10n_fr_sale_closing.menu_account_closing_reporting +msgid "Sales Closings" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.actions.act_window,help:l10n_fr_sale_closing.action_list_view_account_sale_closing +msgid "Sales closings run automatically on a daily, monthly and annual basis. It computes both period and cumulative totals from all the sales entries posted in the system after the previous closing." +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_sequence_number +msgid "Sequence #" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_res_company_l10n_fr_closing_sequence_id +msgid "Sequence to use to build sale closings" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_date_closing_start +msgid "Starting Date" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.actions.act_window,help:l10n_fr_sale_closing.action_list_view_account_sale_closing +msgid "The closings are created by Odoo" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,help:l10n_fr_sale_closing.field_account_sale_closing_currency_id +msgid "The company's currency" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,help:l10n_fr_sale_closing.field_account_sale_closing_total_interval +msgid "Total in receivable accounts during the interval, excluding overlapping periods" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model.fields,help:l10n_fr_sale_closing.field_account_sale_closing_total_beginning +msgid "Total in receivable accounts since the beginnig of times" +msgstr "" + +#. module: l10n_fr_sale_closing +#: model:ir.model,name:l10n_fr_sale_closing.model_account_sale_closing +msgid "account.sale.closing" +msgstr "" + diff --git a/addons/l10n_fr_sale_closing/models/__init__.py b/addons/l10n_fr_sale_closing/models/__init__.py new file mode 100644 index 00000000000..64942388936 --- /dev/null +++ b/addons/l10n_fr_sale_closing/models/__init__.py @@ -0,0 +1,2 @@ +import res_company +import account_closing \ No newline at end of file diff --git a/addons/l10n_fr_sale_closing/models/account_closing.py b/addons/l10n_fr_sale_closing/models/account_closing.py new file mode 100644 index 00000000000..c6bc3b64d91 --- /dev/null +++ b/addons/l10n_fr_sale_closing/models/account_closing.py @@ -0,0 +1,158 @@ +from datetime import datetime, timedelta + +from openerp import models, api, fields +from openerp.fields import Datetime as FieldDateTime +from openerp.tools.translate import _ +from openerp.exceptions import UserError + + +class AccountClosing(models.Model): + """ + This object holds an interval total and a grand total of the accounts of type receivable for a company, + as well as the last account_move that has been counted in a previous object + It takes its earliest brother to infer from when the computation needs to be done + in order to compute its own data. + """ + _name = 'account.sale.closing' + _order = 'date_closing_stop desc, sequence_number desc' + + name = fields.Char(help="Frequency and unique sequence number", required=True) + company_id = fields.Many2one('res.company', string='Company', readonly=True, required=True) + date_closing_stop = fields.Datetime(string="Closing Date", help='Date to which the values are computed', readonly=True, required=True) + date_closing_start = fields.Datetime(string="Starting Date", help='Date from which the total interval is computed', readonly=True, required=True) + frequency = fields.Selection(string='Closing Type', selection=[('daily', 'Daily'), ('monthly', 'Monthly'), ('annually', 'Annual')], readonly=True, required=True) + total_interval = fields.Monetary(string="Period Total", help='Total in receivable accounts during the interval, excluding overlapping periods', readonly=True, required=True) + cumulative_total = fields.Monetary(string="Cumulative Grand Total", help='Total in receivable accounts since the beginnig of times', readonly=True, required=True) + sequence_number = fields.Integer('Sequence #', readonly=True, required=True) + last_move_id = fields.Many2one('account.move', string='Last journal entry', help='Last Journal entry included in the grand total', readonly=True) + last_move_hash = fields.Char(string='Last journal entry\'s inalteralbility hash', readonly=True) + currency_id = fields.Many2one('res.currency', string='Currency', help="The company's currency", readonly=True, related='company_id.currency_id', store=True) + + def _query_for_aml(self, company, first_move_sequence_number, date_start): + params = {'company_id': company.id} + query = '''WITH aggregate AS (SELECT m.id AS move_id, + aml.balance AS balance, + aml.id as line_id + FROM account_move_line aml + JOIN account_journal j ON aml.journal_id = j.id + JOIN account_account acc ON acc.id = aml.account_id + JOIN account_account_type t ON (t.id = acc.user_type_id AND t.type = 'receivable') + JOIN account_move m ON m.id = aml.move_id + WHERE j.type = 'sale' + AND aml.company_id = %(company_id)s + AND m.state = 'posted' ''' + + if first_move_sequence_number is not False and first_move_sequence_number is not None: + params['first_move_sequence_number'] = first_move_sequence_number + query += '''AND m.l10n_fr_secure_sequence_number > %(first_move_sequence_number)s''' + elif date_start: + #the first time we compute the closing, we consider only from the installation of the module + params['date_start'] = date_start + query += '''AND m.date >= %(date_start)s''' + + query += " ORDER BY m.l10n_fr_secure_sequence_number DESC) " + query += '''SELECT array_agg(move_id) AS move_ids, + array_agg(line_id) AS line_ids, + sum(balance) AS balance + FROM aggregate''' + + self.env.cr.execute(query, params) + return self.env.cr.dictfetchall()[0] + + def _compute_amounts(self, frequency, company): + """ + Method used to compute all the business data of the new object. + It will search for previous closings of the same frequency to infer the move from which + account move lines should be fetched. + @param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually) + frequencies are literal (daily means 24 hours and so on) + @param {recordset} company: the company for which the closing is done + @return {dict} containing {field: value} for each business field of the object + """ + interval_dates = self._interval_dates(frequency, company) + previous_closing = self.search([ + ('frequency', '=', frequency), + ('company_id', '=', company.id)], limit=1, order='sequence_number desc') + + first_move = self.env['account.move'] + date_start = interval_dates['interval_from'] + cumulative_total = 0 + if previous_closing: + first_move = previous_closing.last_move_id + date_start = previous_closing.create_date + cumulative_total += previous_closing.cumulative_total + + aml_aggregate = self._query_for_aml(company, first_move.l10n_fr_secure_sequence_number, date_start) + + total_interval = aml_aggregate['balance'] or 0 + cumulative_total += total_interval + + # We keep the reference to avoid gaps (like daily object during the weekend) + last_move = first_move + if aml_aggregate['move_ids']: + last_move = last_move.browse(aml_aggregate['move_ids'][0]) + + return {'total_interval': total_interval, + 'cumulative_total': cumulative_total, + 'last_move_id': last_move.id, + 'last_move_hash': last_move.l10n_fr_hash, + 'date_closing_stop': interval_dates['date_stop'], + 'date_closing_start': date_start, + 'name': interval_dates['name_interval'] + ' - ' + interval_dates['date_stop'][:10]} + + def _interval_dates(self, frequency, company): + """ + Method used to compute the theoretical date from which account move lines should be fetched + @param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually) + frequencies are literal (daily means 24 hours and so on) + @param {recordset} company: the company for which the closing is done + @return {dict} the theoretical date from which account move lines are fetched. + date_stop date to which the move lines are fetched, always now() + the dates are in their Odoo Database string representation + """ + date_stop = datetime.utcnow() + interval_from = None + name_interval = '' + if frequency == 'daily': + interval_from = date_stop - timedelta(days=1) + name_interval = _('Daily Closing') + elif frequency == 'monthly': + month_target = date_stop.month > 1 and date_stop.month - 1 or 12 + year_target = month_target < 12 and date_stop.year or date_stop.year - 1 + interval_from = date_stop.replace(year=year_target, month=month_target) + name_interval = _('Monthly Closing') + elif frequency == 'annually': + year_target = date_stop.year - 1 + interval_from = date_stop.replace(year=year_target) + name_interval = _('Annual Closing') + + return {'interval_from': FieldDateTime.to_string(interval_from), + 'date_stop': FieldDateTime.to_string(date_stop), + 'name_interval': name_interval} + + @api.multi + def write(self, vals): + raise UserError(_('Sale Closings are not meant to be written or deleted under any circumstances.')) + + @api.multi + def unlink(self): + raise UserError(_('Sale Closings are not meant to be written or deleted under any circumstances.')) + + @api.model + def _automated_closing(self, frequency='daily'): + """To be executed by the CRON to create an object of the given frequency for each company that needs it + @param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually) + frequencies are literal (daily means 24 hours and so on) + @return {recordset} all the objects created for the given frequency + """ + res_company = self.env['res.company'].search([]) + account_closings = self.env['account.sale.closing'] + for company in res_company.filtered(lambda c: c._is_accounting_unalterable()): + new_sequence_number = company.l10n_fr_closing_sequence_id.next_by_id() + values = self._compute_amounts(frequency, company) + values['frequency'] = frequency + values['company_id'] = company.id + values['sequence_number'] = new_sequence_number + account_closings |= account_closings.create(values) + + return account_closings diff --git a/addons/l10n_fr_sale_closing/models/res_company.py b/addons/l10n_fr_sale_closing/models/res_company.py new file mode 100644 index 00000000000..7a5a14b46c9 --- /dev/null +++ b/addons/l10n_fr_sale_closing/models/res_company.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp import fields, models, api + + +class ResCompany(models.Model): + _inherit = 'res.company' + + l10n_fr_closing_sequence_id = fields.Many2one('ir.sequence', 'Sequence to use to build sale closings', readonly=True) + + @api.model + def create(self, vals): + company = super(ResCompany, self).create(vals) + #when creating a new french company, create the securisation sequence as well + if company._is_accounting_unalterable(): + sequence_fields = ['l10n_fr_closing_sequence_id'] + company._create_secure_sequence(sequence_fields) + return company + + @api.multi + def write(self, vals): + res = super(ResCompany, self).write(vals) + #if country changed to fr, create the securisation sequence + for company in self: + if company._is_accounting_unalterable(): + sequence_fields = ['l10n_fr_closing_sequence_id'] + company._create_secure_sequence(sequence_fields) + return res diff --git a/addons/l10n_fr_sale_closing/security/account_closing_intercompany.xml b/addons/l10n_fr_sale_closing/security/account_closing_intercompany.xml new file mode 100644 index 00000000000..e5a36f61479 --- /dev/null +++ b/addons/l10n_fr_sale_closing/security/account_closing_intercompany.xml @@ -0,0 +1,8 @@ + + + Sale Closing multi-company + + [('company_id','child_of',[user.company_id.id])] + + + \ No newline at end of file diff --git a/addons/l10n_fr_sale_closing/security/ir.model.access.csv b/addons/l10n_fr_sale_closing/security/ir.model.access.csv new file mode 100644 index 00000000000..32a4e37f3d0 --- /dev/null +++ b/addons/l10n_fr_sale_closing/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_l10n_fr_sale_closing_account_sale_closing_user,l10n_fr_sale_closing.account.sale.closing.user,l10n_fr_sale_closing.model_account_sale_closing,base.group_user,1,0,0,0 \ No newline at end of file diff --git a/addons/l10n_fr_sale_closing/static/description/icon.png b/addons/l10n_fr_sale_closing/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..bbcaa4fcce7321598e556ae85fef65b0ed74fbdb GIT binary patch literal 562 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSY)RhkE>8(%y`dEVi!oTtiUlHNH>77!9&AY3=E8Bo-U3d6}R4AGW2y$lyJDXp1;N53Y(Th zU{h1xR)-+gGZxC3a-|pg>StB=HCf-01nPx?2Rmh-z5QKZdc^s9t(|>evE}~r+hoJ8 z-`W2D^Y_wOug||f^ZN9wS?$}dGOkH&uwXtSsc?>=kgdbmVGiRX9)a|sCIs^~ke+v! zw^xXlV*|_dC?HR@#5JNMC9x#cD!C{XNHG{07#issnCKc9g&3Mx85>&}nQ9vtSs56t zpT<#(q9HdwB{QuOO@o1{m60Jt!@i29DL@U9ARB`7(@M${i&7cN%ggmL^RkPR6AM!H X@{7`Ezq647Dq`?-^>bP0l+XkK28gaM literal 0 HcmV?d00001 diff --git a/addons/l10n_fr_sale_closing/views/account_sale_closure.xml b/addons/l10n_fr_sale_closing/views/account_sale_closure.xml new file mode 100644 index 00000000000..97ddeb05056 --- /dev/null +++ b/addons/l10n_fr_sale_closing/views/account_sale_closure.xml @@ -0,0 +1,68 @@ + + + Sales Closings + account.sale.closing + + + + + + + + + + + + + + + + Sales Closings + account.sale.closing + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + +
+
+
+
+ + + Sales Closings + account.sale.closing + tree + list,form + +

+ The closings are created by Odoo +

+ Sales closings run automatically on a daily, monthly and annual basis. It computes both period and cumulative totals from all the sales entries posted in the system after the previous closing. +

+
+
+ + +
From 849a10b1da09aac4f204c6f75596c1ef520d0139 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 1 Dec 2017 18:12:54 +0100 Subject: [PATCH 11/12] [FIX] website_event_sale: registration visibility - When all the tickets in an event expire, the form is still visible, wich is confusing for users. This closes #21384 --- .../website_event_sale/views/website_event_sale_templates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website_event_sale/views/website_event_sale_templates.xml b/addons/website_event_sale/views/website_event_sale_templates.xml index d0005892fd6..a1a648ce738 100644 --- a/addons/website_event_sale/views/website_event_sale_templates.xml +++ b/addons/website_event_sale/views/website_event_sale_templates.xml @@ -88,7 +88,7 @@ class="btn btn-primary btn-lg pull-right a-submit" t-attf-id="#{event.id}">Order Now - event.event_ticket_ids + event.event_ticket_ids and not all([ticket.is_expired for ticket in event.event_ticket_ids]) From 228f6086b1f06fca246c80e170e6d155ec256e60 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 14 Dec 2017 12:55:32 +0100 Subject: [PATCH 12/12] [FIX] l10n_fr_pos_cert: correct dependency --- addons/l10n_fr_pos_cert/__openerp__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/l10n_fr_pos_cert/__openerp__.py b/addons/l10n_fr_pos_cert/__openerp__.py index fd9ae3459e8..40907b49621 100644 --- a/addons/l10n_fr_pos_cert/__openerp__.py +++ b/addons/l10n_fr_pos_cert/__openerp__.py @@ -21,7 +21,7 @@ The module adds following features: Access to download the mandatory Certificate of Conformity delivered by Odoo SA (only for Odoo Enterprise users) """, - 'depends': ['l10n_fr_certification', 'point_of_sale'], + 'depends': ['l10n_fr_sale_closing', 'point_of_sale'], 'installable': True, 'auto_install': False, 'application': False,