Initial commit: L10N_Europe packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:52 +02:00
commit 9803722600
2377 changed files with 380711 additions and 0 deletions

View file

@ -0,0 +1,60 @@
# France - VAT Anti-Fraud Certification for Point of Sale (CGI 286 I-3 bis)
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)
## Installation
```bash
pip install odoo-bringout-oca-ocb-l10n_fr_pos_cert
```
## Dependencies
This addon depends on:
- l10n_fr
- 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`.
## 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

View file

@ -0,0 +1,32 @@
# Architecture
```mermaid
flowchart TD
U[Users] -->|HTTP| V[Views and QWeb Templates]
V --> C[Controllers]
V --> W[Wizards Transient Models]
C --> M[Models and ORM]
W --> M
M --> R[Reports]
DX[Data XML] --> M
S[Security ACLs and Groups] -. enforces .-> M
subgraph L10n_fr_pos_cert Module - l10n_fr_pos_cert
direction LR
M:::layer
W:::layer
C:::layer
V:::layer
R:::layer
S:::layer
DX:::layer
end
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
```
Notes
- Views include tree/form/kanban templates and report templates.
- Controllers provide website/portal routes when present.
- Wizards are UI flows implemented with `models.TransientModel`.
- Data XML loads data/demo records; Security defines groups and access.

View file

@ -0,0 +1,3 @@
# Configuration
Refer to Odoo settings for l10n_fr_pos_cert. Configure related models, access rights, and options as needed.

View file

@ -0,0 +1,3 @@
# Controllers
This module does not define custom HTTP controllers.

View file

@ -0,0 +1,6 @@
# Dependencies
This addon depends on:
- [l10n_fr](../../odoo-bringout-oca-ocb-l10n_fr)
- [point_of_sale](../../odoo-bringout-oca-ocb-point_of_sale)

View file

@ -0,0 +1,4 @@
# FAQ
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
- Q: How to enable? A: Start server with --addon l10n_fr_pos_cert or install in UI.

View file

@ -0,0 +1,7 @@
# Install
```bash
pip install odoo-bringout-oca-ocb-l10n_fr_pos_cert"
# or
uv pip install odoo-bringout-oca-ocb-l10n_fr_pos_cert"
```

View file

@ -0,0 +1,18 @@
# Models
Detected core models and extensions in l10n_fr_pos_cert.
```mermaid
classDiagram
class account_sale_closing
class account_fiscal_position
class pos_config
class pos_order
class pos_order_line
class pos_session
class res_company
```
Notes
- Classes show model technical names; fields omitted for brevity.
- Items listed under _inherit are extensions of existing models.

View file

@ -0,0 +1,6 @@
# Overview
Packaged Odoo addon: l10n_fr_pos_cert. Provides features documented in upstream Odoo 16 under this addon.
- Source: OCA/OCB 16.0, addon l10n_fr_pos_cert
- License: LGPL-3

View file

@ -0,0 +1,26 @@
# Reports
Report definitions and templates in l10n_fr_pos_cert.
```mermaid
classDiagram
class ReportPosHashIntegrity
AbstractModel <|-- ReportPosHashIntegrity
```
## Available Reports
No named reports found in XML files.
## Report Files
- **__init__.py** (Python logic)
- **pos_hash_integrity.py** (Python logic)
- **pos_hash_integrity.xml** (XML template/definition)
## Notes
- Named reports above are accessible through Odoo's reporting menu
- Python files define report logic and data processing
- XML files contain report templates, definitions, and formatting
- Reports are integrated with Odoo's printing and email systems

View file

@ -0,0 +1,41 @@
# Security
Access control and security definitions in l10n_fr_pos_cert.
## Access Control Lists (ACLs)
Model access permissions defined in:
- **[ir.model.access.csv](../l10n_fr_pos_cert/security/ir.model.access.csv)**
- 1 model access rules
## Record Rules
Row-level security rules defined in:
## Security Groups & Configuration
Security groups and permissions defined in:
- **[account_closing_intercompany.xml](../l10n_fr_pos_cert/security/account_closing_intercompany.xml)**
```mermaid
graph TB
subgraph "Security Layers"
A[Users] --> B[Groups]
B --> C[Access Control Lists]
C --> D[Models]
B --> E[Record Rules]
E --> F[Individual Records]
end
```
Security files overview:
- **[account_closing_intercompany.xml](../l10n_fr_pos_cert/security/account_closing_intercompany.xml)**
- Security groups, categories, and XML-based rules
- **[ir.model.access.csv](../l10n_fr_pos_cert/security/ir.model.access.csv)**
- Model access permissions (CRUD rights)
Notes
- Access Control Lists define which groups can access which models
- Record Rules provide row-level security (filter records by user/group)
- Security groups organize users and define permission sets
- All security is enforced at the ORM level by Odoo

View file

@ -0,0 +1,5 @@
# Troubleshooting
- Ensure Python and Odoo environment matches repo guidance.
- Check database connectivity and logs if startup fails.
- Validate that dependent addons listed in DEPENDENCIES.md are installed.

View file

@ -0,0 +1,7 @@
# Usage
Start Odoo including this addon (from repo root):
```bash
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon l10n_fr_pos_cert
```

View file

@ -0,0 +1,3 @@
# Wizards
This module does not include UI wizards.

View file

@ -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'])

View file

@ -0,0 +1,47 @@
# -*- 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)',
'icon': '/l10n_fr/static/description/icon.png',
'version': '1.1',
'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,
'data': [
'views/pos_views.xml',
'views/account_sale_closure.xml',
'views/pos_inalterability_menuitem.xml',
'views/res_config_settings_views.xml',
'report/pos_hash_integrity.xml',
'data/account_sale_closure_cron.xml',
'security/ir.model.access.csv',
'security/account_closing_intercompany.xml',
],
'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/**/*',
],
},
'license': 'LGPL-3',
}

View file

@ -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>

View file

@ -0,0 +1,590 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * l10n_fr_pos_cert
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.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"
"Last-Translator: Manon Rondou <ronm@odoo.com>\n"
"Language-Team: \n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#. 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
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."
#. 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."
msgstr ""
"Selon la loi française, vous ne pouvez pas modifier une ligne de commande de "
"caisse. 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. "
"Forbidden fields: %s."
msgstr ""
"Selon la loi française, vous ne pouvez pas modifier une commande de caisse. "
"Champs interdits : %s."
#. 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"
#. 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."
msgstr ""
"La comptabilité n'est pas inaltérable pour l'entreprise %s. Ce mécanisme est "
"conçu pour les entreprises dont la comptabilité est inaltérable."
#. 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 ""
"Une erreur s'est produite lors de la vérification de l'inaltérabilité. "
"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"
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:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__date_closing_stop
msgid "Closing Date"
msgstr "Date de clôture"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__frequency
msgid "Closing Type"
msgstr "Type de clôture"
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_res_company
msgid "Companies"
msgstr "Sociétés"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__company_id
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 "Contrôle des données du point de vente"
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."
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__create_uid
msgid "Created by"
msgstr "Créé par"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__create_date
msgid "Created on"
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é"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__currency_id
msgid "Currency"
msgstr "Devise"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields.selection,name:l10n_fr_pos_cert.selection__account_sale_closing__frequency__daily
msgid "Daily"
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"
#. 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"
msgstr "Date à partir de laquelle le total de l'intervalle est calculé"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,help:l10n_fr_pos_cert.field_account_sale_closing__date_closing_stop
msgid "Date to which the values are computed"
msgstr "Date jusqu'à laquelle les valeurs sont calculées"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__display_name
msgid "Display Name"
msgstr "Nom affiché"
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
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"
msgstr "Position fiscale"
#. module: l10n_fr_pos_cert
#: model:ir.ui.menu,name:l10n_fr_pos_cert.pos_fr_statements_menu
msgid "French Statements"
msgstr "Relevés français"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,help:l10n_fr_pos_cert.field_account_sale_closing__name
msgid "Frequency and unique sequence number"
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"
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_report_l10n_fr_pos_cert_report_pos_hash_integrity
msgid "Get french pos hash integrity result as PDF."
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.pos_order_form_inherit
msgid "Hash"
msgstr "Hachage"
#. module: l10n_fr_pos_cert
#: model:ir.actions.report,name:l10n_fr_pos_cert.action_report_pos_hash_integrity
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_sale_closing__id
msgid "ID"
msgstr "ID"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order__l10n_fr_hash
msgid "Inalteralbility Hash"
msgstr "Hachage d'inaltérabilité"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order__l10n_fr_secure_sequence_number
msgid "Inalteralbility No Gap Sequence #"
msgstr "Numéro de séquence sans écart pour inaltérabilité"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_res_company__l10n_fr_pos_cert_sequence_id
msgid "L10N Fr Pos Cert Sequence"
msgstr "Séquence pour la caisse certifiée française"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order__l10n_fr_string_to_hash
msgid "L10N Fr String To Hash"
msgstr "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."
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."
#. 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"
#. 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"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__last_order_hash
msgid "Last Order entry's inalteralbility hash"
msgstr "Hachage d'inaltérabilité sur la dernière commande"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__last_order_id
msgid "Last Pos Order"
msgstr "Dernière commande de caisse"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,help:l10n_fr_pos_cert.field_account_sale_closing__last_order_id
msgid "Last Pos order included in the grand total"
msgstr "Dernière commande de caisse incluse dans le grand total"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__write_uid
msgid "Last Updated by"
msgstr "Dernière mise à jour par"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__write_date
msgid "Last Updated on"
msgstr "Dernière mise à jour le"
#. module: l10n_fr_pos_cert
#. odoo-javascript
#: code:addons/l10n_fr_pos_cert/static/src/js/pos.js:0
#, python-format
msgid "Missing Country"
msgstr "Pays manquant"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields.selection,name:l10n_fr_pos_cert.selection__account_sale_closing__frequency__monthly
msgid "Monthly"
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"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__name
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
msgid "Old unit price:"
msgstr "Anciennement:"
#. module: l10n_fr_pos_cert
#: model:ir.actions.server,name:l10n_fr_pos_cert.action_check_pos_hash_integrity
#: model:ir.ui.menu,name:l10n_fr_pos_cert.menu_check_move_integrity_reporting
msgid "POS Inalterability Check"
msgstr "Vérification d'inaltérabilité de la caisse"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__total_interval
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"
msgstr "Paramétrage du point de vente"
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_pos_order_line
msgid "Point of Sale Order Lines"
msgstr "Lignes des commandes du point de vente"
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_pos_order
msgid "Point of Sale Orders"
msgstr "Commandes du point de vente"
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_pos_session
msgid "Point of Sale Session"
msgstr "Session 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 "Résultat du test d'intégrité -"
msgstr "Résultat du test dintégrité -"
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_account_sale_closing
msgid "Sale Closing"
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."
#. module: l10n_fr_pos_cert
#: model:ir.actions.act_window,name:l10n_fr_pos_cert.action_list_view_account_sale_closing
#: model:ir.ui.menu,name:l10n_fr_pos_cert.menu_account_closing
#: model:ir.ui.menu,name:l10n_fr_pos_cert.menu_account_closing_reporting
msgid "Sales Closings"
msgstr "Clôtures de ventes"
#. module: l10n_fr_pos_cert
#: model_terms:ir.actions.act_window,help:l10n_fr_pos_cert.action_list_view_account_sale_closing
msgid ""
"Sales closings run automatically on a daily, monthly and annual basis. It "
"computes both period and cumulative totals from all the sales entries posted "
"in the system after the previous closing."
msgstr ""
"Les clôtures de ventes sont exécutées automatiquement de manière "
"journalière, mensuelle et annuelle. Des totaux cumulatifs et par période "
"sont calculés à partir de toutes les entrées comptabilisées dans le système "
"depuis la clôture précédente."
#. 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"
" <br/>\n"
" <br/>\n"
" Ces conditions sont respectées via "
"une fonction de hachage des ventes du Point de Vente.\n"
" <br/>\n"
" <br/>"
msgstr ""
"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"
" <br/>\n"
" <br/>\n"
" Ces conditions sont respectées via "
"une fonction de hachage des ventes du Point de Vente.\n"
" <br/>\n"
" <br/>"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__sequence_number
msgid "Sequence #"
msgstr "Numéro de séquence"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__date_closing_start
msgid "Starting Date"
msgstr "Date de début"
#. module: l10n_fr_pos_cert
#: model_terms:ir.actions.act_window,help:l10n_fr_pos_cert.action_list_view_account_sale_closing
msgid "The closings are created by Odoo"
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
msgid "The company %s doesn't have a country set."
msgstr "La société %s n'a pas de pays configuré."
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,help:l10n_fr_pos_cert.field_account_sale_closing__currency_id
msgid "The company's currency"
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 "
"installation of the module France - Certification CGI 286 I-3 bis. - POS"
msgstr ""
"Il n'y a pas encore de commande marquée pour l'inaltérabilité des données "
"pour la société %s. Cette fonction n'est utilisée que pour les commandes "
"générées après l'installation du module France - Certification CGI 286 I-3 "
"bis. - POS"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,help:l10n_fr_pos_cert.field_account_sale_closing__total_interval
msgid ""
"Total in receivable accounts during the interval, excluding overlapping "
"periods"
msgstr ""
"Total sur des comptes débiteurs pendant l'intervalle, en excluant les "
"périodes qui se superposent"
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,help:l10n_fr_pos_cert.field_account_sale_closing__cumulative_total
msgid "Total in receivable accounts since the beginnig of times"
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."
msgstr ""
"Toutes les ventes effectuées via le Point de Vente\n"
" sont bien dans la chaîne de "
"hachage."
#. 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"
#. 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."
msgstr ""
"Vous ne pouvez pas modifer une position fiscale utilisée dans une commande "
"de caisse. Vous pouvez l'archiver et en créer une nouvelle."
#. 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."
msgstr ""
"Vous ne pouvez pas modifier les valeurs assurant de l'inaltérabilité de la "
"caisse."
#. 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

@ -0,0 +1,534 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * l10n_fr_pos_cert
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.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"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: l10n_fr_pos_cert
#. 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
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."
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."
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 ""
#. 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."
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"
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:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__date_closing_stop
msgid "Closing Date"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__frequency
msgid "Closing Type"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_res_company
msgid "Companies"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__company_id
msgid "Company"
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"
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 ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__create_uid
msgid "Created by"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__create_date
msgid "Created on"
msgstr ""
#. 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 ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__currency_id
msgid "Currency"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields.selection,name:l10n_fr_pos_cert.selection__account_sale_closing__frequency__daily
msgid "Daily"
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:ir.model.fields,help:l10n_fr_pos_cert.field_account_sale_closing__date_closing_start
msgid "Date from which the total interval is computed"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,help:l10n_fr_pos_cert.field_account_sale_closing__date_closing_stop
msgid "Date to which the values are computed"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__display_name
msgid "Display Name"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
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"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.ui.menu,name:l10n_fr_pos_cert.pos_fr_statements_menu
msgid "French Statements"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,help:l10n_fr_pos_cert.field_account_sale_closing__name
msgid "Frequency and unique sequence number"
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 ""
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_report_l10n_fr_pos_cert_report_pos_hash_integrity
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.pos_order_form_inherit
msgid "Hash"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.actions.report,name:l10n_fr_pos_cert.action_report_pos_hash_integrity
msgid "Hash integrity result PDF"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__id
msgid "ID"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order__l10n_fr_hash
msgid "Inalteralbility Hash"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order__l10n_fr_secure_sequence_number
msgid "Inalteralbility No Gap Sequence #"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_res_company__l10n_fr_pos_cert_sequence_id
msgid "L10N Fr Pos Cert Sequence"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_pos_order__l10n_fr_string_to_hash
msgid "L10N Fr String To Hash"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_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."
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.ui.view,arch_db:l10n_fr_pos_cert.report_pos_hash_integrity
msgid "Last Entry"
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"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__last_order_hash
msgid "Last Order entry's inalteralbility hash"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__last_order_id
msgid "Last Pos Order"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,help:l10n_fr_pos_cert.field_account_sale_closing__last_order_id
msgid "Last Pos order included in the grand total"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__write_uid
msgid "Last Updated by"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__write_date
msgid "Last Updated on"
msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-javascript
#: code:addons/l10n_fr_pos_cert/static/src/js/pos.js:0
#, python-format
msgid "Missing Country"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields.selection,name:l10n_fr_pos_cert.selection__account_sale_closing__frequency__monthly
msgid "Monthly"
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 ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__name
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
msgid "Old unit price:"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.actions.server,name:l10n_fr_pos_cert.action_check_pos_hash_integrity
#: model:ir.ui.menu,name:l10n_fr_pos_cert.menu_check_move_integrity_reporting
msgid "POS Inalterability Check"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__total_interval
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"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_pos_order_line
msgid "Point of Sale Order Lines"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_pos_order
msgid "Point of Sale Orders"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_pos_session
msgid "Point of Sale Session"
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é -"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model,name:l10n_fr_pos_cert.model_account_sale_closing
msgid "Sale Closing"
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."
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.actions.act_window,name:l10n_fr_pos_cert.action_list_view_account_sale_closing
#: model:ir.ui.menu,name:l10n_fr_pos_cert.menu_account_closing
#: model:ir.ui.menu,name:l10n_fr_pos_cert.menu_account_closing_reporting
msgid "Sales Closings"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.actions.act_window,help:l10n_fr_pos_cert.action_list_view_account_sale_closing
msgid ""
"Sales closings run automatically on a daily, monthly and annual basis. It "
"computes both period and cumulative totals from all the sales entries posted"
" in the system after the previous closing."
msgstr ""
#. module: l10n_fr_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"
" <br/>\n"
" <br/>\n"
" Ces conditions sont respectées via une fonction de hachage des ventes du Point de Vente.\n"
" <br/>\n"
" <br/>"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__sequence_number
msgid "Sequence #"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,field_description:l10n_fr_pos_cert.field_account_sale_closing__date_closing_start
msgid "Starting Date"
msgstr ""
#. module: l10n_fr_pos_cert
#: model_terms:ir.actions.act_window,help:l10n_fr_pos_cert.action_list_view_account_sale_closing
msgid "The closings are created by Odoo"
msgstr ""
#. module: l10n_fr_pos_cert
#. odoo-javascript
#: code:addons/l10n_fr_pos_cert/static/src/js/pos.js:0
#, python-format
msgid "The company %s doesn't have a country set."
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,help:l10n_fr_pos_cert.field_account_sale_closing__currency_id
msgid "The company's currency"
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 "
"installation of the module France - Certification CGI 286 I-3 bis. - POS"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,help:l10n_fr_pos_cert.field_account_sale_closing__total_interval
msgid ""
"Total in receivable accounts during the interval, excluding overlapping "
"periods"
msgstr ""
#. module: l10n_fr_pos_cert
#: model:ir.model.fields,help:l10n_fr_pos_cert.field_account_sale_closing__cumulative_total
msgid "Total in receivable accounts since the beginnig of times"
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."
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"
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."
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."
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

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_fiscal_position
from . import res_company
from . import pos
from . import account_closing

View file

@ -0,0 +1,166 @@
# -*- 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 dateutil.relativedelta import relativedelta
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_move m ON m.id = aml.move_id
WHERE j.type = 'sale'
AND aml.company_id = %(company_id)s
AND m.state = 'posted'
AND acc.account_type = 'asset_receivable' '''
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':
interval_from = date_stop - relativedelta(months=1)
name_interval = _('Monthly Closing')
elif frequency == 'annually':
interval_from = date_stop - relativedelta(years=1)
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.'))
@api.ondelete(at_uninstall=True)
def _unlink_never(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

View file

@ -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)

View file

@ -0,0 +1,200 @@
# -*- 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 collections import defaultdict
class pos_config(models.Model):
_inherit = 'pos.config'
def open_ui(self):
for config in self:
if not config.company_id.country_id:
raise UserError(_("You have to set a country in your company setting."))
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()
class pos_session(models.Model):
_inherit = 'pos.session'
def _check_session_timing(self):
self.ensure_one()
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 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'')
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(field_value, field_type, model_name=None):
if field_type in ('many2many', 'one2many'):
if field_value:
sorted_ids = sorted_relational_ids.get(model_name, [])
value_set = set(field_value)
field_value = [id for id in sorted_ids if id in value_set]
else:
field_value = []
return str(field_value)
def collect_sorted_relational_ids(orders_data, lines_data, order_field_defs, line_field_defs):
relational_ids = defaultdict(set)
for data_list, field_names, field_defs in (
(orders_data, ORDER_FIELDS, order_field_defs),
(lines_data, LINE_FIELDS, line_field_defs),
):
for record in data_list:
for field in field_names:
field_def = field_defs.get(field)
if field_def and field_def['type'] in ('many2many', 'one2many'):
ids = record.get(field) or []
relational_ids[field_def['comodel']].update(ids)
sorted_relational_ids = {}
for model_name, ids in relational_ids.items():
if ids:
# Use search() to get IDs sorted by _order the same way Odoo ORM does for relational fields
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='')
lines_data = self.lines.read(LINE_FIELDS + ['id', 'order_id'], load='')
orders_by_id = {order['id']: order for order in orders_data}
lines_by_order = defaultdict(list)
for line in lines_data:
lines_by_order[line['order_id']].append(line)
order_field_defs = {
field: {
'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
}
line_field_defs = {
field: {
'type': self.lines._fields[field].type,
'comodel': self.lines._fields[field].comodel_name if hasattr(self.lines._fields[field], 'comodel_name') else None
}
for field in LINE_FIELDS
}
sorted_relational_ids = collect_sorted_relational_ids(orders_data, lines_data, order_field_defs, line_field_defs)
for order in self:
values = {}
order_data = orders_by_id[order.id]
for field in ORDER_FIELDS:
field_def = order_field_defs[field]
values[field] = _getattrstring(order_data.get(field), field_def['type'], field_def['comodel'])
for line in lines_by_order[order.id]:
for field in LINE_FIELDS:
k = 'line_%d_%s' % (line['id'], field)
field_def = line_field_defs[field]
values[k] = _getattrstring(line.get(field), field_def['type'], field_def['comodel'])
#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
@api.ondelete(at_uninstall=True)
def _unlink_except_pos_so(self):
for order in self:
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"
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)

View file

@ -0,0 +1,99 @@
# -*- 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 or 'UTC')
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_create_multi
def create(self, vals_list):
companies = super().create(vals_list)
for company in companies:
#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 companies
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')]
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),
('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))
raise UserError(msg_alert)
previous_hash = u''
corrupted_orders = []
for order in orders:
if order.l10n_fr_hash != order._compute_hash(previous_hash=previous_hash):
corrupted_orders.append(order.name)
msg_alert = (_('Corrupted data on point of sale order with id %s.', order.id))
previous_hash = order.l10n_fr_hash
orders.invalidate_recordset()
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],
})
corrupted_orders = ', '.join([o for o in corrupted_orders])
return {
'result': report_dict or 'None',
'msg_alert': msg_alert or 'None',
'printing_date': format_date(self.env, Date.to_string( Date.today())),
'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)

View file

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import pos_hash_integrity

View file

@ -0,0 +1,20 @@
# -*- 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):
data = data or {}
data.update(self.env.company._check_pos_hash_integrity() or {})
return {
'doc_ids' : docids,
'doc_model' : self.env['res.company'],
'data' : data,
'docs' : self.env['res.company'].browse(self.env.company.id),
}

View file

@ -0,0 +1,103 @@
<?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 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.
<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['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>
</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>
<t t-if="data['corrupted_orders'] != 'None'">
<h5>
Données corrompues sur la commande du point de vente:
</h5>
<span t-esc="data['corrupted_orders']"/> <br/>
</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>
</t>
</t>
</t>
</template>
</data>
</odoo>

View file

@ -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>

View file

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 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

View file

@ -0,0 +1,3 @@
.oldPrice {
font-weight: bold;
}

View file

@ -0,0 +1,41 @@
odoo.define('l10n_fr_pos_cert.Chrome', function (require) {
'use strict';
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.'),
});
}
}
}
}
}
};
Registries.Component.extend(Chrome, PosFrCertChrome);
return Chrome;
});

View file

@ -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;
});

View file

@ -0,0 +1,27 @@
odoo.define('l10n_fr_pos_cert.ClosePosPopup', function (require) {
'use strict';
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;
});

View file

@ -0,0 +1,15 @@
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

@ -0,0 +1,85 @@
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

@ -0,0 +1,26 @@
<?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>
<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">
<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
</s>
</span>
</div>
</t>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,19 @@
<?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 @@
from . import test_string_to_hash

View file

@ -0,0 +1,110 @@
from odoo.addons.point_of_sale.tests.common import TestPointOfSaleCommon
from odoo.tests import tagged
from ..models.pos import ORDER_FIELDS, LINE_FIELDS
from json import dumps
@tagged('post_install_l10n', 'post_install', '-at_install')
class TestStringToHash(TestPointOfSaleCommon):
@classmethod
def setUpClass(cls, chart_template_ref="l10n_fr.l10n_fr_pcg_chart_template"):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.pricelist = cls.env['product.pricelist'].create({
'name': 'Test Pricelist',
'currency_id': cls.company_data['company'].currency_id.id,
})
cls.company.country_id = cls.env.company.account_fiscal_country_id.id
def _compute_string_to_hash_original(self, orders):
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 orders:
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)
return dumps(values, sort_keys=True,
ensure_ascii=True, indent=None,
separators=(',', ':'))
def _create_and_pay_pos_order(self, line_data_list, payments):
currency = self.company_data['company'].currency_id
lines = []
total_tax = 0.0
total_amount = 0.0
for idx, line_data in enumerate(line_data_list):
product = line_data.get('product', self.product_a)
qty = line_data['qty']
price_unit = line_data['price_unit']
taxes = line_data.get('tax_ids', self.tax_sale_a)
line_tax = sum((tax.amount / 100) * qty * price_unit for tax in taxes)
line_total = qty * price_unit + line_tax
total_tax += line_tax
total_amount += qty * price_unit
rounded_total = currency.round(line_total)
lines.append((0, 0, {
'name': f"OL/000{idx + 1}",
'product_id': product.id,
'price_unit': price_unit,
'qty': qty,
'tax_ids': [(6, 0, taxes.ids)],
'price_subtotal': qty * price_unit,
'price_subtotal_incl': rounded_total,
}))
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,
'lines': lines,
'amount_total': currency.round(total_amount + total_tax),
'amount_tax': currency.round(total_tax),
'amount_paid': 0,
'amount_return': 0,
'pricelist_id': self.pricelist.id
})
for payment in payments:
context_payment = {
"active_ids": [order.id],
"active_id": order.id
}
pos_make_payment = self.env['pos.make.payment'].with_context(context_payment).create({
'amount': payment['amount'],
'payment_method_id': payment['payment_method'].id,
})
pos_make_payment.with_context(context_payment).check()
return order
def test_string_to_hash(self):
self.pos_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}
])
self.pos_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

@ -0,0 +1,12 @@
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

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.l10n_fr_pos_cert</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="point_of_sale.res_config_settings_view_form" />
<field name="arch" type="xml">
<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

@ -0,0 +1,43 @@
[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"
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",
"requests>=2.25.1"
]
readme = "README.md"
requires-python = ">= 3.11"
classifiers = [
"Development Status :: 5 - Production/Stable",
"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.12",
"Topic :: Office/Business",
]
[project.urls]
homepage = "https://github.com/bringout/0"
repository = "https://github.com/bringout/0"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["l10n_fr_pos_cert"]
[tool.rye]
managed = true
dev-dependencies = [
"pytest>=8.4.1",
]