[MERGE] forward port branch 10.0 up to 767e2509f7
This commit is contained in:
@@ -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})
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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>
|
||||
@@ -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'])
|
||||
@@ -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()
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
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>
|
||||
@@ -187,11 +187,17 @@
|
||||
}
|
||||
|
||||
// All required form fields
|
||||
.o-input-required-style() {
|
||||
background-color: #D2D2FF;
|
||||
}
|
||||
.o_form_required {
|
||||
&.o_form_input, .o_form_input, &.o_form_textarea, .o_form_textarea {
|
||||
background-color: #D2D2FF;
|
||||
&.o_form_input, .o_form_input, textarea.o_form_textarea {
|
||||
.o-input-required-style();
|
||||
}
|
||||
}
|
||||
textarea.o_form_textarea.o_form_required {
|
||||
.o-input-required-style();
|
||||
}
|
||||
|
||||
.o_form_field.o_form_field_many2one .o_external_button {
|
||||
padding: 0;
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
class="btn btn-primary btn-lg pull-right a-submit" t-attf-id="#{event.id}">Order Now</button>
|
||||
</xpath>
|
||||
<xpath expr="//form[@id='registration_form']" position="attributes">
|
||||
<attribute name="t-if">event.event_ticket_ids</attribute>
|
||||
<attribute name="t-if">event.event_ticket_ids and not all([ticket.is_expired for ticket in event.event_ticket_ids])</attribute>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user