[MERGE] forward port branch 9.0 up to f4bd1fd9d2
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user