diff --git a/addons/l10n_fr_certification/__init__.py b/addons/l10n_fr_certification/__init__.py
new file mode 100644
index 00000000000..24644871f1e
--- /dev/null
+++ b/addons/l10n_fr_certification/__init__.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import models
+from openerp import api, SUPERUSER_ID
+
+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')])
+ if fr_companies:
+ # create the securisation sequence per company
+ fr_companies._create_secure_sequence()
+
+ #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})
diff --git a/addons/l10n_fr_certification/__openerp__.py b/addons/l10n_fr_certification/__openerp__.py
new file mode 100644
index 00000000000..e83e9102914
--- /dev/null
+++ b/addons/l10n_fr_certification/__openerp__.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+{
+ 'name': 'France - Accounting - Certified CGI 286 I-3 bis',
+ 'version': '1.0',
+ 'category': 'Localization',
+ 'description': """
+""",
+ 'depends': ['l10n_fr'],
+ 'installable': True,
+ 'auto_install': True,
+ 'application': True,
+ 'data': [
+ 'views/no_cancel.xml',
+ 'data/account_move.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
new file mode 100644
index 00000000000..95adc8d8c2e
--- /dev/null
+++ b/addons/l10n_fr_certification/data/account_move.xml
@@ -0,0 +1,13 @@
+
+
+ Intégrité des données comptables
+
+ ir.actions.server
+
+ action = env['account.move']._check_hash_integrity(env.user.company_id.id)
+
+
+
+
+
+
diff --git a/addons/l10n_fr_certification/models/__init__.py b/addons/l10n_fr_certification/models/__init__.py
new file mode 100644
index 00000000000..17a7d5cf526
--- /dev/null
+++ b/addons/l10n_fr_certification/models/__init__.py
@@ -0,0 +1,2 @@
+import res_company
+import account
diff --git a/addons/l10n_fr_certification/models/account.py b/addons/l10n_fr_certification/models/account.py
new file mode 100644
index 00000000000..8d1a26d0ad6
--- /dev/null
+++ b/addons/l10n_fr_certification/models/account.py
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+from hashlib import sha256
+from json import dumps
+
+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']
+LINE_FIELDS = ['debit', 'credit', 'account_id', 'move_id', 'partner_id']
+
+
+class AccountMove(models.Model):
+ _inherit = "account.move"
+
+ l10n_fr_secure_sequence_number = fields.Integer(readonly=True)
+ l10n_fr_hash = fields.Char(readonly=True)
+ 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 journal entries when they get posted"""
+ self.ensure_one()
+ #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', '=', 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'))
+
+ #build and return the hash
+ return self._compute_hash(prev_move.l10n_fr_hash if prev_move 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
+ return str(field_value)
+
+ for move in self:
+ values = {}
+ for field in MOVE_FIELDS:
+ values[field] = _getattrstring(move, field)
+
+ for line in move.line_ids:
+ for field in LINE_FIELDS:
+ values[field] = _getattrstring(line, field)
+ #make the json serialization canonical
+ # (https://tools.ietf.org/html/draft-staykov-hu-json-canonical-form-00)
+ move.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 move in self:
+ if move.company_id.country_id.code == 'FR':
+ # 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)))
+ # 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)):
+ 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
+
+ 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.'))
+ super(AccountMove, self).button_cancel()
+
+ @api.model
+ def _check_hash_integrity(self, company_id):
+ """Checks that all posted moves have still the same data as when they were posted
+ and raises an error with the result.
+ """
+ moves = self.search([('state', '=', 'posted'),
+ ('company_id', '=', company_id),
+ ('l10n_fr_secure_sequence_number', '!=', False)],
+ order="l10n_fr_secure_sequence_number ASC")
+
+ if not moves:
+ raise UserError(_('Warning: impossible to find any move with a hash.'))
+ 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)
+ 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]
+ 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.
+
+ 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]))
+
+
+class AccountMoveLine(models.Model):
+ _inherit = "account.move.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.country_id.code == 'FR' and l.move_id.state == 'posted' for l in self):
+ raise UserError(ERR_MSG % (self._name, ', '.join(LINE_FIELDS)))
+ return super(AccountMoveLine, self).write(vals)
+
+
+class AccountJournal(models.Model):
+ _inherit = "account.journal"
+
+ @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'))
+ 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'))
+ 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
new file mode 100644
index 00000000000..66a2a633164
--- /dev/null
+++ b/addons/l10n_fr_certification/models/res_company.py
@@ -0,0 +1,42 @@
+# -*- 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_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()
+ 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()
+ return res
+
+ def _create_secure_sequence(self):
+ """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})
diff --git a/addons/l10n_fr_certification/views/no_cancel.xml b/addons/l10n_fr_certification/views/no_cancel.xml
new file mode 100644
index 00000000000..d732364b189
--- /dev/null
+++ b/addons/l10n_fr_certification/views/no_cancel.xml
@@ -0,0 +1,36 @@
+
+
+ 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