[MERGE] forward port branch 9.0 up to f4bd1fd9d2

This commit is contained in:
Christophe Simonis
2017-06-14 16:42:49 +02:00
11 changed files with 320 additions and 5 deletions
+1 -2
View File
@@ -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)
+1 -1
View File
@@ -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,
+33
View File
@@ -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})
@@ -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',
}
@@ -0,0 +1,13 @@
<odoo>
<record model="ir.actions.server" id="action_check_hash_integrity">
<field name="name">Intégrité des données comptables</field>
<field name="model_id" ref="account.model_account_move"/>
<field name="type">ir.actions.server</field>
<field name="code">
action = env['account.move']._check_hash_integrity(env.user.company_id.id)
</field>
</record>
<menuitem action="action_check_hash_integrity" id="menu_check_move_integrity" parent="l10n_fr.account_reports_fr_statements_menu" sequence="90"/>
</odoo>
@@ -0,0 +1,2 @@
import res_company
import account
@@ -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)
@@ -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})
@@ -0,0 +1,36 @@
<odoo>
<record id="bank_statement_draft_form_inherit" model="ir.ui.view">
<field name="name">bank.statement.draft.form.inherit</field>
<field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account.view_bank_statement_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='button_cancel']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
</field>
</record>
<record id="invoice_form_no_cancel_inherit" model="ir.ui.view">
<field name="name">invoice.form.cancel.inherit</field>
<field name="model">account.invoice</field>
<field name="inherit_id" ref="account.invoice_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='invoice_cancel']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
</field>
</record>
<record id="view_move_form_no_cancel_inherit" model="ir.ui.view">
<field name="name">account.move.form.nocancel.inherit</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='button_cancel']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
</field>
</record>
</odoo>
@@ -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);
}
},
+15
View File
@@ -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