[MERGE] forward port branch 9.0 up to 228f6086b1

This commit is contained in:
Christophe Simonis
2017-12-14 13:24:01 +01:00
31 changed files with 1373 additions and 101 deletions
-2
View File
@@ -145,13 +145,11 @@ class AccountConfigSettings(models.TransientModel):
'-This installs the module account_bank_statement_import_csv.')
overdue_msg = fields.Text(related='company_id.overdue_msg', string='Overdue Payments Message *')
@api.model
def _default_has_default_company(self):
count = self.env['res.company'].search_count([])
return bool(count == 1)
@api.onchange('company_id')
def onchange_company_id(self):
# update related fields
+5 -17
View File
@@ -3,31 +3,19 @@
import models
from openerp import api, SUPERUSER_ID
from models.res_company import UNALTERABLE_COUNTRIES
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')])
fr_companies = env['res.company'].search([('partner_id.country_id.code', 'in', UNALTERABLE_COUNTRIES)])
if fr_companies:
# create the securisation sequence per company
fr_companies._create_secure_sequence()
fr_companies._create_secure_sequence(['l10n_fr_secure_sequence_id'])
#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})
journals.write({'update_posted': False})
+15 -3
View File
@@ -2,18 +2,30 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'France - Accounting - Certified CGI 286 I-3 bis',
'name': 'France - VAT Anti-Fraud Certification (CGI 286 I-3 bis)',
'version': '1.0',
'category': 'Localization',
'description': """
This add-on brings the technical requirements of the French regulation CGI art. 286, I. 3° bis that stipulates certain criteria concerning the inalterability, security, storage and archiving of data related to sales to private individuals (B2C).
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
The module adds following features:
Inalterability: deactivation of all the ways to cancel or modify key data, invoices and journal entries
Security: chaining algorithm to verify the inalterability
Storage: automatic sales closings with computation of both period and cumulative totals (daily, monthly, annually)
Access to download the mandatory Certificate of Conformity delivered by Odoo SA (only for Odoo Enterprise users)
""",
'depends': ['l10n_fr'],
'installable': True,
'auto_install': False,
'application': True,
'application': False,
'data': [
'views/no_cancel.xml',
'data/account_move.xml',
'views/res_config.xml',
],
'post_init_hook': '_setup_inalterability',
}
@@ -1,6 +1,6 @@
<odoo>
<record model="ir.actions.server" id="action_check_hash_integrity">
<field name="name">Intégrité des données comptables</field>
<field name="name">Data Inalterability Check</field>
<field name="model_id" ref="account.model_account_move"/>
<field name="type">ir.actions.server</field>
<field name="code">
@@ -8,6 +8,18 @@
</field>
</record>
<menuitem action="action_check_hash_integrity" id="menu_check_move_integrity" parent="l10n_fr.account_reports_fr_statements_menu" sequence="90"/>
<record model="ir.actions.server" id="odoo_french_certification">
<field name="name">Get the certification now!</field>
<field name="type">ir.actions.server</field>
<field name="model_id" ref="account.model_res_company"/>
<field name="code">
db_uuid = env['ir.config_parameter'].sudo().get_param('database.uuid')
action = {"type": "ir.actions.act_url",
"url": "https://www.odoo.com/my/contract/french-certification/db/" + db_uuid,
"target": "new",}
</field>
</record>
<menuitem action="action_check_hash_integrity" id="menu_check_move_integrity_reporting" parent="l10n_fr.account_reports_fr_statements_menu" sequence="90"/>
</odoo>
@@ -0,0 +1,155 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * l10n_fr_certification
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 9.0c\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-12-06 16:03+0000\n"
"PO-Revision-Date: 2017-12-06 16:03+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: l10n_fr_certification
#: code:addons/l10n_fr_certification/models/account.py:108
#, python-format
msgid "(ref.: %s)"
msgstr ""
#. module: l10n_fr_certification
#: code:addons/l10n_fr_certification/models/account.py:79
#, python-format
msgid "According to the French law, you cannot modify a journal entry in order for its posted data to be updated or deleted. Unauthorized field: %s."
msgstr ""
#. module: l10n_fr_certification
#: code:addons/l10n_fr_certification/models/account.py:165
#: code:addons/l10n_fr_certification/models/account.py:185
#: code:addons/l10n_fr_certification/models/account.py:194
#, python-format
msgid "According to the French law, you cannot modify a journal in order for its posted data to be updated or deleted. Unauthorized field: %s."
msgstr ""
#. module: l10n_fr_certification
#: code:addons/l10n_fr_certification/models/account.py:154
#, python-format
msgid "According to the French law, you cannot modify a journal item in order for its posted data to be updated or deleted. Unauthorized field: %s."
msgstr ""
#. module: l10n_fr_certification
#: model:ir.model,name:l10n_fr_certification.model_account_move
msgid "Account Entry"
msgstr ""
#. module: l10n_fr_certification
#: code:addons/l10n_fr_certification/models/account.py:34
#, python-format
msgid "An error occured when computing the inalterability. Impossible to get the unique previous posted journal entry."
msgstr ""
#. module: l10n_fr_certification
#: model:ir.model,name:l10n_fr_certification.model_res_company
msgid "Companies"
msgstr ""
#. module: l10n_fr_certification
#: code:addons/l10n_fr_certification/models/account.py:123
#, python-format
msgid "Corrupted data on journal entry with id %s."
msgstr ""
#. module: l10n_fr_certification
#: model:ir.ui.view,arch_db:l10n_fr_certification.view_general_fr_inalterability_inherit_config
msgid "Data Inalterability"
msgstr ""
#. module: l10n_fr_certification
#: model:ir.actions.server,name:l10n_fr_certification.action_check_hash_integrity
#: model:ir.ui.menu,name:l10n_fr_certification.menu_check_move_integrity
#: model:ir.ui.menu,name:l10n_fr_certification.menu_check_move_integrity_reporting
#: model:ir.ui.view,arch_db:l10n_fr_certification.view_general_fr_inalterability_inherit_config
msgid "Data Inalterability Check"
msgstr ""
#. module: l10n_fr_certification
#: model:ir.ui.menu,name:l10n_fr_certification.inalterability_fr_statements_menu
msgid "French Statements"
msgstr ""
#. module: l10n_fr_certification
#: model:ir.actions.server,name:l10n_fr_certification.odoo_french_certification
#: model:ir.ui.view,arch_db:l10n_fr_certification.view_general_fr_inalterability_inherit_config
msgid "Get the certification now!"
msgstr ""
#. module: l10n_fr_certification
#: model:ir.model.fields,field_description:l10n_fr_certification.field_account_move_l10n_fr_hash
msgid "Inalterability Hash"
msgstr ""
#. module: l10n_fr_certification
#: model:ir.model.fields,field_description:l10n_fr_certification.field_account_move_l10n_fr_secure_sequence_number
msgid "Inalteralbility No Gap Sequence #"
msgstr ""
#. module: l10n_fr_certification
#: code:addons/l10n_fr_certification/models/account.py:175
#, python-format
msgid "It is not permitted to disable the data inalterability in this journal (%s) since journal entries have already been protected."
msgstr ""
#. module: l10n_fr_certification
#: model:ir.model,name:l10n_fr_certification.model_account_journal
msgid "Journal"
msgstr ""
#. module: l10n_fr_certification
#: model:ir.model,name:l10n_fr_certification.model_account_move_line
msgid "Journal Item"
msgstr ""
#. module: l10n_fr_certification
#: model:ir.model.fields,field_description:l10n_fr_certification.field_account_move_l10n_fr_string_to_hash
msgid "L10n fr string to hash"
msgstr ""
#. module: l10n_fr_certification
#: model:ir.model.fields,field_description:l10n_fr_certification.field_res_company_l10n_fr_secure_sequence_id
msgid "Sequence to use to ensure the securisation of data"
msgstr ""
#. module: l10n_fr_certification
#: code:addons/l10n_fr_certification/models/account.py:136
#, python-format
msgid "Successful test !\n"
"\n"
" The journal entries are guaranteed to be in their original and inalterable state\n"
" From: %(start_move_name)s %(start_move_ref)s\n"
" To: %(end_move_name)s %(end_move_ref)s\n"
"\n"
" For this report to be legally meaningful, please download your certification from your customer account on Odoo.com (Only for Odoo Enterprise users)."
msgstr ""
#. module: l10n_fr_certification
#: code:addons/l10n_fr_certification/models/account.py:118
#, python-format
msgid "There isn't any journal entry flagged for data inalterability yet for the company %s. This mechanism only runs for journal entries generated after the installation of the module France - Certification CGI 286 I-3 bis."
msgstr ""
#. module: l10n_fr_certification
#: code:addons/l10n_fr_certification/models/account.py:99
#, python-format
msgid "You cannot modify a posted journal entry. This ensures its inalterability."
msgstr ""
#. module: l10n_fr_certification
#: code:addons/l10n_fr_certification/models/account.py:82
#, python-format
msgid "You cannot overwrite the values ensuring the inalterability of the accounting."
msgstr ""
+64 -26
View File
@@ -6,7 +6,6 @@ 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']
@@ -16,8 +15,10 @@ LINE_FIELDS = ['debit', 'credit', 'account_id', 'partner_id']
class AccountMove(models.Model):
_inherit = "account.move"
l10n_fr_secure_sequence_number = fields.Integer(readonly=True, copy=False)
l10n_fr_hash = fields.Char(readonly=True, copy=False)
# TO DO in master : refactor hashing algo to go into a mixin
l10n_fr_secure_sequence_number = fields.Integer(string="Inalteralbility No Gap Sequence #", readonly=True, copy=False)
l10n_fr_hash = fields.Char(string="Inalterability Hash", readonly=True, copy=False)
l10n_fr_string_to_hash = fields.Char(compute='_compute_string_to_hash', readonly=True, store=False)
def _get_new_hash(self, secure_seq_number):
@@ -26,10 +27,11 @@ class AccountMove(models.Model):
#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', '!=', 0),
('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'))
_('An error occured when computing the inalterability. Impossible to get the unique previous posted journal entry.'))
#build and return the hash
return self._compute_hash(prev_move.l10n_fr_hash if prev_move else '')
@@ -67,33 +69,34 @@ class AccountMove(models.Model):
def write(self, vals):
has_been_posted = False
for move in self:
if move.company_id.country_id.code == 'FR':
if move.company_id._is_accounting_unalterable():
# 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)))
raise UserError(_("According to the French law, you cannot modify a journal entry in order for its posted data to be updated or deleted. Unauthorized field: %s.") % ', '.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)):
for move in self.filtered(lambda m: m.company_id._is_accounting_unalterable() 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
@api.multi
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.'))
if self.company_id._is_accounting_unalterable():
raise UserError(_('You cannot modify a posted journal entry. This ensures its inalterability.'))
super(AccountMove, self).button_cancel()
@api.model
@@ -101,29 +104,43 @@ class AccountMove(models.Model):
"""Checks that all posted moves have still the same data as when they were posted
and raises an error with the result.
"""
def build_move_info(move):
entry_reference = _('(ref.: %s)')
move_reference_string = move.ref and entry_reference % move.ref or ''
return [move.name, move_reference_string]
moves = self.search([('state', '=', 'posted'),
('company_id', '=', company_id),
('l10n_fr_secure_sequence_number', '!=', 0)],
order="l10n_fr_secure_sequence_number ASC")
if not moves:
raise UserError(_('Warning: impossible to find any move with a hash.'))
raise UserError(_('There isn\'t any journal entry flagged for data inalterability yet for the company %s. This mechanism only runs for journal entries generated after the installation of the module France - Certification CGI 286 I-3 bis.') % self.env.user.company_id.name)
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)
raise UserError(_('Corrupted data on journal entry with id %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]
start_move_info = build_move_info(move)
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.
end_move_info = build_move_info(move)
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]))
report_dict = {'start_move_name': start_move_info[0],
'start_move_ref': start_move_info[1],
'end_move_name': end_move_info[0],
'end_move_ref': end_move_info[1]}
# Raise on success
raise UserError(_('''Successful test !
The journal entries are guaranteed to be in their original and inalterable state
From: %(start_move_name)s %(start_move_ref)s
To: %(end_move_name)s %(end_move_ref)s
For this report to be legally meaningful, please download your certification from your customer account on Odoo.com (Only for Odoo Enterprise users).'''
) % report_dict)
class AccountMoveLine(models.Model):
@@ -133,25 +150,46 @@ class AccountMoveLine(models.Model):
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)))
if any(l.company_id._is_accounting_unalterable() and l.move_id.state == 'posted' for l in self):
raise UserError(_("According to the French law, you cannot modify a journal item in order for its posted data to be updated or deleted. Unauthorized field: %s.") % ', '.join(LINE_FIELDS))
return super(AccountMoveLine, self).write(vals)
class AccountJournal(models.Model):
_inherit = "account.journal"
@api.onchange('update_posted')
def _onchange_update_posted(self):
if self.update_posted and self.company_id._is_accounting_unalterable():
field_string = self._fields['update_posted'].get_description(self.env)['string']
raise UserError(_("According to the French law, you cannot modify a journal in order for its posted data to be updated or deleted. Unauthorized field: %s.") % field_string)
@api.multi
def _is_journal_alterable(self):
self.ensure_one()
critical_domain = [('journal_id', '=', self.id),
'|', ('l10n_fr_hash', '!=', False),
'&', ('l10n_fr_secure_sequence_number', '!=', False),
('l10n_fr_secure_sequence_number', '!=', 0)]
if self.env['account.move'].search(critical_domain):
raise UserError(_('It is not permitted to disable the data inalterability in this journal (%s) since journal entries have already been protected.') % (self.name, ))
return True
@api.multi
def write(self, vals):
# restrict the operation in case we are trying to write a forbidden field
for rec in self:
if rec.company_id.country_id.code == 'FR' and vals.get('update_posted'):
raise UserError(ERR_MSG % (self._name, 'update_posted'))
for journal in self:
if journal.company_id._is_accounting_unalterable():
if vals.get('update_posted'):
field_string = journal._fields['update_posted'].get_description(self.env)['string']
raise UserError(_("According to the French law, you cannot modify a journal in order for its posted data to be updated or deleted. Unauthorized field: %s.") % field_string)
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'))
if self.company_id._is_accounting_unalterable():
if vals.get('update_posted'):
field_string = self._fields['update_posted'].get_description(self.env)['string']
raise UserError(_("According to the French law, you cannot modify a journal in order for its posted data to be updated or deleted. Unauthorized field: %s.") % field_string)
return super(AccountJournal, self).create(vals)
@@ -3,40 +3,61 @@
from openerp import fields, models, api
UNALTERABLE_COUNTRIES = ['FR', 'MF', 'MQ', 'NC', 'PF', 'RE', 'GF', 'GP', 'TF']
class ResCompany(models.Model):
_inherit = 'res.company'
# To do in master : refactor to set sequences more generic
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()
if company._is_accounting_unalterable():
sequence_fields = ['l10n_fr_secure_sequence_id']
company._create_secure_sequence(sequence_fields)
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()
for company in self:
if company._is_accounting_unalterable():
sequence_fields = ['l10n_fr_secure_sequence_id']
company._create_secure_sequence(sequence_fields)
return res
def _create_secure_sequence(self):
def _create_secure_sequence(self, sequence_fields):
"""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})
vals_write = {}
for seq_field in sequence_fields:
if not company[seq_field]:
vals = {
'name': 'French Securisation of ' + seq_field + ' - ' + company.name,
'code': 'FRSECUR',
'implementation': 'no_gap',
'prefix': '',
'suffix': '',
'padding': 0,
'company_id': company.id}
seq = self.env['ir.sequence'].create(vals)
vals_write[seq_field] = seq.id
if vals_write:
company.write(vals_write)
def _is_vat_french(self):
return self.vat and self.vat.startswith('FR') and len(self.vat) == 13
def _is_accounting_unalterable(self):
if not self.vat and not self.country_id:
return False
return self.country_id and self.country_id.code in UNALTERABLE_COUNTRIES or self._is_vat_french()
Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

@@ -1,36 +0,0 @@
<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='action_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>
@@ -0,0 +1,18 @@
<odoo>
<record id="view_general_fr_inalterability_inherit_config" model="ir.ui.view">
<field name="name">Inalterability settings</field>
<field name="model">base.config.settings</field>
<field name="inherit_id" ref="base_setup.view_general_configuration"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='company_share_ressources']" position="before">
<group>
<label for="id" string="Data Inalterability"/>
<div>
<button name="%(l10n_fr_certification.action_check_hash_integrity)d" type="action" string="Data Inalterability Check" class="oe_link"/>
<button name="%(l10n_fr_certification.odoo_french_certification)d" type="action" string="Get the certification now!" class="oe_link"/>
</div>
</group>
</xpath>
</field>
</record>
</odoo>
+17
View File
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import models
from openerp import api, SUPERUSER_ID
from ..l10n_fr_certification.models.res_company import UNALTERABLE_COUNTRIES
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)
fr_companies = env['res.company'].search([('partner_id.country_id.code', 'in', UNALTERABLE_COUNTRIES)])
if fr_companies:
# create the securisation sequence per company
fr_companies._create_secure_sequence(['l10n_fr_pos_cert_sequence_id'])
+33
View File
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'France - VAT Anti-Fraud Certification for Point of Sale (CGI 286 I-3 bis)',
'version': '1.0',
'category': 'Localization',
'description': """
This add-on brings the technical requirements of the French regulation CGI art. 286, I. 3° bis that stipulates certain criteria concerning the inalterability, security, storage and archiving of data related to sales to private individuals (B2C).
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Install it if you use the Point of Sale app to sell to individuals.
The module adds following features:
Inalterability: deactivation of all the ways to cancel or modify key data of POS orders, invoices and journal entries
Security: chaining algorithm to verify the inalterability
Storage: automatic sales closings with computation of both period and cumulative totals (daily, monthly, annually)
Access to download the mandatory Certificate of Conformity delivered by Odoo SA (only for Odoo Enterprise users)
""",
'depends': ['l10n_fr_sale_closing', 'point_of_sale'],
'installable': True,
'auto_install': False,
'application': False,
'data': [
'data/pos_inalterability.xml',
'views/account_views.xml',
],
'post_init_hook': '_setup_inalterability',
}
@@ -0,0 +1,17 @@
<odoo>
<menuitem id="pos_fr_statements_menu" name="French Statements" parent="point_of_sale.menu_point_rep" sequence="9" />
<menuitem action="l10n_fr_sale_closing.action_list_view_account_sale_closing" id="menu_account_closing" parent="pos_fr_statements_menu" sequence="90"/>
<record model="ir.actions.server" id="action_check_hash_integrity_pos_orders">
<field name="name">Pos Orders Inalterability Check</field>
<field name="model_id" ref="point_of_sale.model_pos_order"/>
<field name="type">ir.actions.server</field>
<field name="code">
action = env['pos.order']._check_hash_integrity(env.user.company_id.id)
</field>
</record>
<menuitem action="action_check_hash_integrity_pos_orders" id="menu_check_pos_order_integrity" parent="pos_fr_statements_menu" sequence="90"/>
</odoo>
@@ -0,0 +1,166 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * l10n_fr_pos_cert
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 9.0c\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-12-06 16:08+0000\n"
"PO-Revision-Date: 2017-12-06 16:08+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: l10n_fr_pos_cert
#: code:addons/l10n_fr_pos_cert/models/pos.py:145
#, python-format
msgid "(Receipt ref.: %s)"
msgstr ""
#. module: l10n_fr_pos_cert
#: code:addons/l10n_fr_pos_cert/models/pos.py:57
#, python-format
msgid "According to the French law, you cannot modify a %s. Forbidden fields: %s."
msgstr ""
#. module: l10n_fr_pos_cert
#: code:addons/l10n_fr_pos_cert/models/pos.py:193
#, python-format
msgid "According to the French law, you cannot modify a point of sale order line. Forbidden fields: %s."
msgstr ""
#. module: l10n_fr_pos_cert
#: code:addons/l10n_fr_pos_cert/models/pos.py:124
#, python-format
msgid "According to the French law, you cannot modify a point of sale order. Forbidden fields: %s."
msgstr ""
#. module: l10n_fr_pos_cert
#: code:addons/l10n_fr_pos_cert/models/pos.py:77
#, python-format
msgid "An error occured when computing the inalterability. Impossible to get the unique previous posted point of sale order."
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_account_bank_statement
msgid "Bank Statement"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_account_bank_statement_line
msgid "Bank Statement Line"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_res_company
msgid "Companies"
msgstr ""
#. module: l10n_fr_pos_cert
#: code:addons/l10n_fr_pos_cert/models/pos.py:160
#, python-format
msgid "Corrupted data on point of sale order with id %s."
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.ui.menu,name:l10n_fr_pos_cert.pos_fr_statements_menu
msgid "French Statements"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order_l10n_fr_hash
msgid "Inalteralbility Hash"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order_l10n_fr_secure_sequence_number
msgid "Inalteralbility No Gap Sequence #"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_res_company_l10n_fr_pos_cert_sequence_id
msgid "L10n fr pos cert sequence id"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order_l10n_fr_string_to_hash
msgid "L10n fr string to hash"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_pos_order_line
msgid "Lines of Point of Sale"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_pos_order
msgid "Point of Sale"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.actions.server,name:l10n_fr_pos_cert.action_check_hash_integrity_pos_orders
#: model:ir.ui.menu,name:l10n_fr_pos_cert.menu_check_pos_order_integrity
msgid "Pos Orders Inalterability Check"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.ui.menu,name:l10n_fr_pos_cert.menu_account_closing
msgid "Sales Closings"
msgstr ""
#. module: l10n_fr_pos_cert
#: code:addons/l10n_fr_pos_cert/models/pos.py:175
#, python-format
msgid "Successful test !\n"
"\n"
" The point of sale orders are guaranteed to be in their original and inalterable state\n"
" From: %(start_order_name)s %(start_order_ref)s recorded on %(start_order_date)s\n"
" To: %(end_order_name)s %(end_order_ref)s recorded on %(end_order_date)s\n"
"\n"
" For this report to be legally meaningful, please download your certification from your customer account on Odoo.com (Only for Odoo Enterprise users)."
msgstr ""
#. module: l10n_fr_pos_cert
#: code:addons/l10n_fr_pos_cert/models/pos.py:155
#, python-format
msgid "There isn't any order flagged for data inalterability yet for the company %s. This mechanism only runs for point of sale orders generated after the installation of the module France - Certification CGI 286 I-3 bis. - POS"
msgstr ""
#. module: l10n_fr_pos_cert
#: code:addons/l10n_fr_pos_cert/models/pos.py:45
#, python-format
msgid "This session has been opened another day. To comply with the French law, you should close sessions on a daily basis. Please close session %s and open a new one."
msgstr ""
#. module: l10n_fr_pos_cert
#: code:addons/l10n_fr_pos_cert/models/account_bank_statement.py:11
#, python-format
msgid "You cannot modify anything on a bank statement (name: %s) that was created by point of sale operations."
msgstr ""
#. module: l10n_fr_pos_cert
#: code:addons/l10n_fr_pos_cert/models/account_bank_statement.py:20
#, python-format
msgid "You cannot modify anything on a bank statement line (name: %s) that was created by point of sale operations."
msgstr ""
#. module: l10n_fr_pos_cert
#: code:addons/l10n_fr_pos_cert/models/pos.py:127
#, python-format
msgid "You cannot overwrite the values ensuring the inalterability of the point of sale."
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_pos_config
msgid "pos.config"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_pos_session
msgid "pos.session"
msgstr ""
@@ -0,0 +1,3 @@
import account_bank_statement
import res_company
import pos
@@ -0,0 +1,21 @@
from openerp import models, api
from openerp.tools.translate import _
from openerp.exceptions import UserError
class AccountBankStatement(models.Model):
_inherit = 'account.bank.statement'
def unlink(self):
for statement in self.filtered(lambda s: s.company_id._is_accounting_unalterable() and s.journal_id.journal_user):
raise UserError(_('You cannot modify anything on a bank statement (name: %s) that was created by point of sale operations.') % (statement.name,))
return super(AccountBankStatement, self).unlink()
class AccountBankStatementLine(models.Model):
_inherit = 'account.bank.statement.line'
def unlink(self):
for line in self.filtered(lambda s: s.company_id._is_accounting_unalterable() and s.journal_id.journal_user):
raise UserError(_('You cannot modify anything on a bank statement line (name: %s) that was created by point of sale operations.') % (line.name,))
return super(AccountBankStatementLine, self).unlink()
+194
View File
@@ -0,0 +1,194 @@
from datetime import datetime
from hashlib import sha256
from json import dumps
import pytz
from openerp import models, api, fields
from openerp.fields import Datetime
from openerp.tools.translate import _
from openerp.exceptions import UserError
def ctx_tz(record, field):
res_lang = None
ctx = record._context
tz_name = pytz.timezone(ctx.get('tz') or record.env.user.tz)
timestamp = Datetime.from_string(record[field])
if ctx.get('lang'):
res_lang = record.env['res.lang'].search([('code', '=', ctx['lang'])], limit=1)
if res_lang:
timestamp = pytz.utc.localize(timestamp, is_dst=False)
return datetime.strftime(timestamp.astimezone(tz_name), res_lang.date_format + ' ' + res_lang.time_format)
return Datetime.context_timestamp(record, timestamp)
class pos_config(models.Model):
_inherit = 'pos.config'
@api.multi
def open_ui(self):
for config in self.filtered(lambda c: c.company_id._is_accounting_unalterable()):
if config.current_session_id:
config.current_session_id._check_session_timing()
return super(pos_config, self).open_ui()
class pos_session(models.Model):
_inherit = 'pos.session'
@api.multi
def _check_session_timing(self):
self.ensure_one()
date_today = datetime.utcnow()
session_start = Datetime.from_string(self.start_at)
if not date_today - datetime.timedelta(hours=24) <= session_start:
raise UserError(_("This session has been opened another day. To comply with the French law, you should close sessions on a daily basis. Please close session %s and open a new one.") % self.name)
return True
@api.multi
def open_frontend_cb(self):
for session in self.filtered(lambda s: s.config_id.company_id._is_accounting_unalterable()):
session._check_session_timing()
return super(pos_session, self).open_frontend_cb()
ORDER_FIELDS = ['date_order', 'user_id', 'lines', 'statement_ids', 'pricelist_id', 'partner_id', 'session_id', 'pos_reference', 'sale_journal', 'fiscal_position_id']
LINE_FIELDS = ['notice', 'product_id', 'qty', 'price_unit', 'discount', 'tax_ids', 'tax_ids_after_fiscal_position']
ERR_MSG = _('According to the French law, you cannot modify a %s. Forbidden fields: %s.')
class pos_order(models.Model):
_inherit = 'pos.order'
l10n_fr_hash = fields.Char(string="Inalteralbility Hash", readonly=True, copy=False)
l10n_fr_secure_sequence_number = fields.Integer(string="Inalteralbility No Gap Sequence #", readonly=True, copy=False)
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 pos orders when they get posted"""
self.ensure_one()
#get the only one exact previous order in the securisation sequence
prev_order = self.search([('state', 'in', ['paid', 'done', 'invoiced']),
('company_id', '=', self.company_id.id),
('l10n_fr_secure_sequence_number', '!=', 0),
('l10n_fr_secure_sequence_number', '=', int(secure_seq_number) - 1)])
if prev_order and len(prev_order) != 1:
raise UserError(
_('An error occured when computing the inalterability. Impossible to get the unique previous posted point of sale order.'))
#build and return the hash
return self._compute_hash(prev_order.l10n_fr_hash if prev_order 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
if obj._fields[field_str].type in ['many2many', 'one2many']:
field_value = field_value.ids
return str(field_value)
for order in self:
values = {}
for field in ORDER_FIELDS:
values[field] = _getattrstring(order, field)
for line in order.lines:
for field in LINE_FIELDS:
k = 'line_%d_%s' % (line.id, field)
values[k] = _getattrstring(line, field)
#make the json serialization canonical
# (https://tools.ietf.org/html/draft-staykov-hu-json-canonical-form-00)
order.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 order in self:
if order.company_id._is_accounting_unalterable():
# write the hash and the secure_sequence_number when posting or invoicing an pos.order
if vals.get('state') in ['paid', 'done', 'invoiced']:
has_been_posted = True
# restrict the operation in case we are trying to write a forbidden field
if (order.state in ['paid', 'done', 'invoiced'] and set(vals).intersection(ORDER_FIELDS)):
raise UserError(_('According to the French law, you cannot modify a point of sale order. Forbidden fields: %s.') % ', '.join(ORDER_FIELDS))
# restrict the operation in case we are trying to overwrite existing hash
if (order.l10n_fr_hash and 'l10n_fr_hash' in vals) or (order.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 point of sale.'))
res = super(pos_order, self).write(vals)
# write the hash and the secure_sequence_number when posting or invoicing a pos order
if has_been_posted:
for order in self.filtered(lambda o: o.company_id._is_accounting_unalterable() and
not (o.l10n_fr_secure_sequence_number or o.l10n_fr_hash)):
new_number = order.company_id.l10n_fr_pos_cert_sequence_id.next_by_id()
vals_hashing = {'l10n_fr_secure_sequence_number': new_number,
'l10n_fr_hash': order._get_new_hash(new_number)}
res |= super(pos_order, order).write(vals_hashing)
return res
@api.model
def _check_hash_integrity(self, company_id):
"""Checks that all posted or invoiced pos orders have still the same data as when they were posted
and raises an error with the result.
"""
def build_order_info(order):
entry_reference = _('(Receipt ref.: %s)')
order_reference_string = order.pos_reference and entry_reference % order.pos_reference or ''
return [ctx_tz(order, 'date_order'), order.l10n_fr_secure_sequence_number, order.name, order_reference_string, ctx_tz(order, 'write_date')]
orders = self.search([('state', 'in', ['paid', 'done', 'invoiced']),
('company_id', '=', company_id),
('l10n_fr_secure_sequence_number', '!=', 0)],
order="l10n_fr_secure_sequence_number ASC")
if not orders:
raise UserError(_('There isn\'t any order flagged for data inalterability yet for the company %s. This mechanism only runs for point of sale orders generated after the installation of the module France - Certification CGI 286 I-3 bis. - POS') % self.env.user.company_id.name)
previous_hash = ''
start_order_info = []
for order in orders:
if order.l10n_fr_hash != order._compute_hash(previous_hash=previous_hash):
raise UserError(_('Corrupted data on point of sale order with id %s.') % order.id)
previous_hash = order.l10n_fr_hash
orders_sorted_date = orders.sorted(lambda o: o.date_order)
start_order_info = build_order_info(orders_sorted_date[0])
end_order_info = build_order_info(orders_sorted_date[-1])
report_dict = {'start_order_name': start_order_info[2],
'start_order_ref': start_order_info[3],
'start_order_date': start_order_info[0],
'end_order_name': end_order_info[2],
'end_order_ref': end_order_info[3],
'end_order_date': end_order_info[0]}
# Raise on success
raise UserError(_('''Successful test !
The point of sale orders are guaranteed to be in their original and inalterable state
From: %(start_order_name)s %(start_order_ref)s recorded on %(start_order_date)s
To: %(end_order_name)s %(end_order_ref)s recorded on %(end_order_date)s
For this report to be legally meaningful, please download your certification from your customer account on Odoo.com (Only for Odoo Enterprise users).'''
) % report_dict)
class PosOrderLine(models.Model):
_inherit = "pos.order.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._is_accounting_unalterable() and l.order_id.state in ['done', 'invoiced'] for l in self):
raise UserError(_('According to the French law, you cannot modify a point of sale order line. Forbidden fields: %s.') % ', '.join(LINE_FIELDS))
return super(PosOrderLine, self).write(vals)
@@ -0,0 +1,26 @@
from openerp import models, api, fields
class ResCompany(models.Model):
_inherit = 'res.company'
l10n_fr_pos_cert_sequence_id = fields.Many2one('ir.sequence')
@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._is_accounting_unalterable():
sequence_fields = ['l10n_fr_pos_cert_sequence_id']
company._create_secure_sequence(sequence_fields)
return company
@api.multi
def write(self, vals):
res = super(ResCompany, self).write(vals)
#if country changed to fr, create the securisation sequence
for company in self:
if company._is_accounting_unalterable():
sequence_fields = ['l10n_fr_pos_cert_sequence_id']
company._create_secure_sequence(sequence_fields)
return res
Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

@@ -0,0 +1,26 @@
<odoo>
<record id="view_bank_statement_form_readonly" model="ir.ui.view">
<field name="name">account.bank.statement.form</field>
<field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account.view_bank_statement_form"/>
<field name="mode">extension</field>
<field name="arch" type="xml">
<xpath expr="//form" position="inside">
<field name="pos_session_id" invisible="1"/>
</xpath>
<xpath expr="//field[@name='name']" position="attributes">
<attribute name="attrs">{'readonly': [('pos_session_id', '!=', False)]}</attribute>
</xpath>
<xpath expr="//field[@name='journal_id']" position="attributes">
<attribute name="attrs">{'readonly': [('pos_session_id', '!=', False)]}</attribute>
</xpath>
<xpath expr="//field[@name='date']" position="attributes">
<attribute name="attrs">{'readonly': [('pos_session_id', '!=', False)]}</attribute>
</xpath>
<xpath expr="//field[@name='line_ids']" position="attributes">
<attribute name="attrs">{'readonly': [('pos_session_id', '!=', False)]}</attribute>
</xpath>
</field>
</record>
</odoo>
+14
View File
@@ -0,0 +1,14 @@
import models
from openerp import api, SUPERUSER_ID
from ..l10n_fr_certification.models.res_company import UNALTERABLE_COUNTRIES
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)
fr_companies = env['res.company'].search([('partner_id.country_id.code', 'in', UNALTERABLE_COUNTRIES)])
if fr_companies:
# create the securisation sequence per company
fr_companies._create_secure_sequence(['l10n_fr_closing_sequence_id'])
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'France - VAT Anti-Fraud Certification (CGI 286 I-3 bis) - Sale Closings',
'version': '1.0',
'category': 'Localization',
'description': """
This module adds support for automatic sales closings with computation of both period and cumulative totals (daily, monthly, annually)
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
The module adds following features:
Storage: automatic sales closings with computation of both period and cumulative totals (daily, monthly, annually)
""",
'depends': ['l10n_fr_certification'],
'installable': True,
'auto_install': True,
'application': False,
'data': [
'views/account_sale_closure.xml',
'data/account_sale_closure_cron.xml',
'security/ir.model.access.csv',
'security/account_closing_intercompany.xml',
],
'post_init_hook': '_setup_inalterability',
}
@@ -0,0 +1,35 @@
<odoo>
<record model="ir.cron" id="account_sale_closing_daily">
<field name="name">Generate Daily Sales Closing</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False"/>
<field name="model" eval="'account.sale.closing'"/>
<field name="function" eval="'_automated_closing'"/>
<field name="args" eval="'(\'daily\',)'" />
</record>
<record model="ir.cron" id="account_sale_closing_monthly">
<field name="name">Generate Monthly Sales Closing</field>
<field name="interval_number">1</field>
<field name="interval_type">months</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False"/>
<field name="model" eval="'account.sale.closing'"/>
<field name="function" eval="'_automated_closing'"/>
<field name="args" eval="'(\'monthly\',)'" />
</record>
<record model="ir.cron" id="account_sale_closing_annually">
<field name="name">Generate Annual Sales Closing</field>
<field name="interval_number">12</field>
<field name="interval_type">months</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False"/>
<field name="model" eval="'account.sale.closing'"/>
<field name="function" eval="'_automated_closing'"/>
<field name="args" eval="'(\'annually\',)'" />
</record>
</odoo>
@@ -0,0 +1,219 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * l10n_fr_sale_closing
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 9.0c\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-12-06 16:03+0000\n"
"PO-Revision-Date: 2017-12-06 16:03+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: l10n_fr_sale_closing
#: model:ir.ui.view,arch_db:l10n_fr_sale_closing.form_view_account_sale_closing
msgid "Account Closing"
msgstr ""
#. module: l10n_fr_sale_closing
#: selection:account.sale.closing,frequency:0
msgid "Annual"
msgstr ""
#. module: l10n_fr_sale_closing
#: code:addons/l10n_fr_sale_closing/models/account_closing.py:134
#, python-format
msgid "Annual Closing"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_date_closing_stop
msgid "Closing Date"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_frequency
msgid "Closing Type"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model,name:l10n_fr_sale_closing.model_res_company
msgid "Companies"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_company_id
msgid "Company"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_create_uid
msgid "Created by"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_create_date
msgid "Created on"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_total_beginning
msgid "Cumulative Grand Total"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_currency_id
msgid "Currency"
msgstr ""
#. module: l10n_fr_sale_closing
#: selection:account.sale.closing,frequency:0
msgid "Daily"
msgstr ""
#. module: l10n_fr_sale_closing
#: code:addons/l10n_fr_sale_closing/models/account_closing.py:125
#, python-format
msgid "Daily Closing"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,help:l10n_fr_sale_closing.field_account_sale_closing_date_closing_start
msgid "Date from which the total interval is computed"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,help:l10n_fr_sale_closing.field_account_sale_closing_date_closing_stop
msgid "Date to which the values are computed"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_display_name
msgid "Display Name"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,help:l10n_fr_sale_closing.field_account_sale_closing_name
msgid "Frequency and unique sequence number"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_id
msgid "ID"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,help:l10n_fr_sale_closing.field_account_sale_closing_last_move_id
msgid "Last Journal entry included in the grand total"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing___last_update
msgid "Last Modified on"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_write_uid
msgid "Last Updated by"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_write_date
msgid "Last Updated on"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_last_move_id
msgid "Last journal entry"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_last_move_hash
msgid "Last journal entry's inalteralbility hash"
msgstr ""
#. module: l10n_fr_sale_closing
#: selection:account.sale.closing,frequency:0
msgid "Monthly"
msgstr ""
#. module: l10n_fr_sale_closing
#: code:addons/l10n_fr_sale_closing/models/account_closing.py:130
#, python-format
msgid "Monthly Closing"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_name
msgid "Name"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_total_interval
msgid "Period Total"
msgstr ""
#. module: l10n_fr_sale_closing
#: code:addons/l10n_fr_sale_closing/models/account_closing.py:142
#: code:addons/l10n_fr_sale_closing/models/account_closing.py:146
#, python-format
msgid "Sale Closings are not meant to be written or deleted under any circumstances."
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.actions.act_window,name:l10n_fr_sale_closing.action_list_view_account_sale_closing
#: model:ir.ui.menu,name:l10n_fr_sale_closing.menu_account_closing
#: model:ir.ui.menu,name:l10n_fr_sale_closing.menu_account_closing_reporting
msgid "Sales Closings"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.actions.act_window,help:l10n_fr_sale_closing.action_list_view_account_sale_closing
msgid "Sales closings run automatically on a daily, monthly and annual basis. It computes both period and cumulative totals from all the sales entries posted in the system after the previous closing."
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_sequence_number
msgid "Sequence #"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_res_company_l10n_fr_closing_sequence_id
msgid "Sequence to use to build sale closings"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,field_description:l10n_fr_sale_closing.field_account_sale_closing_date_closing_start
msgid "Starting Date"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.actions.act_window,help:l10n_fr_sale_closing.action_list_view_account_sale_closing
msgid "The closings are created by Odoo"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,help:l10n_fr_sale_closing.field_account_sale_closing_currency_id
msgid "The company's currency"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,help:l10n_fr_sale_closing.field_account_sale_closing_total_interval
msgid "Total in receivable accounts during the interval, excluding overlapping periods"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model.fields,help:l10n_fr_sale_closing.field_account_sale_closing_total_beginning
msgid "Total in receivable accounts since the beginnig of times"
msgstr ""
#. module: l10n_fr_sale_closing
#: model:ir.model,name:l10n_fr_sale_closing.model_account_sale_closing
msgid "account.sale.closing"
msgstr ""
@@ -0,0 +1,2 @@
import res_company
import account_closing
@@ -0,0 +1,158 @@
from datetime import datetime, timedelta
from openerp import models, api, fields
from openerp.fields import Datetime as FieldDateTime
from openerp.tools.translate import _
from openerp.exceptions import UserError
class AccountClosing(models.Model):
"""
This object holds an interval total and a grand total of the accounts of type receivable for a company,
as well as the last account_move that has been counted in a previous object
It takes its earliest brother to infer from when the computation needs to be done
in order to compute its own data.
"""
_name = 'account.sale.closing'
_order = 'date_closing_stop desc, sequence_number desc'
name = fields.Char(help="Frequency and unique sequence number", required=True)
company_id = fields.Many2one('res.company', string='Company', readonly=True, required=True)
date_closing_stop = fields.Datetime(string="Closing Date", help='Date to which the values are computed', readonly=True, required=True)
date_closing_start = fields.Datetime(string="Starting Date", help='Date from which the total interval is computed', readonly=True, required=True)
frequency = fields.Selection(string='Closing Type', selection=[('daily', 'Daily'), ('monthly', 'Monthly'), ('annually', 'Annual')], readonly=True, required=True)
total_interval = fields.Monetary(string="Period Total", help='Total in receivable accounts during the interval, excluding overlapping periods', readonly=True, required=True)
cumulative_total = fields.Monetary(string="Cumulative Grand Total", help='Total in receivable accounts since the beginnig of times', readonly=True, required=True)
sequence_number = fields.Integer('Sequence #', readonly=True, required=True)
last_move_id = fields.Many2one('account.move', string='Last journal entry', help='Last Journal entry included in the grand total', readonly=True)
last_move_hash = fields.Char(string='Last journal entry\'s inalteralbility hash', readonly=True)
currency_id = fields.Many2one('res.currency', string='Currency', help="The company's currency", readonly=True, related='company_id.currency_id', store=True)
def _query_for_aml(self, company, first_move_sequence_number, date_start):
params = {'company_id': company.id}
query = '''WITH aggregate AS (SELECT m.id AS move_id,
aml.balance AS balance,
aml.id as line_id
FROM account_move_line aml
JOIN account_journal j ON aml.journal_id = j.id
JOIN account_account acc ON acc.id = aml.account_id
JOIN account_account_type t ON (t.id = acc.user_type_id AND t.type = 'receivable')
JOIN account_move m ON m.id = aml.move_id
WHERE j.type = 'sale'
AND aml.company_id = %(company_id)s
AND m.state = 'posted' '''
if first_move_sequence_number is not False and first_move_sequence_number is not None:
params['first_move_sequence_number'] = first_move_sequence_number
query += '''AND m.l10n_fr_secure_sequence_number > %(first_move_sequence_number)s'''
elif date_start:
#the first time we compute the closing, we consider only from the installation of the module
params['date_start'] = date_start
query += '''AND m.date >= %(date_start)s'''
query += " ORDER BY m.l10n_fr_secure_sequence_number DESC) "
query += '''SELECT array_agg(move_id) AS move_ids,
array_agg(line_id) AS line_ids,
sum(balance) AS balance
FROM aggregate'''
self.env.cr.execute(query, params)
return self.env.cr.dictfetchall()[0]
def _compute_amounts(self, frequency, company):
"""
Method used to compute all the business data of the new object.
It will search for previous closings of the same frequency to infer the move from which
account move lines should be fetched.
@param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually)
frequencies are literal (daily means 24 hours and so on)
@param {recordset} company: the company for which the closing is done
@return {dict} containing {field: value} for each business field of the object
"""
interval_dates = self._interval_dates(frequency, company)
previous_closing = self.search([
('frequency', '=', frequency),
('company_id', '=', company.id)], limit=1, order='sequence_number desc')
first_move = self.env['account.move']
date_start = interval_dates['interval_from']
cumulative_total = 0
if previous_closing:
first_move = previous_closing.last_move_id
date_start = previous_closing.create_date
cumulative_total += previous_closing.cumulative_total
aml_aggregate = self._query_for_aml(company, first_move.l10n_fr_secure_sequence_number, date_start)
total_interval = aml_aggregate['balance'] or 0
cumulative_total += total_interval
# We keep the reference to avoid gaps (like daily object during the weekend)
last_move = first_move
if aml_aggregate['move_ids']:
last_move = last_move.browse(aml_aggregate['move_ids'][0])
return {'total_interval': total_interval,
'cumulative_total': cumulative_total,
'last_move_id': last_move.id,
'last_move_hash': last_move.l10n_fr_hash,
'date_closing_stop': interval_dates['date_stop'],
'date_closing_start': date_start,
'name': interval_dates['name_interval'] + ' - ' + interval_dates['date_stop'][:10]}
def _interval_dates(self, frequency, company):
"""
Method used to compute the theoretical date from which account move lines should be fetched
@param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually)
frequencies are literal (daily means 24 hours and so on)
@param {recordset} company: the company for which the closing is done
@return {dict} the theoretical date from which account move lines are fetched.
date_stop date to which the move lines are fetched, always now()
the dates are in their Odoo Database string representation
"""
date_stop = datetime.utcnow()
interval_from = None
name_interval = ''
if frequency == 'daily':
interval_from = date_stop - timedelta(days=1)
name_interval = _('Daily Closing')
elif frequency == 'monthly':
month_target = date_stop.month > 1 and date_stop.month - 1 or 12
year_target = month_target < 12 and date_stop.year or date_stop.year - 1
interval_from = date_stop.replace(year=year_target, month=month_target)
name_interval = _('Monthly Closing')
elif frequency == 'annually':
year_target = date_stop.year - 1
interval_from = date_stop.replace(year=year_target)
name_interval = _('Annual Closing')
return {'interval_from': FieldDateTime.to_string(interval_from),
'date_stop': FieldDateTime.to_string(date_stop),
'name_interval': name_interval}
@api.multi
def write(self, vals):
raise UserError(_('Sale Closings are not meant to be written or deleted under any circumstances.'))
@api.multi
def unlink(self):
raise UserError(_('Sale Closings are not meant to be written or deleted under any circumstances.'))
@api.model
def _automated_closing(self, frequency='daily'):
"""To be executed by the CRON to create an object of the given frequency for each company that needs it
@param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually)
frequencies are literal (daily means 24 hours and so on)
@return {recordset} all the objects created for the given frequency
"""
res_company = self.env['res.company'].search([])
account_closings = self.env['account.sale.closing']
for company in res_company.filtered(lambda c: c._is_accounting_unalterable()):
new_sequence_number = company.l10n_fr_closing_sequence_id.next_by_id()
values = self._compute_amounts(frequency, company)
values['frequency'] = frequency
values['company_id'] = company.id
values['sequence_number'] = new_sequence_number
account_closings |= account_closings.create(values)
return account_closings
@@ -0,0 +1,29 @@
# -*- 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_closing_sequence_id = fields.Many2one('ir.sequence', 'Sequence to use to build sale closings', 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._is_accounting_unalterable():
sequence_fields = ['l10n_fr_closing_sequence_id']
company._create_secure_sequence(sequence_fields)
return company
@api.multi
def write(self, vals):
res = super(ResCompany, self).write(vals)
#if country changed to fr, create the securisation sequence
for company in self:
if company._is_accounting_unalterable():
sequence_fields = ['l10n_fr_closing_sequence_id']
company._create_secure_sequence(sequence_fields)
return res
@@ -0,0 +1,8 @@
<odoo>
<record model="ir.rule" id="account_sale_closing_multi_company">
<field name="name">Sale Closing multi-company</field>
<field name="model_id" ref="model_account_sale_closing"/>
<field name="domain_force">[('company_id','child_of',[user.company_id.id])]</field>
</record>
</odoo>
@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_l10n_fr_sale_closing_account_sale_closing_user,l10n_fr_sale_closing.account.sale.closing.user,l10n_fr_sale_closing.model_account_sale_closing,base.group_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_l10n_fr_sale_closing_account_sale_closing_user l10n_fr_sale_closing.account.sale.closing.user l10n_fr_sale_closing.model_account_sale_closing base.group_user 1 0 0 0
Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

@@ -0,0 +1,68 @@
<odoo>
<record id="list_view_account_sale_closing" model="ir.ui.view">
<field name="name">Sales Closings</field>
<field name="model">account.sale.closing</field>
<field name="arch" type="xml">
<tree create="false" import="false">
<field name="date_closing_start"/>
<field name="date_closing_stop"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="currency_id" invisible="1"/>
<field name="frequency"/>
<field name="sequence_number" groups="base.group_no_one"/>
<field name="total_interval"/>
<field name="cumulative_total"/>
</tree>
</field>
</record>
<record id="form_view_account_sale_closing" model="ir.ui.view">
<field name="name">Sales Closings</field>
<field name="model">account.sale.closing</field>
<field name="arch" type="xml">
<form create="false" edit="false" string="Account Closing">
<sheet>
<div class="oe_title">
<h1>
<field name="name"/>
</h1>
</div>
<group>
<group>
<field name="date_closing_start"/>
<field name="date_closing_stop"/>
<field name="frequency"/>
<field name="sequence_number" groups="base.group_no_one"/>
</group>
<group>
<field name="total_interval"/>
<field name="cumulative_total"/>
<field name="last_move_id" groups="account.group_account_user"/>
<field name="last_move_hash" groups="account.group_account_user"/>
</group>
<group>
<field name="company_id" groups="base.group_multi_company"/>
<field name="currency_id" invisible="1"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_list_view_account_sale_closing" model="ir.actions.act_window">
<field name="name">Sales Closings</field>
<field name="res_model">account.sale.closing</field>
<field name="view_type">tree</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_nocreate">
The closings are created by Odoo
</p><p>
Sales closings run automatically on a daily, monthly and annual basis. It computes both period and cumulative totals from all the sales entries posted in the system after the previous closing.
</p>
</field>
</record>
<menuitem action="action_list_view_account_sale_closing" id="menu_account_closing_reporting" parent="l10n_fr.account_reports_fr_statements_menu" sequence="90"/>
</odoo>