19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:28 +01:00
parent ff721d030e
commit 7721452493
1826 changed files with 124775 additions and 274114 deletions

View file

@ -25,36 +25,15 @@ pip install odoo-bringout-oca-ocb-l10n_fr_pos_cert
## Dependencies
This addon depends on:
- l10n_fr
- l10n_fr_account
- point_of_sale
## Manifest Information
- **Name**: France - VAT Anti-Fraud Certification for Point of Sale (CGI 286 I-3 bis)
- **Version**: 1.1
- **Category**: Accounting/Localizations/Point of Sale
- **License**: LGPL-3
- **Installable**: True
## Source
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `l10n_fr_pos_cert`.
- Repository: https://github.com/OCA/OCB
- Branch: 19.0
- Path: addons/l10n_fr_pos_cert
## License
This package maintains the original LGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md
This package preserves the original LGPL-3 license.

View file

@ -3,14 +3,12 @@
from . import models
from . import report
from odoo import api, SUPERUSER_ID
def _setup_inalterability(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
def _setup_inalterability(env):
# 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())])
fr_companies = env['res.company'].search([('partner_id.country_id.code', 'in', env['res.company']._get_france_country_codes())])
if fr_companies:
fr_companies._create_secure_sequence(['l10n_fr_pos_cert_sequence_id'])

View file

@ -3,7 +3,6 @@
{
'name': 'France - VAT Anti-Fraud Certification for Point of Sale (CGI 286 I-3 bis)',
'icon': '/l10n_fr/static/description/icon.png',
'version': '1.1',
'category': 'Accounting/Localizations/Point of Sale',
'description': """
@ -22,7 +21,7 @@ The module adds following features:
Access to download the mandatory Certificate of Conformity delivered by Odoo SA (only for Odoo Enterprise users)
""",
'depends': ['l10n_fr', 'point_of_sale'],
'depends': ['l10n_fr_account', 'point_of_sale'],
'installable': True,
'auto_install': True,
'data': [
@ -37,11 +36,16 @@ The module adds following features:
],
'post_init_hook': '_setup_inalterability',
'assets': {
'point_of_sale.assets': [
'l10n_fr_pos_cert/static/src/js/**/*',
'l10n_fr_pos_cert/static/src/css/pos.css',
'l10n_fr_pos_cert/static/src/xml/**/*',
'web.assets_unit_tests': [
'l10n_fr_pos_cert/static/tests/unit/**/*',
],
'point_of_sale._assets_pos': [
'l10n_fr_pos_cert/static/src/**/*',
],
'web.assets_tests': [
'l10n_fr_pos_cert/static/tests/tours/**/*',
],
},
'author': 'Odoo S.A.',
'license': 'LGPL-3',
}

View file

@ -3,8 +3,6 @@
<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>
@ -14,8 +12,6 @@
<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>
@ -25,8 +21,6 @@
<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>

View file

@ -4,10 +4,10 @@
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-10-30 09:34+0000\n"
"PO-Revision-Date: 2024-10-30 09:34+0000\n"
"POT-Creation-Date: 2025-12-30 19:06+0000\n"
"PO-Revision-Date: 2024-11-04 08:46+0000\n"
"Last-Translator: Manon Rondou <ronm@odoo.com>\n"
"Language-Team: \n"
"Language: fr\n"
@ -19,22 +19,19 @@ msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/res_company.py:0
#, python-format
msgid "(Receipt ref.: %s)"
msgstr "(Réf. reçu : %s)"
#. module: l10n_fr_pos_cert
#. odoo-javascript
#: code:addons/l10n_fr_pos_cert/static/src/xml/OrderReceipt.xml:0
#: code:addons/l10n_fr_pos_cert/static/src/xml/Orderline.xml:0
#, python-format
#: code:addons/l10n_fr_pos_cert/static/src/xml/OrderSummary.xml:0
msgid "/ Units"
msgstr "/ Unité"
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/pos.py:0
#, python-format
msgid "According to French law, you cannot delete a point of sale order."
msgstr ""
"Selon la loi française, vous ne pouvez pas supprimer une commande de caisse."
@ -42,17 +39,6 @@ msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/pos.py:0
#, python-format
msgid ""
"According to the French law, you cannot modify a %s. Forbidden fields: %s."
msgstr ""
"Selon la loi française, vous ne pouvez pas modifier un %s. Champs "
"interdits : %s."
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/pos.py:0
#, python-format
msgid ""
"According to the French law, you cannot modify a point of sale order line. "
"Forbidden fields: %s."
@ -63,7 +49,6 @@ msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/pos.py:0
#, python-format
msgid ""
"According to the French law, you cannot modify a point of sale order. "
"Forbidden fields: %s."
@ -74,12 +59,11 @@ msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.form_view_account_sale_closing
msgid "Account Closing"
msgstr "Clotûre de Compte"
msgstr "Clotûre de compte"
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/res_company.py:0
#, python-format
msgid ""
"Accounting is not unalterable for the company %s. This mechanism is designed "
"for companies where accounting is unalterable."
@ -90,7 +74,6 @@ msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/pos.py:0
#, python-format
msgid ""
"An error occurred when computing the inalterability. Impossible to get the "
"unique previous posted point of sale order."
@ -99,13 +82,6 @@ msgstr ""
"Impossible de récupérer la dernière commande de caisse unique et "
"comptabilisée."
#. module: l10n_fr_pos_cert
#. odoo-javascript
#: code:addons/l10n_fr_pos_cert/static/src/js/Chrome.js:0
#, python-format
msgid "An unknown error prevents us from getting closing information."
msgstr "Une erreur inconnue empêche d'obtenir les informations de fermeture."
#. module: l10n_fr_pos_cert
#: model:ir.model.fields.selection,name:l10n_fr_pos_cert.selection__account_sale_closing__frequency__annually
msgid "Annual"
@ -114,10 +90,14 @@ msgstr "Annuelle"
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/account_closing.py:0
#, python-format
msgid "Annual Closing"
msgstr "Clôture annuelle"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Aucune donnée corrompue n'a été détectée."
msgstr "Aucune donnée corrompue n'a été détectée."
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__date_closing_stop
msgid "Closing Date"
@ -138,6 +118,11 @@ msgstr "Sociétés"
msgid "Company"
msgstr "Société"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Contexte"
msgstr "Contexte"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Contrôle des données du point de vente"
@ -146,7 +131,6 @@ msgstr "Contrôle des données du point de vente"
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/res_company.py:0
#, python-format
msgid "Corrupted data on point of sale order with id %s."
msgstr "Données corrompues sur la commande de caisse avec l'id %s."
@ -163,7 +147,7 @@ msgstr "Créé le"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__cumulative_total
msgid "Cumulative Grand Total"
msgstr "Grant Total Cumulé"
msgstr "Grand total cumulé"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__currency_id
@ -178,9 +162,13 @@ msgstr "Journalière"
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/account_closing.py:0
#, python-format
msgid "Daily Closing"
msgstr "Clôture Journalière"
msgstr "Clôture journalière"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Date"
msgstr "Date"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,help:l10n_fr_pos_cert.field_account_sale_closing__date_closing_start
@ -193,7 +181,19 @@ msgid "Date to which the values are computed"
msgstr "Date jusqu'à laquelle les valeurs sont calculées"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Dernière transaction"
msgstr "Dernière transaction"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_fiscal_position__display_name
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__display_name
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_config__display_name
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order__display_name
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order_line__display_name
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_session__display_name
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_report_l10n_fr_pos_cert_report_pos_hash_integrity__display_name
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_res_company__display_name
msgid "Display Name"
msgstr "Nom affiché"
@ -202,16 +202,6 @@ msgstr "Nom affiché"
msgid "Données corrompues sur la commande du point de vente:"
msgstr "Données corrompues sur la commande du point de vente:"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "First Entry"
msgstr "Première entrée"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "First Hash"
msgstr "Premier hachage"
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_account_fiscal_position
msgid "Fiscal Position"
@ -229,19 +219,16 @@ msgstr "Fréquence et numéro de séquence unique"
#. module: l10n_fr_pos_cert
#: model:ir.actions.server,name:l10n_fr_pos_cert.account_sale_closing_annually_ir_actions_server
#: model:ir.cron,cron_name:l10n_fr_pos_cert.account_sale_closing_annually
msgid "Generate Annual Sales Closing"
msgstr "Générer la clôture annuelle des ventes"
#. module: l10n_fr_pos_cert
#: model:ir.actions.server,name:l10n_fr_pos_cert.account_sale_closing_daily_ir_actions_server
#: model:ir.cron,cron_name:l10n_fr_pos_cert.account_sale_closing_daily
msgid "Generate Daily Sales Closing"
msgstr "Générer la clôture journalière des ventes"
#. module: l10n_fr_pos_cert
#: model:ir.actions.server,name:l10n_fr_pos_cert.account_sale_closing_monthly_ir_actions_server
#: model:ir.cron,cron_name:l10n_fr_pos_cert.account_sale_closing_monthly
msgid "Generate Monthly Sales Closing"
msgstr "Générer la clôture mensuelle des ventes"
@ -252,6 +239,11 @@ msgstr ""
"Obtenir le résultat de la vérication d'intégrité de la caisse française en "
"PDF."
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Hachage"
msgstr "Hachage"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.pos_order_form_inherit
msgid "Hash"
@ -263,7 +255,14 @@ msgid "Hash integrity result PDF"
msgstr "Résultat d'intégrité en PDF"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_fiscal_position__id
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__id
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_config__id
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order__id
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order_line__id
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_session__id
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_report_l10n_fr_pos_cert_report_pos_hash_integrity__id
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_res_company__id
msgid "ID"
msgstr "ID"
@ -290,30 +289,25 @@ msgstr "Texte à hacher"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid ""
"La chaîne de hachage est conforme: il nest pas possible daltérer les "
"données\n"
" sans casser la chaîne de hachage "
"pour les pièces ultérieures."
"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érieurs"
msgstr ""
"La chaîne de hachage est conforme: il nest pas possible daltérer les "
"données\n"
" sans casser la chaîne de hachage "
"pour les pièces ultérieures."
"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érieurs"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Last Entry"
msgstr "Dernière entrée"
msgid "La date (année-mois-jour-heure-minute)"
msgstr "La date (année-mois-jour-heure-minute)"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Last Hash"
msgstr "Dernier hachage"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing____last_update
msgid "Last Modified on"
msgstr "Dernière modification le"
msgid ""
"La fonction de hachage garantit que les données suivates des transactions "
"sont inaltérables:"
msgstr ""
"La fonction de hachage garantit que les données suivates des transactions "
"sont inaltérables:"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__last_order_hash
@ -340,10 +334,33 @@ msgstr "Dernière mise à jour par"
msgid "Last Updated on"
msgstr "Dernière mise à jour le"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid ""
"Le détail des articles ou prestations (libellé, quantité, prix unitaire, "
"total hors taxes de la ligne, taux de TVA associé)"
msgstr ""
"Le détail des articles ou prestations (libellé, quantité, prix unitaire, "
"total hors taxes de la ligne, taux de TVA associé)"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Le montant total toutes taxes comprises"
msgstr "Le montant total toutes taxes comprises"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Le numéro de caisse"
msgstr "Le numéro de caisse"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Le numéro du justificatif"
msgstr "Le numéro du justificatif"
#. module: l10n_fr_pos_cert
#. odoo-javascript
#: code:addons/l10n_fr_pos_cert/static/src/js/pos.js:0
#, python-format
#: code:addons/l10n_fr_pos_cert/static/src/app/services/pos.js:0
msgid "Missing Country"
msgstr "Pays manquant"
@ -355,7 +372,6 @@ msgstr "Mensuelle"
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/account_closing.py:0
#, python-format
msgid "Monthly Closing"
msgstr "Clôture mensuelle"
@ -364,20 +380,12 @@ msgstr "Clôture mensuelle"
msgid "Name"
msgstr "Nom"
#. module: l10n_fr_pos_cert
#. odoo-javascript
#: code:addons/l10n_fr_pos_cert/static/src/js/Chrome.js:0
#, python-format
msgid "Network Error"
msgstr "Erreur réseau"
#. module: l10n_fr_pos_cert
#. odoo-javascript
#: code:addons/l10n_fr_pos_cert/static/src/xml/OrderReceipt.xml:0
#: code:addons/l10n_fr_pos_cert/static/src/xml/Orderline.xml:0
#, python-format
#: code:addons/l10n_fr_pos_cert/static/src/xml/OrderSummary.xml:0
msgid "Old unit price:"
msgstr "Anciennement:"
msgstr "Anciennement :"
#. module: l10n_fr_pos_cert
#: model:ir.actions.server,name:l10n_fr_pos_cert.action_check_pos_hash_integrity
@ -390,13 +398,6 @@ msgstr "Vérification d'inaltérabilité de la caisse"
msgid "Period Total"
msgstr "Total sur la période"
#. module: l10n_fr_pos_cert
#. odoo-javascript
#: code:addons/l10n_fr_pos_cert/static/src/js/Chrome.js:0
#, python-format
msgid "Please check your internet connection and try again."
msgstr "Veuillez vérifier votre connexion internet et réessayez."
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_pos_config
msgid "Point of Sale Configuration"
@ -417,6 +418,31 @@ msgstr "Commandes du point de vente"
msgid "Point of Sale Session"
msgstr "Session du point de vente"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order__pos_version
msgid "Pos Version"
msgstr "Version du PdV"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Première transaction"
msgstr "Première transaction"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order__previous_order_id
msgid "Previous Order"
msgstr "Commande précédente"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Réf. Commande"
msgstr "Réf. Commande"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Résultat du test"
msgstr "Résultat du test"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Résultat du test d'intégrité -"
@ -430,7 +456,6 @@ msgstr "Clôture des ventes"
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/account_closing.py:0
#, python-format
msgid ""
"Sale Closings are not meant to be written or deleted under any circumstances."
msgstr "Les clôtures de ventes ne peuvent ni être modifiées ni supprimées."
@ -457,14 +482,14 @@ msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid ""
"Selon larticle 286 du code général des impôts français, toute livraison de "
"Selon l'article 286 du code général des impôts français, toute livraison de "
"bien ou prestation\n"
" de services ne donnant pas lieu à "
"facturation et étant enregistrée au moyen dun logiciel ou\n"
" dun système de caisse doit "
"satisfaire à des conditions dinaltérabilité et de sécurisation des\n"
" données en vue dun contrôle de "
"ladministration fiscale.\n"
"facturation et étant enregistrée au moyen d'un logiciel ou\n"
" d'un système de caisse doit "
"satisfaire à des conditions d'inaltérabilité et de sécurisation des\n"
" données en vue d'un contrôle de "
"l'administration fiscale.\n"
" <br/>\n"
" <br/>\n"
" Ces conditions sont respectées via "
@ -472,14 +497,14 @@ msgid ""
" <br/>\n"
" <br/>"
msgstr ""
"Selon larticle 286 du code général des impôts français, toute livraison de "
"Selon l'article 286 du code général des impôts français, toute livraison de "
"bien ou prestation\n"
" de services ne donnant pas lieu à "
"facturation et étant enregistrée au moyen dun logiciel ou\n"
" dun système de caisse doit "
"satisfaire à des conditions dinaltérabilité et de sécurisation des\n"
" données en vue dun contrôle de "
"ladministration fiscale.\n"
"facturation et étant enregistrée au moyen d'un logiciel ou\n"
" d'un système de caisse doit "
"satisfaire à des conditions d'inaltérabilité et de sécurisation des\n"
" données en vue d'un contrôle de "
"l'administration fiscale.\n"
" <br/>\n"
" <br/>\n"
" Ces conditions sont respectées via "
@ -504,8 +529,7 @@ msgstr "Les clôtures sont créées par Odoo"
#. module: l10n_fr_pos_cert
#. odoo-javascript
#: code:addons/l10n_fr_pos_cert/static/src/js/pos.js:0
#, python-format
#: code:addons/l10n_fr_pos_cert/static/src/app/services/pos.js:0
msgid "The company %s doesn't have a country set."
msgstr "La société %s n'a pas de pays configuré."
@ -517,7 +541,6 @@ msgstr "La devis de la société"
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/res_company.py:0
#, 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 "
@ -545,25 +568,27 @@ msgstr "Total sur les comptes débiteurs depuis l'installation de la caisse"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid ""
"Toutes les ventes effectuées via le Point de Vente\n"
" sont bien dans la chaîne de "
"hachage."
"Toutes les données liées à la réception du paiement en contrepartie (mode de "
"réglement notamment)"
msgstr ""
"Toutes les ventes effectuées via le Point de Vente\n"
" sont bien dans la chaîne de "
"hachage."
"Toutes les données liées à la réception du paiement en contrepartie (mode de "
"réglement notamment)"
#. module: l10n_fr_pos_cert
#. odoo-javascript
#: code:addons/l10n_fr_pos_cert/static/src/js/Chrome.js:0
#, python-format
msgid "Unknown Error"
msgstr "Erreur inconnue"
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid ""
"Toutes les ventes effectuées via le Point de Vente sont bien dans la chaîne "
"de hachage."
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,help:l10n_fr_pos_cert.field_pos_order__pos_version
msgid "Version of Odoo that created the order"
msgstr "Version d'Odoo utilisée pour la création de la commande"
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/account_fiscal_position.py:0
#, python-format
msgid ""
"You cannot modify a fiscal position used in a POS order. You should archive "
"it and create a new one."
@ -574,7 +599,6 @@ msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/pos.py:0
#, python-format
msgid ""
"You cannot overwrite the values ensuring the inalterability of the point of "
"sale."
@ -585,6 +609,5 @@ msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/pos.py:0
#, python-format
msgid "You have to set a country in your company setting."
msgstr "Vous devez définir un pays dans les paramètres de votre entreprise."

View file

@ -4,10 +4,10 @@
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Project-Id-Version: Odoo Server 19.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-10-30 09:34+0000\n"
"PO-Revision-Date: 2024-10-30 09:34+0000\n"
"POT-Creation-Date: 2025-12-30 19:06+0000\n"
"PO-Revision-Date: 2025-12-30 19:06+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@ -18,37 +18,25 @@ msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/res_company.py:0
#, python-format
msgid "(Receipt ref.: %s)"
msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-javascript
#: code:addons/l10n_fr_pos_cert/static/src/xml/OrderReceipt.xml:0
#: code:addons/l10n_fr_pos_cert/static/src/xml/Orderline.xml:0
#, python-format
#: code:addons/l10n_fr_pos_cert/static/src/xml/OrderSummary.xml:0
msgid "/ Units"
msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/pos.py:0
#, python-format
msgid "According to French law, you cannot delete a point of sale order."
msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/pos.py:0
#, python-format
msgid ""
"According to the French law, you cannot modify a %s. Forbidden fields: %s."
msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/pos.py:0
#, python-format
msgid ""
"According to the French law, you cannot modify a point of sale order line. "
"Forbidden fields: %s."
@ -57,7 +45,6 @@ msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/pos.py:0
#, python-format
msgid ""
"According to the French law, you cannot modify a point of sale order. "
"Forbidden fields: %s."
@ -71,7 +58,6 @@ msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/res_company.py:0
#, python-format
msgid ""
"Accounting is not unalterable for the company %s. This mechanism is designed"
" for companies where accounting is unalterable."
@ -80,19 +66,11 @@ msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/pos.py:0
#, python-format
msgid ""
"An error occurred when computing the inalterability. Impossible to get the "
"unique previous posted point of sale order."
msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-javascript
#: code:addons/l10n_fr_pos_cert/static/src/js/Chrome.js:0
#, python-format
msgid "An unknown error prevents us from getting closing information."
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields.selection,name:l10n_fr_pos_cert.selection__account_sale_closing__frequency__annually
msgid "Annual"
@ -101,10 +79,14 @@ msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/account_closing.py:0
#, python-format
msgid "Annual Closing"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Aucune donnée corrompue n'a été détectée."
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__date_closing_stop
msgid "Closing Date"
@ -125,6 +107,11 @@ msgstr ""
msgid "Company"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Contexte"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Contrôle des données du point de vente"
@ -133,7 +120,6 @@ msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/res_company.py:0
#, python-format
msgid "Corrupted data on point of sale order with id %s."
msgstr ""
@ -165,10 +151,14 @@ msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/account_closing.py:0
#, python-format
msgid "Daily Closing"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Date"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,help:l10n_fr_pos_cert.field_account_sale_closing__date_closing_start
msgid "Date from which the total interval is computed"
@ -180,7 +170,19 @@ msgid "Date to which the values are computed"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Dernière transaction"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_fiscal_position__display_name
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__display_name
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_config__display_name
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order__display_name
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order_line__display_name
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_session__display_name
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_report_l10n_fr_pos_cert_report_pos_hash_integrity__display_name
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_res_company__display_name
msgid "Display Name"
msgstr ""
@ -189,16 +191,6 @@ msgstr ""
msgid "Données corrompues sur la commande du point de vente:"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "First Entry"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "First Hash"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_account_fiscal_position
msgid "Fiscal Position"
@ -216,19 +208,16 @@ msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.actions.server,name:l10n_fr_pos_cert.account_sale_closing_annually_ir_actions_server
#: model:ir.cron,cron_name:l10n_fr_pos_cert.account_sale_closing_annually
msgid "Generate Annual Sales Closing"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.actions.server,name:l10n_fr_pos_cert.account_sale_closing_daily_ir_actions_server
#: model:ir.cron,cron_name:l10n_fr_pos_cert.account_sale_closing_daily
msgid "Generate Daily Sales Closing"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.actions.server,name:l10n_fr_pos_cert.account_sale_closing_monthly_ir_actions_server
#: model:ir.cron,cron_name:l10n_fr_pos_cert.account_sale_closing_monthly
msgid "Generate Monthly Sales Closing"
msgstr ""
@ -237,6 +226,11 @@ msgstr ""
msgid "Get french pos hash integrity result as PDF."
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Hachage"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.pos_order_form_inherit
msgid "Hash"
@ -248,7 +242,14 @@ msgid "Hash integrity result PDF"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_fiscal_position__id
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__id
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_config__id
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order__id
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order_line__id
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_session__id
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_report_l10n_fr_pos_cert_report_pos_hash_integrity__id
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_res_company__id
msgid "ID"
msgstr ""
@ -275,23 +276,20 @@ msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid ""
"La chaîne de hachage est conforme: il nest pas possible daltérer les données\n"
" sans casser la chaîne de hachage pour les pièces ultérieures."
"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érieurs"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Last Entry"
msgid "La date (année-mois-jour-heure-minute)"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Last Hash"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing____last_update
msgid "Last Modified on"
msgid ""
"La fonction de hachage garantit que les données suivates des transactions "
"sont inaltérables:"
msgstr ""
#. module: l10n_fr_pos_cert
@ -319,10 +317,31 @@ msgstr ""
msgid "Last Updated on"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid ""
"Le détail des articles ou prestations (libellé, quantité, prix unitaire, "
"total hors taxes de la ligne, taux de TVA associé)"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Le montant total toutes taxes comprises"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Le numéro de caisse"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Le numéro du justificatif"
msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-javascript
#: code:addons/l10n_fr_pos_cert/static/src/js/pos.js:0
#, python-format
#: code:addons/l10n_fr_pos_cert/static/src/app/services/pos.js:0
msgid "Missing Country"
msgstr ""
@ -334,7 +353,6 @@ msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/account_closing.py:0
#, python-format
msgid "Monthly Closing"
msgstr ""
@ -343,18 +361,10 @@ msgstr ""
msgid "Name"
msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-javascript
#: code:addons/l10n_fr_pos_cert/static/src/js/Chrome.js:0
#, python-format
msgid "Network Error"
msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-javascript
#: code:addons/l10n_fr_pos_cert/static/src/xml/OrderReceipt.xml:0
#: code:addons/l10n_fr_pos_cert/static/src/xml/Orderline.xml:0
#, python-format
#: code:addons/l10n_fr_pos_cert/static/src/xml/OrderSummary.xml:0
msgid "Old unit price:"
msgstr ""
@ -369,13 +379,6 @@ msgstr ""
msgid "Period Total"
msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-javascript
#: code:addons/l10n_fr_pos_cert/static/src/js/Chrome.js:0
#, python-format
msgid "Please check your internet connection and try again."
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_pos_config
msgid "Point of Sale Configuration"
@ -396,6 +399,31 @@ msgstr ""
msgid "Point of Sale Session"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order__pos_version
msgid "Pos Version"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Première transaction"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order__previous_order_id
msgid "Previous Order"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Réf. Commande"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Résultat du test"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Résultat du test d'intégrité -"
@ -409,8 +437,6 @@ msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/account_closing.py:0
#: code:addons/l10n_fr_pos_cert/models/account_closing.py:0
#, python-format
msgid ""
"Sale Closings are not meant to be written or deleted under any "
"circumstances."
@ -434,10 +460,10 @@ msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid ""
"Selon larticle 286 du code général des impôts français, toute livraison de bien ou prestation\n"
" de services ne donnant pas lieu à facturation et étant enregistrée au moyen dun logiciel ou\n"
" dun système de caisse doit satisfaire à des conditions dinaltérabilité et de sécurisation des\n"
" données en vue dun contrôle de ladministration fiscale.\n"
"Selon l'article 286 du code général des impôts français, toute livraison de bien ou prestation\n"
" de services ne donnant pas lieu à facturation et étant enregistrée au moyen d'un logiciel ou\n"
" d'un système de caisse doit satisfaire à des conditions d'inaltérabilité et de sécurisation des\n"
" données en vue d'un contrôle de l'administration fiscale.\n"
" <br/>\n"
" <br/>\n"
" Ces conditions sont respectées via une fonction de hachage des ventes du Point de Vente.\n"
@ -462,8 +488,7 @@ msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-javascript
#: code:addons/l10n_fr_pos_cert/static/src/js/pos.js:0
#, python-format
#: code:addons/l10n_fr_pos_cert/static/src/app/services/pos.js:0
msgid "The company %s doesn't have a country set."
msgstr ""
@ -475,7 +500,6 @@ msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/res_company.py:0
#, 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 "
@ -497,21 +521,25 @@ msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid ""
"Toutes les ventes effectuées via le Point de Vente\n"
" sont bien dans la chaîne de hachage."
"Toutes les données liées à la réception du paiement en contrepartie (mode de"
" réglement notamment)"
msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-javascript
#: code:addons/l10n_fr_pos_cert/static/src/js/Chrome.js:0
#, python-format
msgid "Unknown Error"
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid ""
"Toutes les ventes effectuées via le Point de Vente sont bien dans la chaîne "
"de hachage."
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,help:l10n_fr_pos_cert.field_pos_order__pos_version
msgid "Version of Odoo that created the order"
msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/account_fiscal_position.py:0
#, python-format
msgid ""
"You cannot modify a fiscal position used in a POS order. You should archive "
"it and create a new one."
@ -520,7 +548,6 @@ msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/pos.py:0
#, python-format
msgid ""
"You cannot overwrite the values ensuring the inalterability of the point of "
"sale."
@ -529,6 +556,5 @@ msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-python
#: code:addons/l10n_fr_pos_cert/models/pos.py:0
#, python-format
msgid "You have to set a country in your company setting."
msgstr ""

View file

@ -1,16 +1,14 @@
# -*- 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.fields import Datetime as FieldDateTime, Domain
from dateutil.relativedelta import relativedelta
from odoo.tools.translate import _
from odoo.exceptions import UserError
from odoo.osv.expression import AND
class AccountClosing(models.Model):
class AccountSaleClosing(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
@ -42,8 +40,9 @@ class AccountClosing(models.Model):
JOIN account_journal j ON aml.journal_id = j.id
JOIN account_account acc ON acc.id = aml.account_id
JOIN account_move m ON m.id = aml.move_id
JOIN res_company move_company ON move_company.id = m.company_id
WHERE j.type = 'sale'
AND aml.company_id = %(company_id)s
AND SPLIT_PART(move_company.parent_path, '/', 1)::int = %(company_id)s
AND m.state = 'posted'
AND acc.account_type = 'asset_receivable' '''
@ -87,12 +86,12 @@ class AccountClosing(models.Model):
date_start = previous_closing.create_date
cumulative_total += previous_closing.cumulative_total
domain = [('company_id', '=', company.id), ('state', 'in', ('paid', 'done', 'invoiced'))]
domain = Domain('company_id', '=', company.id) & Domain('state', 'in', ('paid', 'done'))
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)]])
domain &= 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)]])
# the first time we compute the closing, we consider only from the installation of the module
domain &= Domain('date_order', '>=', date_start)
orders = self.env['pos.order'].search(domain, order='date_order desc')

View file

@ -1,17 +1,17 @@
# -*- 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
from json import dumps, loads
import logging
from collections import defaultdict
from odoo import models, api, fields, release, _
from odoo.exceptions import UserError
class pos_config(models.Model):
_logger = logging.getLogger(__name__)
class PosConfig(models.Model):
_inherit = 'pos.config'
def open_ui(self):
@ -21,13 +21,10 @@ class pos_config(models.Model):
if config.company_id._is_accounting_unalterable():
if config.current_session_id:
config.current_session_id._check_session_timing()
return super(pos_config, self).open_ui()
def _config_sequence_implementation(self):
return 'no_gap' if self.env.company._is_accounting_unalterable() else super()._config_sequence_implementation()
return super().open_ui()
class pos_session(models.Model):
class PosSession(models.Model):
_inherit = 'pos.session'
def _check_session_timing(self):
@ -39,35 +36,61 @@ class pos_session(models.Model):
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()
return super().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']
ORDER_FIELDS_BEFORE_17_4 = ['date_order', 'user_id', 'lines', 'payment_ids', 'pricelist_id', 'session_id', 'pos_reference', 'sale_journal', 'fiscal_position_id', 'partner_id']
ORDER_FIELDS_FROM_17_4 = ['date_order', 'user_id', 'lines', 'payment_ids', 'pricelist_id', 'session_id', 'pos_reference', 'sale_journal', 'fiscal_position_id', 'pos_version']
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):
class PosOrder(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)
previous_order_id = fields.Many2one('pos.order', string='Previous Order', readonly=True, compute='_compute_previous_order', store=True, copy=False)
pos_version = fields.Char(help="Version of Odoo that created the order", readonly=True, copy=False)
def _get_new_hash(self, secure_seq_number):
@api.depends('l10n_fr_secure_sequence_number')
def _compute_previous_order(self):
orders_by_company = defaultdict(list)
for order in self.filtered(lambda o: o.l10n_fr_secure_sequence_number):
orders_by_company[order.company_id.id].append(order)
for company_id, orders in orders_by_company.items():
# Since sequence number can't be zero, we don't consider
# it as a posible previous sequence number
prev_seq = [o.l10n_fr_secure_sequence_number - 1 for o in orders if o.l10n_fr_secure_sequence_number > 1]
prev_orders = self.search([
('state', 'in', ['paid', 'done']),
('company_id', '=', company_id),
('l10n_fr_secure_sequence_number', 'in', prev_seq),
])
prev_map = defaultdict(list)
for po in prev_orders:
prev_map[po.l10n_fr_secure_sequence_number].append(po)
for order in orders:
match = prev_map.get(order.l10n_fr_secure_sequence_number - 1, [])
if len(match) > 1:
raise UserError(_('An error occurred when computing the inalterability. Impossible to get the unique previous posted point of sale order.'))
order.previous_order_id = match[0] if match else False
def _get_new_hash(self):
""" 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 occurred 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'')
# build and return the hash
computed_hash = self._compute_hash(self.previous_order_id.l10n_fr_hash if self.previous_order_id else '')
_logger.info(
'Computed hash for order ID %s: %s \n String to hash: %s \n Previous hash: %s',
self.id,
computed_hash,
dumps(loads(self.l10n_fr_string_to_hash), indent=2),
self.previous_order_id.l10n_fr_hash
)
return computed_hash
def _compute_hash(self, previous_hash):
""" Computes the hash of the browse_record given as self, based on the hash
@ -91,7 +114,7 @@ class pos_order(models.Model):
relational_ids = defaultdict(set)
for data_list, field_names, field_defs in (
(orders_data, ORDER_FIELDS, order_field_defs),
(orders_data, fields_to_fetch, order_field_defs),
(lines_data, LINE_FIELDS, line_field_defs),
):
for record in data_list:
@ -108,8 +131,8 @@ class pos_order(models.Model):
sorted_relational_ids[model_name] = self.env[model_name].search([('id', 'in', list(ids))]).ids
return sorted_relational_ids
orders_data = self.read(ORDER_FIELDS + ['id'], load='')
fields_to_fetch = list(set(ORDER_FIELDS_BEFORE_17_4) | set(ORDER_FIELDS_FROM_17_4))
orders_data = self.read(fields_to_fetch + ['id'], load='')
lines_data = self.lines.read(LINE_FIELDS + ['id', 'order_id'], load='')
orders_by_id = {order['id']: order for order in orders_data}
@ -121,7 +144,7 @@ class pos_order(models.Model):
'type': self._fields[field].type,
'comodel': self._fields[field].comodel_name if hasattr(self._fields[field], 'comodel_name') else None
}
for field in ORDER_FIELDS
for field in fields_to_fetch
}
line_field_defs = {
field: {
@ -135,9 +158,15 @@ class pos_order(models.Model):
for order in self:
values = {}
if order.pos_version:
order_fields = ORDER_FIELDS_FROM_17_4
else:
order_fields = ORDER_FIELDS_BEFORE_17_4
for field in order_fields:
values[field] = _getattrstring(order, field)
order_data = orders_by_id[order.id]
for field in ORDER_FIELDS:
for field in order_fields:
field_def = order_field_defs[field]
values[field] = _getattrstring(order_data.get(field), field_def['type'], field_def['comodel'])
@ -153,29 +182,38 @@ class pos_order(models.Model):
ensure_ascii=True, indent=None,
separators=(',',':'))
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
vals['pos_version'] = release.version
return super().create(vals_list)
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']:
if vals.get('state') in ['paid', 'done']:
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)):
if order.pos_version:
ORDER_FIELDS = ORDER_FIELDS_FROM_17_4
else:
ORDER_FIELDS = ORDER_FIELDS_BEFORE_17_4
if (order.state in ['paid', 'done'] 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)
res = super().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)
res |= super(PosOrder, order).write({'l10n_fr_secure_sequence_number': new_number})
res |= super(PosOrder, order).write({'l10n_fr_hash': order._get_new_hash()})
return res
@api.ondelete(at_uninstall=True)
@ -184,10 +222,6 @@ class pos_order(models.Model):
if order.company_id._is_accounting_unalterable():
raise UserError(_("According to French law, you cannot delete a point of sale order."))
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"
@ -195,6 +229,6 @@ class PosOrderLine(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._is_accounting_unalterable() and l.order_id.state in ['done', 'invoiced'] for l in self):
if any(l.company_id._is_accounting_unalterable() and (l.order_id.account_move or l.order_id.state == 'done') 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)

View file

@ -10,14 +10,14 @@ 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 or 'UTC')
ctx = record.env.context
timestamp = Datetime.from_string(record[field])
if ctx.get('lang'):
res_lang = record.env['res.lang']._lang_get(ctx['lang'])
res_lang = record.env['res.lang']._get_data(code=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)
tz = record.env.tz
return datetime.strftime(timestamp.astimezone(tz), res_lang.date_format + ' ' + res_lang.time_format)
return Datetime.context_timestamp(record, timestamp)
@ -60,7 +60,7 @@ class ResCompany(models.Model):
msg_alert = ''
report_dict = {}
if self._is_accounting_unalterable():
orders = self.with_context(prefetch_fields=False).env['pos.order'].search([('state', 'in', ['paid', 'done', 'invoiced']), ('company_id', '=', self.id),
orders = self.with_context(prefetch_fields=False).env['pos.order'].search([('state', 'in', ['paid', 'done']), ('company_id', '=', self.id),
('l10n_fr_secure_sequence_number', '!=', 0)], order="l10n_fr_secure_sequence_number ASC")
if not orders:
@ -96,4 +96,4 @@ class ResCompany(models.Model):
'corrupted_orders': corrupted_orders or 'None'
}
else:
raise UserError(_('Accounting is not unalterable for the company %s. This mechanism is designed for companies where accounting is unalterable.') % self.env.company.name)
raise UserError(_('Accounting is not unalterable for the company %s. This mechanism is designed for companies where accounting is unalterable.', self.env.company.name))

View file

@ -4,7 +4,7 @@
from odoo import api, fields, models
class ReportPosHashIntegrity(models.AbstractModel):
class ReportL10n_Fr_Pos_CertReport_Pos_Hash_Integrity(models.AbstractModel):
_name = 'report.l10n_fr_pos_cert.report_pos_hash_integrity'
_description = 'Get french pos hash integrity result as PDF.'

View file

@ -9,17 +9,20 @@
<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>
<h2>Résultat du test d'intégrité - <span t-out="data['printing_date']"/></h2>
<br/>
</div>
</div>
<div class="row">
<div class="col-12" id="hash_config_review">
<br/>
<h3>Contexte</h3>
<br/>
<h6>
Selon larticle 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 dun logiciel ou
dun système de caisse doit satisfaire à des conditions dinaltérabilité et de sécurisation des
données en vue dun contrôle de ladministration fiscale.
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.
@ -34,64 +37,70 @@
<br/>
<h3>Contrôle des données du point de vente</h3>
<br/>
<t t-if="data['result'] != 'None' and data['corrupted_orders'] == 'None'">
<h5>
Toutes les ventes effectuées via le Point de Vente
sont bien dans la chaîne de hachage.
</h5>
<br/>
</t>
<ul>
<li t-if="data['result'] != 'None' and data['corrupted_orders'] == 'None'">Toutes les ventes effectuées via le Point de Vente sont bien dans la chaîne de hachage.</li>
<li>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érieurs</li>
<li>La fonction de hachage garantit que les données suivates des transactions sont inaltérables:
<ul>
<li>Le numéro du justificatif</li>
<li>La date (année-mois-jour-heure-minute)</li>
<li>Le numéro de caisse</li>
<li>Le montant total toutes taxes comprises</li>
<li>Le détail des articles ou prestations (libellé, quantité, prix unitaire, total hors taxes de la ligne, taux de TVA associé)</li>
<li>Toutes les données liées à la réception du paiement en contrepartie (mode de réglement notamment)</li>
</ul>
</li>
</ul>
</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">
<table class="table table-borderless" style="table-layout: fixed">
<thead>
<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>
<th class="text-center col-2" scope="col"></th>
<th class="text-center col-5" scope="col">Première transaction</th>
<th class="text-center col-5" scope="col">Dernière transaction</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 t-if="data['result']['first_order_hash'] != 'None'">
<tr>
<td>Date</td>
<td><span t-out="data['result']['first_order_date']"/></td>
<td><span t-out="data['result']['last_order_date']"/></td>
</tr>
<tr>
<td>Réf. Commande</td>
<td><span t-out="data['result']['first_order_name']"/></td>
<td><span t-out="data['result']['last_order_name']"/></td>
</tr>
<tr>
<td>Hachage</td>
<td><span t-out="data['result']['first_order_hash']"/></td>
<td><span t-out="data['result']['last_order_hash']"/></td>
</tr>
</t>
</tbody>
</table>
</div>
</div>
<div>
<div class="col-12" id="hash_results">
<br/>
<h3>Résultat du test</h3>
<br/>
<t t-if="data['corrupted_orders'] != 'None'">
<h5>
<h5 class="bg-danger">
Données corrompues sur la commande du point de vente:
</h5>
<span t-esc="data['corrupted_orders']"/> <br/>
<span t-out="data['corrupted_orders']"/>
<br/>
</t>
<t t-else="">
<h5 class="bg-success">
Aucune donnée corrompue n'a été détectée.
</h5>
</t>
</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 nest pas possible daltérer les données
sans casser la chaîne de hachage pour les pièces ultérieures.
</h6>
<br/>
</div>
</div>
</t>
</div>

View file

@ -0,0 +1,21 @@
import { PosStore } from "@point_of_sale/app/services/pos_store";
import { _t } from "@web/core/l10n/translation";
import { patch } from "@web/core/utils/patch";
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
patch(PosStore.prototype, {
is_french_country() {
const french_countries = ["FR", "MF", "MQ", "NC", "PF", "RE", "GF", "GP", "TF"];
if (!this.company.country_id) {
this.dialog.add(AlertDialog, {
title: _t("Missing Country"),
body: _t("The company %s doesn't have a country set.", this.company.name),
});
return false;
}
return french_countries.includes(this.company.country_id?.code);
},
canEditPayment(order) {
return this.is_french_country() ? false : super.canEditPayment(order);
},
});

View file

@ -1,41 +1,23 @@
odoo.define('l10n_fr_pos_cert.Chrome', function (require) {
'use strict';
import { Chrome } from "@point_of_sale/app/pos_app";
import { patch } from "@web/core/utils/patch";
import { ClosePosPopup } from "@point_of_sale/app/components/popups/closing_popup/closing_popup";
import { onMounted } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
const Chrome = require('point_of_sale.Chrome');
const Registries = require('point_of_sale.Registries');
const { isConnectionError } = require('point_of_sale.utils');
const PosFrCertChrome = (Chrome) =>
class extends Chrome {
async start() {
await super.start();
if (this.env.pos.is_french_country() && this.env.pos.pos_session.start_at) {
const now = Date.now();
let limitDate = new Date(this.env.pos.pos_session.start_at);
limitDate.setDate(limitDate.getDate() + 1);
if (limitDate < now) {
try {
const info = await this.env.pos.getClosePosInfo();
this.showPopup('ClosePosPopup', { info: info });
} catch (e) {
if (isConnectionError(e)) {
this.showPopup('OfflineErrorPopup', {
title: this.env._t('Network Error'),
body: this.env._t('Please check your internet connection and try again.'),
});
} else {
this.showPopup('ErrorPopup', {
title: this.env._t('Unknown Error'),
body: this.env._t('An unknown error prevents us from getting closing information.'),
});
}
}
}
patch(Chrome.prototype, {
setup() {
super.setup(...arguments);
this.dialog = useService("dialog");
onMounted(async () => {
if (this.pos.is_french_country() && this.pos.session.start_at) {
const now = Date.now();
const limitDate = new Date(this.pos.session.start_at);
limitDate.setDate(limitDate.getDate() + 1);
if (limitDate.getTime() < now) {
const info = await this.pos.getClosePosInfo();
this.dialog.add(ClosePosPopup, info);
}
}
};
Registries.Component.extend(Chrome, PosFrCertChrome);
return Chrome;
});
},
});

View file

@ -1,29 +0,0 @@
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;
});

View file

@ -1,27 +1,18 @@
odoo.define('l10n_fr_pos_cert.ClosePosPopup', function (require) {
'use strict';
import { ClosePosPopup } from "@point_of_sale/app/components/popups/closing_popup/closing_popup";
import { patch } from "@web/core/utils/patch";
const ClosePosPopup = require('point_of_sale.ClosePosPopup');
const Registries = require('point_of_sale.Registries');
const PosFrCertClosePopup = (ClosePosPopup) =>
class extends ClosePosPopup {
sessionIsOutdated() {
let isOutdated = false;
if (this.env.pos.is_french_country() && this.env.pos.pos_session.start_at) {
const now = Date.now();
let limitDate = new Date(this.env.pos.pos_session.start_at);
limitDate.setDate(limitDate.getDate() + 1);
isOutdated = limitDate < now;
}
return isOutdated;
}
canCancel() {
return super.canCancel() && !this.sessionIsOutdated();
}
};
Registries.Component.extend(ClosePosPopup, PosFrCertClosePopup);
return ClosePosPopup;
patch(ClosePosPopup.prototype, {
sessionIsOutdated() {
let isOutdated = false;
if (this.pos.is_french_country() && this.pos.session.start_at) {
const now = Date.now();
const limitDate = new Date(this.pos.session.start_at);
limitDate.setDate(limitDate.getDate() + 1);
isOutdated = limitDate < now;
}
return isOutdated;
},
canCancel() {
return super.canCancel(...arguments) && !this.sessionIsOutdated();
},
});

View file

@ -1,15 +0,0 @@
odoo.define('l10n_fr_pos_cert.TicketScreen', function(require) {
'use strict';
const TicketScreen = require('point_of_sale.TicketScreen');
const Registries = require('point_of_sale.Registries');
const PosFrCertTicketScreen = TicketScreen => class extends TicketScreen {
shouldHideDeleteButton(order) {
return this.env.pos.is_french_country() && !order.is_empty() || super.shouldHideDeleteButton(order);
}
};
Registries.Component.extend(TicketScreen, PosFrCertTicketScreen);
return PosFrCertTicketScreen;
});

View file

@ -1,85 +0,0 @@
odoo.define('l10n_fr_pos_cert.pos', function (require) {
"use strict";
const { Gui } = require('point_of_sale.Gui');
var { PosGlobalState, Order, Orderline } = require('point_of_sale.models');
var core = require('web.core');
const Registries = require('point_of_sale.Registries');
var _t = core._t;
const L10nFrPosGlobalState = (PosGlobalState) => class L10nFrPosGlobalState extends PosGlobalState {
is_french_country(){
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);
}
disallowLineQuantityChange() {
let result = super.disallowLineQuantityChange(...arguments);
let selectedOrderLine = this.selectedOrder.get_selected_orderline();
//Note: is_reward_line is a field in the pos_loyalty module
if (selectedOrderLine && selectedOrderLine.is_reward_line) {
//Always allow quantity change for reward lines
return false || result;
}
return this.is_french_country() || result;
}
}
Registries.Model.extend(PosGlobalState, L10nFrPosGlobalState);
const L10nFrOrder = (Order) => class L10nFrOrder extends Order {
constructor() {
super(...arguments);
this.l10n_fr_hash = this.l10n_fr_hash || false;
this.save_to_db();
}
export_for_printing() {
var result = super.export_for_printing(...arguments);
result.l10n_fr_hash = this.get_l10n_fr_hash();
return result;
}
set_l10n_fr_hash (l10n_fr_hash){
this.l10n_fr_hash = l10n_fr_hash;
}
get_l10n_fr_hash() {
return this.l10n_fr_hash;
}
wait_for_push_order() {
var result = super.wait_for_push_order(...arguments);
result = Boolean(result || this.pos.is_french_country());
return result;
}
_get_qr_code_data() {
if (this.pos.is_french_country()){
return false;
} else {
return super._get_qr_code_data(...arguments);
}
}
}
Registries.Model.extend(Order, L10nFrOrder);
const L10nFrOrderline = (Orderline) => class L10nFrOrderline extends Orderline {
can_be_merged_with(orderline) {
if (this.pos.is_french_country()) {
const order = this.pos.get_order();
const lastOrderline = order.orderlines.at(order.orderlines.length - 1);
if ((lastOrderline.product.id !== orderline.product.id || lastOrderline.quantity < 0)) {
return false;
}
return super.can_be_merged_with(...arguments);
}
return super.can_be_merged_with(...arguments);
}
}
Registries.Model.extend(Orderline, L10nFrOrderline);
});

View file

@ -1,22 +1,21 @@
<?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 t-name="l10n_fr_pos_cert.OrderReceipt" t-inherit="point_of_sale.OrderReceipt" t-inherit-mode="extension">
<xpath expr="//div[hasclass('before-footer')]" position="inside">
<t t-if="order.l10n_fr_hash !== false">
<div class="text-break pt-3"><t t-out="order.l10n_fr_hash"/></div>
</t>
</xpath>
</t>
<t t-name="OrderLinesReceipt" t-inherit="point_of_sale.OrderLinesReceipt" t-inherit-mode="extension" owl="1">
<xpath expr="//t[@t-foreach='receipt.orderlines']" position="inside">
<t t-if="receipt.l10n_fr_hash !== false and line.price_manually_set">
<t t-name="l10n_fr_pos_cert.OrderReceipt" t-inherit="point_of_sale.OrderReceipt" t-inherit-mode="extension">
<xpath expr="//Orderline" position="inside">
<t t-if="line.order_id.l10n_fr_hash !== false and line.price_type === 'manual' and !props.basic_receipt">
<div class="pos-receipt-right-padding">
Old unit price:
<span class="oldPrice">
<s>
<t t-esc="env.pos.format_currency(line.taxed_lst_unit_price, 'Product Price')" /> / Units
<t t-out="line.product_id.displayPriceUnit" /> / Units
</s>
</span>
</div>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="l10n_fr_pos_cert.OrderSummary" t-inherit="point_of_sale.OrderSummary" t-inherit-mode="extension">
<xpath expr="//Orderline" position="inside" >
<t t-if="pos.is_french_country() !== false and line.price_type === 'manual'">
<li class="info">
Old unit price:
<span class="oldPrice">
<s>
<t t-esc="line.product_id.displayPriceUnit" /> / Units
</s>
</span>
</li>
</t>
</xpath>
</t>
</templates>

View file

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="Orderline" t-inherit="point_of_sale.Orderline" t-inherit-mode="extension" owl="1">
<xpath expr="//ul[hasclass('info-list')]" position="inside">
<t t-if="env.pos.is_french_country() !== false and props.line.price_manually_set">
<li class="info">
Old unit price:
<span class="oldPrice">
<s>
<t t-esc="env.pos.format_currency(props.line.get_taxed_lst_unit_price(),'Product Price')" /> / Units
</s>
</span>
</li>
</t>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,50 @@
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import * as ReceiptScreen from "@point_of_sale/../tests/pos/tours/utils/receipt_screen_util";
import * as PaymentScreen from "@point_of_sale/../tests/pos/tours/utils/payment_screen_util";
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
import * as Numpad from "@point_of_sale/../tests/generic_helpers/numpad_util";
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("l10nFrPosCertSelfInvoicingTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Desk Pad"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
{
trigger: ".pos-receipt #posqrcode",
content: "QR code is visible on the receipt",
},
].flat(),
});
registry.category("web_tour.tours").add("test_correct_old_price_upon_price_change_fr", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Desk Pad"),
ProductScreen.selectedOrderlineHas("Desk Pad", "1", "1.98"),
Numpad.click("Price"),
Numpad.isActive("Price"),
Numpad.click("5"),
ProductScreen.selectedOrderlineHas("Desk Pad", "1", "5.00"),
{
content: "Old unit price is correctly shown",
trigger: ".order-container .orderline.selected:has(.oldPrice:contains(1.98))",
},
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
{
content: "Old unit price is correctly shown",
trigger: ".order-container .orderline:has(.oldPrice:contains(1.98))",
},
].flat(),
});

View file

@ -0,0 +1,6 @@
import { PosConfig } from "@point_of_sale/../tests/unit/data/pos_config.data";
PosConfig._records = PosConfig._records.map((record) => ({
...record,
company_id: 251,
}));

View file

@ -0,0 +1,26 @@
import { ResCompany } from "@point_of_sale/../tests/unit/data/res_company.data";
ResCompany._records = [
...ResCompany._records,
{
id: 251,
currency_id: 125,
email: false,
website: false,
company_registry: false,
vat: false,
name: "My FR Company",
phone: "",
partner_id: 1,
country_id: 75,
state_id: false,
tax_calculation_rounding_method: "round_per_line",
point_of_sale_use_ticket_qr_code: true,
point_of_sale_ticket_unique_code: false,
point_of_sale_ticket_portal_url_display_mode: "qr_code_and_url",
street: "",
city: "",
zip: "",
account_fiscal_country_id: 75,
},
];

View file

@ -0,0 +1,11 @@
import { ResCountry } from "@point_of_sale/../tests/unit/data/res_country.data";
ResCountry._records = [
...ResCountry._records,
{
id: 75,
name: "France",
code: "FR",
vat_label: "VAT",
},
];

View file

@ -0,0 +1,13 @@
import { test, expect } from "@odoo/hoot";
import { setupPosEnv } from "@point_of_sale/../tests/unit/utils";
import { definePosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
definePosModels();
test("canEditPayment", async () => {
const store = await setupPosEnv();
store.addNewOrder();
const order = store.getOrder();
// In FR localisation, edit payment should not be visble even when order.nb_print === 0
expect(store.canEditPayment(order)).toBe(false);
});

View file

@ -1 +1,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_frontend
from . import test_string_to_hash
from . import test_fr_pos
from . import test_hash

View file

@ -0,0 +1,11 @@
from odoo.addons.account_edi.tests.common import AccountTestInvoicingCommon
from odoo.addons.point_of_sale.tests.test_generic_localization import TestGenericLocalization
from odoo.tests import tagged
@tagged('post_install', '-at_install', 'post_install_l10n')
class TestGenericFR(TestGenericLocalization):
@classmethod
@AccountTestInvoicingCommon.setup_country('fr')
def setUpClass(cls):
super().setUpClass()

View file

@ -0,0 +1,29 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.addons.point_of_sale.tests.test_frontend import TestPointOfSaleHttpCommon
class Testl10nFrPosCert(TestPointOfSaleHttpCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
company = cls.main_pos_config.company_id
company.country_id = cls.env.ref("base.fr")
company.point_of_sale_use_ticket_qr_code = True
company.point_of_sale_ticket_portal_url_display_mode = 'qr_code_and_url'
@tagged("post_install_l10n", "post_install", "-at_install")
class TestUi(Testl10nFrPosCert):
def test_pos_use_ticket_qr_code_for_fr(self):
company = self.main_pos_config.company_id
self.assertEqual(company.country_id.code, "FR", "Company should be set to France (FR)")
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_pos_tour("l10nFrPosCertSelfInvoicingTour", login="pos_user")
def test_correct_old_price_upon_price_change_fr(self):
company = self.main_pos_config.company_id
self.assertEqual(company.country_id.code, "FR", "Company should be set to France (FR)")
self.main_pos_config.with_user(self.pos_user).open_ui()
self.start_pos_tour("test_correct_old_price_upon_price_change_fr", login="pos_user")

View file

@ -0,0 +1,84 @@
from odoo.addons.point_of_sale.tests.common import CommonPosTest
from odoo.addons.account_edi.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
from odoo import fields
@tagged('post_install_l10n', 'post_install', '-at_install')
class TestHash(CommonPosTest):
@classmethod
@AccountTestInvoicingCommon.setup_country('fr')
def setUpClass(cls):
super().setUpClass()
def test_hashes_should_be_equal_if_no_alteration(self):
product1 = self.env['product.product'].create({
'name': 'product1',
})
self.pos_config_usd.open_ui()
pos_session = self.pos_config_usd.current_session_id
draft_order = {
'access_token': False,
'amount_paid': 0,
'amount_return': 0,
'amount_tax': 0,
'amount_total': 0,
'date_order': fields.Datetime.to_string(fields.Datetime.now()),
'lines': [],
'name': '/',
'partner_id': False,
'session_id': pos_session.id,
'sequence_number': 2,
'payment_ids': [],
'uuid': '12345-123-1234',
'last_order_preparation_change': '{}',
'user_id': self.env.uid,
'state': 'draft',
}
self.env['pos.order'].sync_from_ui([draft_order])
self.env.invalidate_all()
paid_order = {
'access_token': False,
'amount_paid': 20,
'amount_return': -5.0,
'amount_tax': 0,
'amount_total': 15.0,
'date_order': fields.Datetime.to_string(fields.Datetime.now()),
'lines': [[0,
0,
{'discount': 0,
'pack_lot_ids': [],
'price_unit': 15.0,
'product_id': product1.id,
'price_subtotal': 15.0,
'price_subtotal_incl': 15.0,
'qty': 1,
'tax_ids': []}]],
'name': 'Order 12345-123-1234',
'partner_id': False,
'session_id': pos_session.id,
'sequence_number': 2,
'payment_ids': [[0,
0,
{'amount': 20.0,
'name': fields.Datetime.now(),
'payment_method_id': self.cash_payment_method.id}]],
'uuid': '12345-123-1234',
'last_order_preparation_change': '{}',
'user_id': self.env.uid,
'state': 'paid',
}
self.env['pos.order'].sync_from_ui([paid_order])
self.env.invalidate_all()
posted_order = self.env['pos.order'].search([('uuid', '=', '12345-123-1234')])
self.assertEqual(posted_order.state, 'paid')
self.pos_config_usd.current_session_id.action_pos_session_closing_control()
self.assertEqual(posted_order.l10n_fr_hash, posted_order._compute_hash(''))

View file

@ -1,15 +1,15 @@
from odoo.addons.point_of_sale.tests.common import TestPointOfSaleCommon
from odoo.addons.point_of_sale.tests.common import TestPoSCommon
from odoo.tests import tagged
from ..models.pos import ORDER_FIELDS, LINE_FIELDS
from ..models.pos import ORDER_FIELDS_BEFORE_17_4, ORDER_FIELDS_FROM_17_4, LINE_FIELDS
from json import dumps
@tagged('post_install_l10n', 'post_install', '-at_install')
class TestStringToHash(TestPointOfSaleCommon):
class TestStringToHash(TestPoSCommon):
@classmethod
def setUpClass(cls, chart_template_ref="l10n_fr.l10n_fr_pcg_chart_template"):
super().setUpClass(chart_template_ref=chart_template_ref)
def setUpClass(cls):
super().setUpClass()
cls.pricelist = cls.env['product.pricelist'].create({
'name': 'Test Pricelist',
@ -28,7 +28,11 @@ class TestStringToHash(TestPointOfSaleCommon):
for order in orders:
values = {}
for field in ORDER_FIELDS:
if order.pos_version:
order_fields = ORDER_FIELDS_FROM_17_4
else:
order_fields = ORDER_FIELDS_BEFORE_17_4
for field in order_fields:
values[field] = _getattrstring(order, field)
for line in order.lines:
@ -74,7 +78,7 @@ class TestStringToHash(TestPointOfSaleCommon):
order = self.env['pos.order'].create({
'company_id': self.company_data['company'].id,
'partner_id': self.partner_a.id,
'session_id': self.pos_config.current_session_id.id,
'session_id': self.basic_config.current_session_id.id,
'lines': lines,
'amount_total': currency.round(total_amount + total_tax),
'amount_tax': currency.round(total_tax),
@ -96,15 +100,15 @@ class TestStringToHash(TestPointOfSaleCommon):
return order
def test_string_to_hash(self):
self.pos_config.open_ui()
self.basic_config.open_ui()
order = self._create_and_pay_pos_order([
{'qty': 1, 'price_unit': 10000, 'product': self.product_a, 'tax_ids': self.tax_sale_a},
{'qty': 2, 'price_unit': 5000, 'product': self.product_a, 'tax_ids': self.tax_sale_b},
{'qty': 3, 'price_unit': 2000, 'tax_ids': self.tax_sale_b | self.tax_sale_b}
], [
{'amount': 10000, 'payment_method': self.bank_payment_method},
{'amount': 1200, 'payment_method': self.cash_payment_method},
{'amount': 20000, 'payment_method': self.credit_payment_method}
{'amount': 10000, 'payment_method': self.bank_pm1},
{'amount': 8900, 'payment_method': self.cash_pm1},
{'amount': 11000, 'payment_method': self.pay_later_pm}
])
self.pos_config.current_session_id.action_pos_session_closing_control()
self.basic_config.current_session_id.action_pos_session_closing_control()
self.assertEqual(order.l10n_fr_string_to_hash, self._compute_string_to_hash_original(order))

View file

@ -1,12 +0,0 @@
def migrate(cr, version):
cr.execute("""
UPDATE ir_sequence iseq
SET implementation = 'no_gap'
FROM pos_config pconfig,res_company rcomp, res_country rcount, res_partner rpart
WHERE rcount.code in ('FR', 'MF', 'MQ', 'NC', 'PF', 'RE', 'GF', 'GP', 'TF', 'BL', 'PM', 'YT', 'WF')
AND rpart.country_id = rcount.id
AND rcomp.partner_id = rpart.id
AND pconfig.company_id = rcomp.id
AND (pconfig.sequence_id = iseq.id or pconfig.sequence_line_id = iseq.id)
AND iseq.implementation = 'standard'
""")

View file

@ -3,16 +3,16 @@
<field name="name">Sales Closings</field>
<field name="model">account.sale.closing</field>
<field name="arch" type="xml">
<tree create="false" import="false">
<list 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="currency_id" column_invisible="True"/>
<field name="frequency"/>
<field name="sequence_number" groups="base.group_no_one"/>
<field name="total_interval"/>
<field name="cumulative_total"/>
</tree>
</list>
</field>
</record>
@ -63,5 +63,5 @@
</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"/>
<menuitem action="action_list_view_account_sale_closing" id="menu_account_closing_reporting" parent="l10n_fr_account.account_reports_fr_statements_menu" sequence="90"/>
</odoo>

View file

@ -9,7 +9,6 @@
<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()

View file

@ -7,6 +7,9 @@
<xpath expr="//field[@name='pos_reference']" position='after'>
<field string='Hash' name="l10n_fr_hash" groups="base.group_no_one"/>
</xpath>
<field name="payment_ids" position="attributes">
<attribute name="readonly" add="country_code in ['FR', 'MF', 'MQ', 'NC', 'PF', 'RE', 'GF', 'GP', 'TF']" separator=" or " />
</field>
</field>
</record>
</odoo>

View file

@ -8,12 +8,6 @@
<form position="inside">
<field name="country_code" invisible="1"/>
</form>
<xpath expr="//field[@name='point_of_sale_use_ticket_qr_code']/.." position="attributes">
<attribute name="attrs">{'invisible': [('country_code', 'in', ('FR', 'MF', 'MQ', 'NC', 'PF', 'RE', 'GF', 'GP', 'TF'))]}</attribute>
</xpath>
<xpath expr="//label[@for='point_of_sale_use_ticket_qr_code']/.." position="attributes">
<attribute name="attrs">{'invisible': [('country_code', 'in', ('FR', 'MF', 'MQ', 'NC', 'PF', 'RE', 'GF', 'GP', 'TF'))]}</attribute>
</xpath>
</field>
</record>
</odoo>

View file

@ -1,13 +1,15 @@
[project]
name = "odoo-bringout-oca-ocb-l10n_fr_pos_cert"
version = "16.0.0"
description = "France - VAT Anti-Fraud Certification for Point of Sale (CGI 286 I-3 bis) - Odoo addon"
description = "France - VAT Anti-Fraud Certification for Point of Sale (CGI 286 I-3 bis) -
Odoo addon
"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-ocb-l10n_fr>=16.0.0",
"odoo-bringout-oca-ocb-point_of_sale>=16.0.0",
"TODO_MAP-l10n_fr_account>=19.0.0",
"odoo-bringout-oca-ocb-point_of_sale>=19.0.0",
"requests>=2.25.1"
]
readme = "README.md"
@ -17,7 +19,7 @@ classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]