From 71d54a0afef2940b8f5cf418c59a56569bbf3132 Mon Sep 17 00:00:00 2001 From: Carlos Mayo Date: Mon, 12 Jun 2017 10:46:57 -0700 Subject: [PATCH 1/5] [CLA] Fairhall Solutions Done at #17563 --- doc/cla/corporate/fairhall.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 doc/cla/corporate/fairhall.md diff --git a/doc/cla/corporate/fairhall.md b/doc/cla/corporate/fairhall.md new file mode 100644 index 00000000000..b075820986c --- /dev/null +++ b/doc/cla/corporate/fairhall.md @@ -0,0 +1,15 @@ +Spain, 2017-06-12 + +Fairhall Solutions, S.L. agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Harsh Sharma harsh@fairhall.es https://github.com/fairhall + +List of contributors: + +Carlos Mayo carlos.mayo@fairhall.es https://github.com/cmayo \ No newline at end of file From f4ed08ceda1273870b6e5544b6bce53ba34a518a Mon Sep 17 00:00:00 2001 From: Carlos Mayo Date: Mon, 12 Jun 2017 10:50:29 -0700 Subject: [PATCH 2/5] [FIX] pos_restaurant: add_new_order should return new order object Closes #17563 --- addons/pos_restaurant/static/src/js/floors.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/addons/pos_restaurant/static/src/js/floors.js b/addons/pos_restaurant/static/src/js/floors.js index 365188fd8ea..2cb35afb897 100644 --- a/addons/pos_restaurant/static/src/js/floors.js +++ b/addons/pos_restaurant/static/src/js/floors.js @@ -817,12 +817,13 @@ models.PosModel = models.PosModel.extend({ add_new_order: function() { if (this.config.iface_floorplan) { if (this.table) { - _super_posmodel.add_new_order.call(this); + return _super_posmodel.add_new_order.call(this); } else { console.warn("WARNING: orders cannot be created when there is no active table in restaurant mode"); + return undefined; } } else { - _super_posmodel.add_new_order.apply(this,arguments); + return _super_posmodel.add_new_order.apply(this,arguments); } }, From 05ef0790c955b1b7fcde85bb66b8451cb687d71b Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Tue, 13 Jun 2017 14:35:11 +0200 Subject: [PATCH 3/5] [FIX] account_asset: depreciation rate date - Activate multi-currency - Define a rate today (e.g. 2017-06-01) and next month (e.g. 2017-07-01) - Create a deferred revenue on 2017-07-02 in the foreign currency - Post the first line The rate taken is the rate of today, not the rate of next month. opw-745509 --- addons/account_asset/account_asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/account_asset/account_asset.py b/addons/account_asset/account_asset.py index 68bbbb33167..e0865a77981 100644 --- a/addons/account_asset/account_asset.py +++ b/addons/account_asset/account_asset.py @@ -431,7 +431,7 @@ class AccountAssetDepreciationLine(models.Model): depreciation_date = self.env.context.get('depreciation_date') or line.depreciation_date or fields.Date.context_today(self) company_currency = line.asset_id.company_id.currency_id current_currency = line.asset_id.currency_id - amount = current_currency.compute(line.amount, company_currency) + amount = current_currency.with_context(date=depreciation_date).compute(line.amount, company_currency) asset_name = line.asset_id.name + ' (%s/%s)' % (line.sequence, len(line.asset_id.depreciation_line_ids)) reference = line.asset_id.code journal_id = line.asset_id.category_id.journal_id.id From 944f8e33537c69d88f2c1ea6fe6207059caaee6e Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Tue, 13 Jun 2017 15:45:20 +0200 Subject: [PATCH 4/5] [FIX] account: improve performance when validating an invoice When doing a delete, the cache is cleared, putting an unlink in a loop multiplies the number of requests made (for access rules, relational field, recompute ...) by the number of rows. Doing it outside the loop speeds up the validation of the invoice. Backport of 10.0's 2687571781a9518323b3c08ea7772ecb56eb39d1 --- addons/account/models/account_move.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index f7c00e65dd3..8232d99f071 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1243,9 +1243,8 @@ class AccountMoveLine(models.Model): """ Create analytic items upon validation of an account.move.line having an analytic account. This method first remove any existing analytic item related to the line before creating any new one. """ + self.mapped('analytic_line_ids').unlink() for obj_line in self: - if obj_line.analytic_line_ids: - obj_line.analytic_line_ids.unlink() if obj_line.analytic_account_id: vals_line = obj_line._prepare_analytic_line()[0] self.env['account.analytic.line'].create(vals_line) From f4bd1fd9d27de082fb17f1199a1e956673f1db7e Mon Sep 17 00:00:00 2001 From: qdp-odoo Date: Wed, 14 Jun 2017 14:09:24 +0200 Subject: [PATCH 5/5] [ADD] l10n_fr_certification: module to ensure the inalterability of accounting in France. --- addons/l10n_fr_certification/__init__.py | 33 ++++ addons/l10n_fr_certification/__openerp__.py | 19 +++ .../data/account_move.xml | 13 ++ .../l10n_fr_certification/models/__init__.py | 2 + .../l10n_fr_certification/models/account.py | 155 ++++++++++++++++++ .../models/res_company.py | 42 +++++ .../l10n_fr_certification/views/no_cancel.xml | 36 ++++ 7 files changed, 300 insertions(+) create mode 100644 addons/l10n_fr_certification/__init__.py create mode 100644 addons/l10n_fr_certification/__openerp__.py create mode 100644 addons/l10n_fr_certification/data/account_move.xml create mode 100644 addons/l10n_fr_certification/models/__init__.py create mode 100644 addons/l10n_fr_certification/models/account.py create mode 100644 addons/l10n_fr_certification/models/res_company.py create mode 100644 addons/l10n_fr_certification/views/no_cancel.xml 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