summaryrefslogtreecommitdiff
path: root/addons/l10n_fr_pos_cert
diff options
context:
space:
mode:
Diffstat (limited to 'addons/l10n_fr_pos_cert')
-rw-r--r--addons/l10n_fr_pos_cert/__init__.py16
-rw-r--r--addons/l10n_fr_pos_cert/__manifest__.py42
-rw-r--r--addons/l10n_fr_pos_cert/data/account_sale_closure_cron.xml34
-rw-r--r--addons/l10n_fr_pos_cert/models/__init__.py7
-rw-r--r--addons/l10n_fr_pos_cert/models/account_bank_statement.py23
-rw-r--r--addons/l10n_fr_pos_cert/models/account_closing.py167
-rw-r--r--addons/l10n_fr_pos_cert/models/account_fiscal_position.py19
-rw-r--r--addons/l10n_fr_pos_cert/models/pos.py145
-rw-r--r--addons/l10n_fr_pos_cert/models/res_company.py96
-rw-r--r--addons/l10n_fr_pos_cert/report/__init__.py4
-rw-r--r--addons/l10n_fr_pos_cert/report/pos_hash_integrity.py22
-rw-r--r--addons/l10n_fr_pos_cert/report/pos_hash_integrity.xml95
-rw-r--r--addons/l10n_fr_pos_cert/security/account_closing_intercompany.xml9
-rw-r--r--addons/l10n_fr_pos_cert/security/ir.model.access.csv2
-rw-r--r--addons/l10n_fr_pos_cert/static/description/icon.pngbin0 -> 562 bytes
-rw-r--r--addons/l10n_fr_pos_cert/static/src/js/NumpadWidget.js20
-rw-r--r--addons/l10n_fr_pos_cert/static/src/js/PaymentScreen.js29
-rw-r--r--addons/l10n_fr_pos_cert/static/src/js/pos.js84
-rw-r--r--addons/l10n_fr_pos_cert/static/src/xml/OrderReceipt.xml11
-rw-r--r--addons/l10n_fr_pos_cert/views/account_sale_closure.xml67
-rw-r--r--addons/l10n_fr_pos_cert/views/account_views.xml26
-rw-r--r--addons/l10n_fr_pos_cert/views/l10n_fr_pos_cert_templates.xml12
-rw-r--r--addons/l10n_fr_pos_cert/views/pos_inalterability_menuitem.xml21
-rw-r--r--addons/l10n_fr_pos_cert/views/pos_views.xml12
24 files changed, 963 insertions, 0 deletions
diff --git a/addons/l10n_fr_pos_cert/__init__.py b/addons/l10n_fr_pos_cert/__init__.py
new file mode 100644
index 00000000..c4104881
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/__init__.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import models
+from . import report
+from odoo import api, SUPERUSER_ID
+
+
+def _setup_inalterability(cr, registry):
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ # enable ping for this module
+ env['publisher_warranty.contract'].update_notification(cron_mode=True)
+
+ fr_companies = env['res.company'].search([('partner_id.country_id.code', 'in', env['res.company']._get_unalterable_country())])
+ if fr_companies:
+ fr_companies._create_secure_sequence(['l10n_fr_pos_cert_sequence_id'])
diff --git a/addons/l10n_fr_pos_cert/__manifest__.py b/addons/l10n_fr_pos_cert/__manifest__.py
new file mode 100644
index 00000000..fcd9c38d
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/__manifest__.py
@@ -0,0 +1,42 @@
+# -*- 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': 'Accounting/Localizations/Point of Sale',
+ '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', 'point_of_sale'],
+ 'installable': True,
+ 'auto_install': True,
+ 'application': False,
+ 'data': [
+ 'views/account_views.xml',
+ 'views/l10n_fr_pos_cert_templates.xml',
+ 'views/pos_views.xml',
+ 'views/account_sale_closure.xml',
+ 'views/pos_inalterability_menuitem.xml',
+ 'report/pos_hash_integrity.xml',
+ 'data/account_sale_closure_cron.xml',
+ 'security/ir.model.access.csv',
+ 'security/account_closing_intercompany.xml',
+ ],
+ 'qweb': ['static/src/xml/OrderReceipt.xml'],
+ 'post_init_hook': '_setup_inalterability',
+ 'license': 'LGPL-3',
+}
diff --git a/addons/l10n_fr_pos_cert/data/account_sale_closure_cron.xml b/addons/l10n_fr_pos_cert/data/account_sale_closure_cron.xml
new file mode 100644
index 00000000..ba2bd2f3
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/data/account_sale_closure_cron.xml
@@ -0,0 +1,34 @@
+<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_id" ref="model_account_sale_closing"/>
+ <field name="state">code</field>
+ <field name="code">model._automated_closing('daily')</field>
+ </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_id" ref="model_account_sale_closing"/>
+ <field name="state">code</field>
+ <field name="code">model._automated_closing('monthly')</field>
+ </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_id" ref="model_account_sale_closing"/>
+ <field name="state">code</field>
+ <field name="code">model._automated_closing('annually')</field>
+ </record>
+</odoo>
diff --git a/addons/l10n_fr_pos_cert/models/__init__.py b/addons/l10n_fr_pos_cert/models/__init__.py
new file mode 100644
index 00000000..771f4698
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/models/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from . import account_bank_statement
+from . import account_fiscal_position
+from . import res_company
+from . import pos
+from . import account_closing
diff --git a/addons/l10n_fr_pos_cert/models/account_bank_statement.py b/addons/l10n_fr_pos_cert/models/account_bank_statement.py
new file mode 100644
index 00000000..f8ace6d2
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/models/account_bank_statement.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo import models, api
+from odoo.tools.translate import _
+from odoo.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.pos_payment_method_ids):
+ 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.pos_payment_method_ids):
+ 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()
diff --git a/addons/l10n_fr_pos_cert/models/account_closing.py b/addons/l10n_fr_pos_cert/models/account_closing.py
new file mode 100644
index 00000000..ecc77bac
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/models/account_closing.py
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from datetime import datetime, timedelta
+
+from odoo import models, api, fields
+from odoo.fields import Datetime as FieldDateTime
+from odoo.tools.translate import _
+from odoo.exceptions import UserError
+from odoo.osv.expression import AND
+
+
+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'
+ _description = "Sale Closing"
+
+ 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_order_id = fields.Many2one('pos.order', string='Last Pos Order', help='Last Pos order included in the grand total', readonly=True)
+ last_order_hash = fields.Char(string='Last Order 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.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.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_order = self.env['pos.order']
+ date_start = interval_dates['interval_from']
+ cumulative_total = 0
+ if previous_closing:
+ first_order = previous_closing.last_order_id
+ date_start = previous_closing.create_date
+ cumulative_total += previous_closing.cumulative_total
+
+ domain = [('company_id', '=', company.id), ('state', 'in', ('paid', 'done', 'invoiced'))]
+ if first_order.l10n_fr_secure_sequence_number is not False and first_order.l10n_fr_secure_sequence_number is not None:
+ domain = AND([domain, [('l10n_fr_secure_sequence_number', '>', first_order.l10n_fr_secure_sequence_number)]])
+ elif date_start:
+ #the first time we compute the closing, we consider only from the installation of the module
+ domain = AND([domain, [('date_order', '>=', date_start)]])
+
+ orders = self.env['pos.order'].search(domain, order='date_order desc')
+
+ total_interval = sum(orders.mapped('amount_total'))
+ cumulative_total += total_interval
+
+ # We keep the reference to avoid gaps (like daily object during the weekend)
+ last_order = first_order
+ if orders:
+ last_order = orders[0]
+
+ return {'total_interval': total_interval,
+ 'cumulative_total': cumulative_total,
+ 'last_order_id': last_order.id,
+ 'last_order_hash': last_order.l10n_fr_secure_sequence_number,
+ '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}
+
+ def write(self, vals):
+ raise UserError(_('Sale Closings are not meant to be written or deleted under any circumstances.'))
+
+ 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
diff --git a/addons/l10n_fr_pos_cert/models/account_fiscal_position.py b/addons/l10n_fr_pos_cert/models/account_fiscal_position.py
new file mode 100644
index 00000000..17661e20
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/models/account_fiscal_position.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+
+from odoo import _, models
+from odoo.exceptions import UserError
+
+
+class AccountFiscalPosition(models.Model):
+ _inherit = "account.fiscal.position"
+
+ def write(self, vals):
+ if "tax_ids" in vals:
+ if self.env["pos.order"].sudo().search_count([("fiscal_position_id", "in", self.ids)]):
+ raise UserError(
+ _(
+ "You cannot modify a fiscal position used in a POS order. "
+ "You should archive it and create a new one."
+ )
+ )
+ return super(AccountFiscalPosition, self).write(vals)
diff --git a/addons/l10n_fr_pos_cert/models/pos.py b/addons/l10n_fr_pos_cert/models/pos.py
new file mode 100644
index 00000000..ec67ad81
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/models/pos.py
@@ -0,0 +1,145 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from datetime import datetime, timedelta
+from hashlib import sha256
+from json import dumps
+
+from odoo import models, api, fields
+from odoo.fields import Datetime
+from odoo.tools.translate import _, _lt
+from odoo.exceptions import UserError
+
+
+class pos_config(models.Model):
+ _inherit = 'pos.config'
+
+ 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'
+
+ def _check_session_timing(self):
+ self.ensure_one()
+ date_today = datetime.utcnow()
+ session_start = Datetime.from_string(self.start_at)
+ if not date_today - 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
+
+ def open_frontend_cb(self):
+ sessions_to_check = self.filtered(lambda s: s.config_id.company_id._is_accounting_unalterable())
+ sessions_to_check.filtered(lambda s: s.state == 'opening_control').start_at = fields.Datetime.now()
+ for session in sessions_to_check:
+ session._check_session_timing()
+ return super(pos_session, self).open_frontend_cb()
+
+
+ORDER_FIELDS = ['date_order', 'user_id', 'lines', 'payment_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 = _lt('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 u'')
+
+ 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).encode('utf-8'))
+ 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.sorted().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,
+ ensure_ascii=True, indent=None,
+ separators=(',',':'))
+
+ 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
+
+ def unlink(self):
+ for order in self:
+ if order.company_id._is_accounting_unalterable():
+ raise UserError(_("According to French law, you cannot delet a point of sale order."))
+ return super(pos_order, self).unlink()
+
+ def _export_for_ui(self, order):
+ res = super()._export_for_ui(order)
+ res['l10n_fr_hash'] = order.l10n_fr_hash
+ return res
+
+
+class PosOrderLine(models.Model):
+ _inherit = "pos.order.line"
+
+ 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)
diff --git a/addons/l10n_fr_pos_cert/models/res_company.py b/addons/l10n_fr_pos_cert/models/res_company.py
new file mode 100644
index 00000000..c701c6f3
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/models/res_company.py
@@ -0,0 +1,96 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo import models, api, fields, _
+from odoo.exceptions import UserError
+from datetime import datetime
+from odoo.fields import Datetime, Date
+from odoo.tools.misc import format_date
+import pytz
+
+
+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']._lang_get(ctx['lang'])
+ 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 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
+
+ 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
+
+ def _action_check_pos_hash_integrity(self):
+ return self.env.ref('l10n_fr_pos_cert.action_report_pos_hash_integrity').report_action(self.id)
+
+ def _check_pos_hash_integrity(self):
+ """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_hash, order.name, order_reference_string, ctx_tz(order, 'write_date')]
+
+ hash_verified = True
+ msg_alert = ''
+ report_dict = {}
+ if self._is_accounting_unalterable():
+ orders = self.env['pos.order'].search([('state', 'in', ['paid', 'done', 'invoiced']), ('company_id', '=', self.id),
+ ('l10n_fr_secure_sequence_number', '!=', 0)], order="l10n_fr_secure_sequence_number ASC")
+
+ if not orders:
+ msg_alert = (_('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.company.name))
+ hash_verified = False
+
+ previous_hash = u''
+ start_order_info = []
+ for order in orders:
+ if order.l10n_fr_hash != order._compute_hash(previous_hash=previous_hash):
+ msg_alert = (_('Corrupted data on point of sale order with id %s.', order.id))
+ hash_verified = False
+ break
+ previous_hash = order.l10n_fr_hash
+
+ if hash_verified:
+ 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.update({
+ 'first_order_name': start_order_info[2],
+ 'first_order_hash': start_order_info[1],
+ 'first_order_date': start_order_info[0],
+ 'last_order_name': end_order_info[2],
+ 'last_order_hash': end_order_info[1],
+ 'last_order_date': end_order_info[0],
+ })
+ return {
+ 'result': hash_verified and report_dict or 'None',
+ 'msg_alert': msg_alert or 'None',
+ 'printing_date': format_date(self.env, Date.to_string( Date.today())),
+ }
diff --git a/addons/l10n_fr_pos_cert/report/__init__.py b/addons/l10n_fr_pos_cert/report/__init__.py
new file mode 100644
index 00000000..af5f8a80
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/report/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import pos_hash_integrity
diff --git a/addons/l10n_fr_pos_cert/report/pos_hash_integrity.py b/addons/l10n_fr_pos_cert/report/pos_hash_integrity.py
new file mode 100644
index 00000000..d7c36038
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/report/pos_hash_integrity.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+class ReportPosHashIntegrity(models.AbstractModel):
+ _name = 'report.l10n_fr_pos_cert.report_pos_hash_integrity'
+ _description = 'Get french pos hash integrity result as PDF.'
+
+ @api.model
+ def _get_report_values(self, docids, data=None):
+ if data:
+ data.update(self.env.company._check_pos_hash_integrity())
+ else:
+ data = self.env.company._check_hash_pos_integrity()
+ return {
+ 'doc_ids' : docids,
+ 'doc_model' : self.env['res.company'],
+ 'data' : data,
+ 'docs' : self.env['res.company'].browse(self.env.company.id),
+ }
diff --git a/addons/l10n_fr_pos_cert/report/pos_hash_integrity.xml b/addons/l10n_fr_pos_cert/report/pos_hash_integrity.xml
new file mode 100644
index 00000000..a64b3d9a
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/report/pos_hash_integrity.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+ <data>
+ <template id="report_pos_hash_integrity">
+ <t t-call="web.html_container">
+ <t t-foreach="docs" t-as="company">
+ <t t-call="web.external_layout">
+ <div class="page">
+ <div class="row" id="hash_header">
+ <div class="col-12">
+ <br/>
+ <h2>Résultat du test d'intégrité - <span t-esc="data['printing_date']"/></h2>
+ <br/>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-12" id="hash_config_review">
+ <h6>
+ Selon l’article 286 du code général des impôts français, toute livraison de bien ou prestation
+ de services ne donnant pas lieu à facturation et étant enregistrée au moyen d’un logiciel ou
+ d’un système de caisse doit satisfaire à des conditions d’inaltérabilité et de sécurisation des
+ données en vue d’un contrôle de l’administration fiscale.
+ <br/>
+ <br/>
+ Ces conditions sont respectées via une fonction de hachage des ventes du Point de Vente.
+ <br/>
+ <br/>
+ </h6>
+ </div>
+ </div>
+ <t t-if="data['result'] != 'None'">
+ <div class="row">
+ <div class="col-12" id="hash_data_consistency">
+ <br/>
+ <h3>Contrôle des données du point de vente</h3>
+ <br/>
+ <t t-if="data['result'] != 'None' and data['msg_alert'] == 'None'">
+ <h5>
+ Toutes les ventes effectuées via le Point de Vente
+ sont bien dans la chaîne de hachage.
+ </h5>
+ <br/>
+ </t>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-12" id="hash_data_consistency_table">
+ <table class="table table-bordered" style="table-layout: fixed">
+ <thead style="display: table-row-group">
+ <tr>
+ <th class="text-center" style="width: 25%" scope="col">First Hash</th>
+ <th class="text-center" style="width: 25%" scope="col">First Entry</th>
+ <th class="text-center" style="width: 25%" scope="col">Last Hash</th>
+ <th class="text-center" style="width: 25%" scope="col">Last Entry</th>
+ </tr>
+ </thead>
+ <tbody>
+ <t t-if="data['result'] != 'None'">
+ <t t-if="data['result']['first_order_hash'] != 'None'">
+ <tr>
+ <td><span t-esc="data['result']['first_order_hash']"/></td>
+ <td>
+ <span t-esc="data['result']['first_order_name']"/> <br/>
+ <span t-esc="data['result']['first_order_date']"/>
+ </td>
+ <td><span t-esc="data['result']['last_order_hash']"/></td>
+ <td>
+ <span t-esc="data['result']['last_order_name']"/> <br/>
+ <span t-esc="data['result']['last_order_date']"/>
+ </td>
+ </tr>
+ </t>
+ </t>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <div class="row" id="hash_last_div">
+ <div class="col-12" id="hash_chain_compliant">
+ <br/>
+ <h6>
+ La chaîne de hachage est conforme: il n’est pas possible d’altérer les données
+ sans casser la chaîne de hachage pour les pièces ultérieures.
+ </h6>
+ <br/>
+ </div>
+ </div>
+ </t>
+ </div>
+ </t>
+ </t>
+ </t>
+ </template>
+ </data>
+</odoo>
diff --git a/addons/l10n_fr_pos_cert/security/account_closing_intercompany.xml b/addons/l10n_fr_pos_cert/security/account_closing_intercompany.xml
new file mode 100644
index 00000000..90015388
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/security/account_closing_intercompany.xml
@@ -0,0 +1,9 @@
+<odoo noupdate="1">
+
+ <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', 'in', company_ids)]</field>
+ </record>
+
+</odoo>
diff --git a/addons/l10n_fr_pos_cert/security/ir.model.access.csv b/addons/l10n_fr_pos_cert/security/ir.model.access.csv
new file mode 100644
index 00000000..a2553eb8
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_l10n_fr_pos_cert_account_sale_closing_user,l10n_fr_pos_cert.account.sale.closing.user,l10n_fr_pos_cert.model_account_sale_closing,base.group_user,1,0,0,0
diff --git a/addons/l10n_fr_pos_cert/static/description/icon.png b/addons/l10n_fr_pos_cert/static/description/icon.png
new file mode 100644
index 00000000..bbcaa4fc
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/static/description/icon.png
Binary files differ
diff --git a/addons/l10n_fr_pos_cert/static/src/js/NumpadWidget.js b/addons/l10n_fr_pos_cert/static/src/js/NumpadWidget.js
new file mode 100644
index 00000000..ad6f0948
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/static/src/js/NumpadWidget.js
@@ -0,0 +1,20 @@
+odoo.define('l10n_fr_pos_cert.NumpadWidget', function(require) {
+ 'use strict';
+
+ const NumpadWidget = require('point_of_sale.NumpadWidget');
+ const Registries = require('point_of_sale.Registries');
+
+ const PosFrNumpadWidget = NumpadWidget => class extends NumpadWidget {
+ get hasPriceControlRights() {
+ if (this.env.pos.is_french_country()) {
+ return false;
+ } else {
+ return super.hasPriceControlRights;
+ }
+ }
+ };
+
+ Registries.Component.extend(NumpadWidget, PosFrNumpadWidget);
+
+ return NumpadWidget;
+ });
diff --git a/addons/l10n_fr_pos_cert/static/src/js/PaymentScreen.js b/addons/l10n_fr_pos_cert/static/src/js/PaymentScreen.js
new file mode 100644
index 00000000..1ec68f9b
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/static/src/js/PaymentScreen.js
@@ -0,0 +1,29 @@
+odoo.define('l10n_fr_pos_cert.PaymentScreen', function(require) {
+
+ const PaymentScreen = require('point_of_sale.PaymentScreen');
+ const Registries = require('point_of_sale.Registries');
+ const session = require('web.session');
+
+ const PosFrPaymentScreen = PaymentScreen => class extends PaymentScreen {
+ async _postPushOrderResolve(order, order_server_ids) {
+ try {
+ if(this.env.pos.is_french_country()) {
+ let result = await this.rpc({
+ model: 'pos.order',
+ method: 'search_read',
+ domain: [['id', 'in', order_server_ids]],
+ fields: ['l10n_fr_hash'],
+ context: session.user_context,
+ });
+ order.set_l10n_fr_hash(result[0].l10n_fr_hash || false);
+ }
+ } finally {
+ return super._postPushOrderResolve(...arguments);
+ }
+ }
+ };
+
+ Registries.Component.extend(PaymentScreen, PosFrPaymentScreen);
+
+ return PaymentScreen;
+});
diff --git a/addons/l10n_fr_pos_cert/static/src/js/pos.js b/addons/l10n_fr_pos_cert/static/src/js/pos.js
new file mode 100644
index 00000000..86dc047e
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/static/src/js/pos.js
@@ -0,0 +1,84 @@
+odoo.define('l10n_fr_pos_cert.pos', function (require) {
+"use strict";
+
+const { Gui } = require('point_of_sale.Gui');
+var models = require('point_of_sale.models');
+var rpc = require('web.rpc');
+var session = require('web.session');
+var core = require('web.core');
+var utils = require('web.utils');
+
+var _t = core._t;
+var round_di = utils.round_decimals;
+
+var _super_posmodel = models.PosModel.prototype;
+models.PosModel = models.PosModel.extend({
+ is_french_country: function(){
+ var french_countries = ['FR', 'MF', 'MQ', 'NC', 'PF', 'RE', 'GF', 'GP', 'TF'];
+ if (!this.company.country) {
+ Gui.showPopup("ErrorPopup", {
+ 'title': _t("Missing Country"),
+ 'body': _.str.sprintf(_t('The company %s doesn\'t have a country set.'), this.company.name),
+ });
+ return false;
+ }
+ return _.contains(french_countries, this.company.country.code);
+ },
+ delete_current_order: function () {
+ if (this.is_french_country() && this.get_order().get_orderlines().length) {
+ Gui.showPopup("ErrorPopup", {
+ 'title': _t("Fiscal Data Module error"),
+ 'body': _t("Deleting of orders is not allowed."),
+ });
+ } else {
+ _super_posmodel.delete_current_order.apply(this, arguments);
+ }
+ },
+
+ disallowLineQuantityChange() {
+ let result = _super_posmodel.disallowLineQuantityChange.bind(this)();
+ return this.is_french_country() || result;
+ }
+});
+
+
+var _super_order = models.Order.prototype;
+models.Order = models.Order.extend({
+ initialize: function() {
+ _super_order.initialize.apply(this,arguments);
+ this.l10n_fr_hash = this.l10n_fr_hash || false;
+ this.save_to_db();
+ },
+ export_for_printing: function() {
+ var result = _super_order.export_for_printing.apply(this,arguments);
+ result.l10n_fr_hash = this.get_l10n_fr_hash();
+ return result;
+ },
+ set_l10n_fr_hash: function (l10n_fr_hash){
+ this.l10n_fr_hash = l10n_fr_hash;
+ },
+ get_l10n_fr_hash: function() {
+ return this.l10n_fr_hash;
+ },
+ wait_for_push_order: function() {
+ var result = _super_order.wait_for_push_order.apply(this,arguments);
+ result = Boolean(result || this.pos.is_french_country());
+ return result;
+ }
+});
+
+var orderline_super = models.Orderline.prototype;
+models.Orderline = models.Orderline.extend({
+ can_be_merged_with: function(orderline) {
+ let order = this.pos.get_order();
+ let lastId = order.orderlines.last().cid;
+
+ if(this.pos.is_french_country() && (order.orderlines._byId[lastId].product.id !== orderline.product.id || order.orderlines._byId[lastId].quantity < 0)) {
+ return false;
+ } else {
+ return orderline_super.can_be_merged_with.apply(this, arguments);
+ }
+ }
+});
+
+});
diff --git a/addons/l10n_fr_pos_cert/static/src/xml/OrderReceipt.xml b/addons/l10n_fr_pos_cert/static/src/xml/OrderReceipt.xml
new file mode 100644
index 00000000..bb77291a
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/static/src/xml/OrderReceipt.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+ <t t-name="OrderReceipt" t-inherit="point_of_sale.OrderReceipt" t-inherit-mode="extension" owl="1">
+ <xpath expr="//div[hasclass('pos-receipt-order-data')]" position="inside">
+ <t t-if="receipt.l10n_fr_hash !== false">
+ <br/>
+ <div style="word-wrap:break-word;"><t t-esc="receipt.l10n_fr_hash"/></div>
+ </t>
+ </xpath>
+ </t>
+</templates>
diff --git a/addons/l10n_fr_pos_cert/views/account_sale_closure.xml b/addons/l10n_fr_pos_cert/views/account_sale_closure.xml
new file mode 100644
index 00000000..1a94ad56
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/views/account_sale_closure.xml
@@ -0,0 +1,67 @@
+<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_order_id" groups="account.group_account_readonly"/>
+ <field name="last_order_hash" groups="account.group_account_readonly"/>
+ </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_mode">list,form</field>
+ <field name="help" type="html">
+ <p class="o_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>
diff --git a/addons/l10n_fr_pos_cert/views/account_views.xml b/addons/l10n_fr_pos_cert/views/account_views.xml
new file mode 100644
index 00000000..27d4d2e9
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/views/account_views.xml
@@ -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>
diff --git a/addons/l10n_fr_pos_cert/views/l10n_fr_pos_cert_templates.xml b/addons/l10n_fr_pos_cert/views/l10n_fr_pos_cert_templates.xml
new file mode 100644
index 00000000..739d9dc9
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/views/l10n_fr_pos_cert_templates.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+
+ <template id="assets" inherit_id="point_of_sale.assets">
+ <xpath expr="." position="inside">
+ <script type="text/javascript" src="/l10n_fr_pos_cert/static/src/js/pos.js"></script>
+ <script type="text/javascript" src="/l10n_fr_pos_cert/static/src/js/NumpadWidget.js"></script>
+ <script type="text/javascript" src="/l10n_fr_pos_cert/static/src/js/PaymentScreen.js"></script>
+ </xpath>
+ </template>
+
+</odoo>
diff --git a/addons/l10n_fr_pos_cert/views/pos_inalterability_menuitem.xml b/addons/l10n_fr_pos_cert/views/pos_inalterability_menuitem.xml
new file mode 100644
index 00000000..09c42b7e
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/views/pos_inalterability_menuitem.xml
@@ -0,0 +1,21 @@
+<odoo>
+ <record id="action_report_pos_hash_integrity" model="ir.actions.report">
+ <field name="name">Hash integrity result PDF</field>
+ <field name="model">res.company</field>
+ <field name="report_type">qweb-pdf</field>
+ <field name="report_name">l10n_fr_pos_cert.report_pos_hash_integrity</field>
+ <field name="report_file">l10n_fr_pos_cert.report_pos_hash_integrity</field>
+ </record>
+ <record model="ir.actions.server" id="action_check_pos_hash_integrity">
+ <field name="name">POS Inalterability Check</field>
+ <field name="model_id" ref="account.model_res_company"/>
+ <field name="type">ir.actions.server</field>
+ <field name="state">code</field>
+ <field name="code">
+ action = env.company._action_check_pos_hash_integrity()
+ </field>
+ </record>
+ <menuitem id="pos_fr_statements_menu" name="French Statements" parent="point_of_sale.menu_point_rep" sequence="9" />
+ <menuitem action="l10n_fr_pos_cert.action_list_view_account_sale_closing" id="menu_account_closing" parent="pos_fr_statements_menu" sequence="80"/>
+ <menuitem action="l10n_fr_pos_cert.action_check_pos_hash_integrity" id="menu_check_move_integrity_reporting" parent="pos_fr_statements_menu" sequence="90"/>
+</odoo>
diff --git a/addons/l10n_fr_pos_cert/views/pos_views.xml b/addons/l10n_fr_pos_cert/views/pos_views.xml
new file mode 100644
index 00000000..860021d2
--- /dev/null
+++ b/addons/l10n_fr_pos_cert/views/pos_views.xml
@@ -0,0 +1,12 @@
+<odoo>
+ <record id="pos_order_form_inherit" model="ir.ui.view">
+ <field name="name">pos.order.form.inherit</field>
+ <field name="model">pos.order</field>
+ <field name="inherit_id" ref="point_of_sale.view_pos_pos_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='pos_reference']" position='after'>
+ <field string='Hash' name="l10n_fr_hash" groups="base.group_no_one"/>
+ </xpath>
+ </field>
+ </record>
+</odoo>