diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index a3a27f05540..c513cbdbc18 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1269,9 +1269,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) diff --git a/addons/account_asset/account_asset.py b/addons/account_asset/account_asset.py index a0f19fb2788..c0e98ccddb5 100644 --- a/addons/account_asset/account_asset.py +++ b/addons/account_asset/account_asset.py @@ -460,7 +460,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)) move_line_1 = { 'name': asset_name, 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 diff --git a/addons/pos_restaurant/static/src/js/floors.js b/addons/pos_restaurant/static/src/js/floors.js index ab2de451d03..50b750756a3 100644 --- a/addons/pos_restaurant/static/src/js/floors.js +++ b/addons/pos_restaurant/static/src/js/floors.js @@ -833,12 +833,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); } }, 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