factpulse 2.0.37__py3-none-any.whl → 3.0.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of factpulse might be problematic. Click here for more details.

Files changed (262) hide show
  1. factpulse/__init__.py +265 -197
  2. factpulse/api/__init__.py +5 -4
  3. factpulse/api/afnorpdppa_api.py +34 -34
  4. factpulse/api/afnorpdppa_directory_service_api.py +59 -59
  5. factpulse/api/afnorpdppa_flow_service_api.py +23 -23
  6. factpulse/api/chorus_pro_api.py +211 -211
  7. factpulse/api/document_conversion_api.py +1506 -0
  8. factpulse/api/{sant_api.py → health_api.py} +22 -22
  9. factpulse/api/invoice_processing_api.py +3437 -0
  10. factpulse/api/{vrification_pdfxml_api.py → pdfxml_verification_api.py} +240 -240
  11. factpulse/api/{utilisateur_api.py → user_api.py} +17 -17
  12. factpulse/api_client.py +3 -3
  13. factpulse/configuration.py +3 -3
  14. factpulse/exceptions.py +2 -2
  15. factpulse/models/__init__.py +128 -95
  16. factpulse/models/acknowledgment_status.py +38 -0
  17. factpulse/models/additional_document.py +115 -0
  18. factpulse/models/afnor_credentials.py +106 -0
  19. factpulse/models/afnor_destination.py +127 -0
  20. factpulse/models/afnor_health_check_response.py +91 -0
  21. factpulse/models/afnor_result.py +105 -0
  22. factpulse/models/allowance_charge.py +147 -0
  23. factpulse/models/allowance_reason_code.py +42 -0
  24. factpulse/models/allowance_total_amount.py +145 -0
  25. factpulse/models/amount.py +139 -0
  26. factpulse/models/amount_due.py +139 -0
  27. factpulse/models/api_error.py +5 -5
  28. factpulse/models/async_task_status.py +97 -0
  29. factpulse/models/base_amount.py +145 -0
  30. factpulse/models/bounding_box_schema.py +10 -10
  31. factpulse/models/celery_status.py +40 -0
  32. factpulse/models/certificate_info_response.py +24 -24
  33. factpulse/models/charge_total_amount.py +145 -0
  34. factpulse/models/chorus_pro_destination.py +108 -0
  35. factpulse/models/chorus_pro_result.py +101 -0
  36. factpulse/models/contact.py +113 -0
  37. factpulse/models/convert_error_response.py +105 -0
  38. factpulse/models/convert_pending_input_response.py +114 -0
  39. factpulse/models/convert_resume_request.py +87 -0
  40. factpulse/models/convert_success_response.py +126 -0
  41. factpulse/models/convert_validation_failed_response.py +120 -0
  42. factpulse/models/delivery_party.py +121 -0
  43. factpulse/models/destination.py +27 -27
  44. factpulse/models/document_type_info.py +91 -0
  45. factpulse/models/electronic_address.py +90 -0
  46. factpulse/models/enriched_invoice_info.py +133 -0
  47. factpulse/models/error_level.py +2 -2
  48. factpulse/models/error_source.py +2 -2
  49. factpulse/models/extraction_info.py +93 -0
  50. factpulse/models/factur_x_invoice.py +320 -0
  51. factpulse/models/factur_x_profile.py +39 -0
  52. factpulse/models/factur_xpdf_info.py +91 -0
  53. factpulse/models/facture_electronique_rest_api_schemas_chorus_pro_chorus_pro_credentials.py +95 -0
  54. factpulse/models/facture_electronique_rest_api_schemas_processing_chorus_pro_credentials.py +115 -0
  55. factpulse/models/field_status.py +40 -0
  56. factpulse/models/file_info.py +94 -0
  57. factpulse/models/files_info.py +106 -0
  58. factpulse/models/flow_direction.py +37 -0
  59. factpulse/models/flow_profile.py +38 -0
  60. factpulse/models/flow_summary.py +131 -0
  61. factpulse/models/flow_syntax.py +40 -0
  62. factpulse/models/flow_type.py +40 -0
  63. factpulse/models/generate_certificate_request.py +26 -26
  64. factpulse/models/generate_certificate_response.py +15 -15
  65. factpulse/models/get_chorus_pro_id_request.py +100 -0
  66. factpulse/models/get_chorus_pro_id_response.py +98 -0
  67. factpulse/models/get_invoice_request.py +98 -0
  68. factpulse/models/get_invoice_response.py +142 -0
  69. factpulse/models/get_structure_request.py +100 -0
  70. factpulse/models/get_structure_response.py +142 -0
  71. factpulse/models/global_allowance_amount.py +139 -0
  72. factpulse/models/gross_unit_price.py +145 -0
  73. factpulse/models/http_validation_error.py +2 -2
  74. factpulse/models/incoming_invoice.py +196 -0
  75. factpulse/models/incoming_supplier.py +144 -0
  76. factpulse/models/invoice_format.py +38 -0
  77. factpulse/models/invoice_line.py +354 -0
  78. factpulse/models/invoice_line_allowance_amount.py +145 -0
  79. factpulse/models/invoice_note.py +94 -0
  80. factpulse/models/invoice_references.py +194 -0
  81. factpulse/models/invoice_status.py +96 -0
  82. factpulse/models/invoice_totals.py +177 -0
  83. factpulse/models/invoice_totals_prepayment.py +145 -0
  84. factpulse/models/invoice_type_code.py +51 -0
  85. factpulse/models/invoicing_framework.py +110 -0
  86. factpulse/models/invoicing_framework_code.py +39 -0
  87. factpulse/models/line_net_amount.py +145 -0
  88. factpulse/models/line_total_amount.py +145 -0
  89. factpulse/models/mandatory_note_schema.py +124 -0
  90. factpulse/models/manual_rate.py +139 -0
  91. factpulse/models/manual_vat_rate.py +139 -0
  92. factpulse/models/missing_field.py +107 -0
  93. factpulse/models/operation_nature.py +49 -0
  94. factpulse/models/output_format.py +37 -0
  95. factpulse/models/page_dimensions_schema.py +89 -0
  96. factpulse/models/payee.py +168 -0
  97. factpulse/models/payment_card.py +99 -0
  98. factpulse/models/payment_means.py +41 -0
  99. factpulse/models/pdf_validation_result_api.py +169 -0
  100. factpulse/models/pdp_credentials.py +15 -15
  101. factpulse/models/percentage.py +145 -0
  102. factpulse/models/postal_address.py +134 -0
  103. factpulse/models/price_allowance_amount.py +145 -0
  104. factpulse/models/price_basis_quantity.py +145 -0
  105. factpulse/models/processing_options.py +94 -0
  106. factpulse/models/product_characteristic.py +89 -0
  107. factpulse/models/product_classification.py +101 -0
  108. factpulse/models/quantity.py +139 -0
  109. factpulse/models/recipient.py +167 -0
  110. factpulse/models/rounding_amount.py +145 -0
  111. factpulse/models/scheme_id.py +10 -4
  112. factpulse/models/search_flow_request.py +143 -0
  113. factpulse/models/search_flow_response.py +101 -0
  114. factpulse/models/search_services_response.py +101 -0
  115. factpulse/models/search_structure_request.py +119 -0
  116. factpulse/models/search_structure_response.py +101 -0
  117. factpulse/models/signature_info.py +6 -6
  118. factpulse/models/signature_info_api.py +122 -0
  119. factpulse/models/signature_parameters.py +133 -0
  120. factpulse/models/simplified_invoice_data.py +124 -0
  121. factpulse/models/structure_info.py +14 -14
  122. factpulse/models/structure_parameters.py +91 -0
  123. factpulse/models/structure_service.py +93 -0
  124. factpulse/models/submission_mode.py +38 -0
  125. factpulse/models/submit_complete_invoice_request.py +116 -0
  126. factpulse/models/submit_complete_invoice_response.py +145 -0
  127. factpulse/models/submit_flow_request.py +123 -0
  128. factpulse/models/submit_flow_response.py +109 -0
  129. factpulse/models/submit_gross_amount.py +139 -0
  130. factpulse/models/submit_invoice_request.py +176 -0
  131. factpulse/models/submit_invoice_response.py +103 -0
  132. factpulse/models/submit_net_amount.py +139 -0
  133. factpulse/models/submit_vat_amount.py +139 -0
  134. factpulse/models/supplementary_attachment.py +95 -0
  135. factpulse/models/supplier.py +225 -0
  136. factpulse/models/task_response.py +87 -0
  137. factpulse/models/tax_representative.py +95 -0
  138. factpulse/models/taxable_amount.py +139 -0
  139. factpulse/models/total_gross_amount.py +139 -0
  140. factpulse/models/total_net_amount.py +139 -0
  141. factpulse/models/total_vat_amount.py +139 -0
  142. factpulse/models/unit_net_price.py +139 -0
  143. factpulse/models/unit_of_measure.py +41 -0
  144. factpulse/models/validation_error.py +2 -2
  145. factpulse/models/validation_error_detail.py +6 -6
  146. factpulse/models/validation_error_loc_inner.py +2 -2
  147. factpulse/models/validation_error_response.py +87 -0
  148. factpulse/models/validation_info.py +105 -0
  149. factpulse/models/validation_success_response.py +87 -0
  150. factpulse/models/vat_accounting_code.py +39 -0
  151. factpulse/models/vat_amount.py +139 -0
  152. factpulse/models/vat_category.py +44 -0
  153. factpulse/models/vat_line.py +140 -0
  154. factpulse/models/vat_point_date_code.py +38 -0
  155. factpulse/models/vat_rate.py +145 -0
  156. factpulse/models/verification_success_response.py +135 -0
  157. factpulse/models/verified_field_schema.py +129 -0
  158. factpulse/rest.py +2 -2
  159. factpulse-3.0.7.dist-info/METADATA +292 -0
  160. factpulse-3.0.7.dist-info/RECORD +168 -0
  161. factpulse_helpers/__init__.py +34 -34
  162. factpulse_helpers/client.py +1019 -795
  163. factpulse_helpers/exceptions.py +68 -68
  164. factpulse/api/traitement_facture_api.py +0 -3437
  165. factpulse/models/adresse_electronique.py +0 -90
  166. factpulse/models/adresse_postale.py +0 -120
  167. factpulse/models/cadre_de_facturation.py +0 -110
  168. factpulse/models/categorie_tva.py +0 -44
  169. factpulse/models/champ_verifie_schema.py +0 -129
  170. factpulse/models/chorus_pro_credentials.py +0 -95
  171. factpulse/models/code_cadre_facturation.py +0 -39
  172. factpulse/models/code_raison_reduction.py +0 -42
  173. factpulse/models/consulter_facture_request.py +0 -98
  174. factpulse/models/consulter_facture_response.py +0 -142
  175. factpulse/models/consulter_structure_request.py +0 -100
  176. factpulse/models/consulter_structure_response.py +0 -142
  177. factpulse/models/credentials_afnor.py +0 -106
  178. factpulse/models/credentials_chorus_pro.py +0 -115
  179. factpulse/models/destinataire.py +0 -130
  180. factpulse/models/destination_afnor.py +0 -127
  181. factpulse/models/destination_chorus_pro.py +0 -108
  182. factpulse/models/dimension_page_schema.py +0 -89
  183. factpulse/models/direction_flux.py +0 -37
  184. factpulse/models/donnees_facture_simplifiees.py +0 -124
  185. factpulse/models/facture_enrichie_info.py +0 -133
  186. factpulse/models/facture_entrante.py +0 -196
  187. factpulse/models/facture_factur_x.py +0 -183
  188. factpulse/models/flux_resume.py +0 -131
  189. factpulse/models/format_facture.py +0 -38
  190. factpulse/models/format_sortie.py +0 -37
  191. factpulse/models/fournisseur.py +0 -153
  192. factpulse/models/fournisseur_entrant.py +0 -144
  193. factpulse/models/information_signature_api.py +0 -122
  194. factpulse/models/ligne_de_poste.py +0 -183
  195. factpulse/models/ligne_de_poste_montant_remise_ht.py +0 -145
  196. factpulse/models/ligne_de_poste_taux_tva_manuel.py +0 -145
  197. factpulse/models/ligne_de_tva.py +0 -132
  198. factpulse/models/mode_depot.py +0 -38
  199. factpulse/models/mode_paiement.py +0 -41
  200. factpulse/models/montant_a_payer.py +0 -139
  201. factpulse/models/montant_base_ht.py +0 -139
  202. factpulse/models/montant_ht_total.py +0 -139
  203. factpulse/models/montant_remise_globale_ttc.py +0 -139
  204. factpulse/models/montant_total.py +0 -133
  205. factpulse/models/montant_total_acompte.py +0 -145
  206. factpulse/models/montant_total_ligne_ht.py +0 -139
  207. factpulse/models/montant_ttc_total.py +0 -139
  208. factpulse/models/montant_tva.py +0 -139
  209. factpulse/models/montant_tva_ligne.py +0 -139
  210. factpulse/models/montant_tva_total.py +0 -139
  211. factpulse/models/montant_unitaire_ht.py +0 -139
  212. factpulse/models/nature_operation.py +0 -49
  213. factpulse/models/note.py +0 -94
  214. factpulse/models/note_obligatoire_schema.py +0 -124
  215. factpulse/models/obtenir_id_chorus_pro_request.py +0 -100
  216. factpulse/models/obtenir_id_chorus_pro_response.py +0 -98
  217. factpulse/models/options_processing.py +0 -94
  218. factpulse/models/parametres_signature.py +0 -133
  219. factpulse/models/parametres_structure.py +0 -91
  220. factpulse/models/pdf_factur_x_info.py +0 -91
  221. factpulse/models/piece_jointe_complementaire.py +0 -95
  222. factpulse/models/profil_api.py +0 -39
  223. factpulse/models/profil_flux.py +0 -38
  224. factpulse/models/quantite.py +0 -139
  225. factpulse/models/rechercher_services_response.py +0 -101
  226. factpulse/models/rechercher_structure_request.py +0 -119
  227. factpulse/models/rechercher_structure_response.py +0 -101
  228. factpulse/models/references.py +0 -124
  229. factpulse/models/reponse_healthcheck_afnor.py +0 -91
  230. factpulse/models/reponse_recherche_flux.py +0 -101
  231. factpulse/models/reponse_soumission_flux.py +0 -109
  232. factpulse/models/reponse_tache.py +0 -87
  233. factpulse/models/reponse_validation_erreur.py +0 -87
  234. factpulse/models/reponse_validation_succes.py +0 -87
  235. factpulse/models/reponse_verification_succes.py +0 -135
  236. factpulse/models/requete_recherche_flux.py +0 -143
  237. factpulse/models/requete_soumission_flux.py +0 -123
  238. factpulse/models/resultat_afnor.py +0 -105
  239. factpulse/models/resultat_chorus_pro.py +0 -101
  240. factpulse/models/resultat_validation_pdfapi.py +0 -169
  241. factpulse/models/service_structure.py +0 -93
  242. factpulse/models/soumettre_facture_complete_request.py +0 -116
  243. factpulse/models/soumettre_facture_complete_response.py +0 -145
  244. factpulse/models/soumettre_facture_request.py +0 -176
  245. factpulse/models/soumettre_facture_response.py +0 -103
  246. factpulse/models/statut_acquittement.py +0 -38
  247. factpulse/models/statut_celery.py +0 -40
  248. factpulse/models/statut_champ_api.py +0 -40
  249. factpulse/models/statut_facture.py +0 -96
  250. factpulse/models/statut_tache.py +0 -97
  251. factpulse/models/syntaxe_flux.py +0 -40
  252. factpulse/models/tauxmanuel.py +0 -139
  253. factpulse/models/type_document.py +0 -40
  254. factpulse/models/type_facture.py +0 -37
  255. factpulse/models/type_flux.py +0 -40
  256. factpulse/models/type_tva.py +0 -39
  257. factpulse/models/unite.py +0 -41
  258. factpulse-2.0.37.dist-info/METADATA +0 -292
  259. factpulse-2.0.37.dist-info/RECORD +0 -134
  260. {factpulse-2.0.37.dist-info → factpulse-3.0.7.dist-info}/WHEEL +0 -0
  261. {factpulse-2.0.37.dist-info → factpulse-3.0.7.dist-info}/licenses/LICENSE +0 -0
  262. {factpulse-2.0.37.dist-info → factpulse-3.0.7.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- """Client simplifié pour l'API FactPulse avec authentification JWT et polling intégrés."""
1
+ """Simplified client for the FactPulse API with built-in JWT authentication and polling."""
2
2
  import base64
3
3
  import json
4
4
  import logging
@@ -12,7 +12,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union
12
12
  import requests
13
13
 
14
14
  import factpulse
15
- from factpulse import ApiClient, Configuration, TraitementFactureApi
15
+ from factpulse import ApiClient, Configuration, InvoiceProcessingApi
16
16
 
17
17
  from .exceptions import (
18
18
  FactPulseAuthError,
@@ -25,39 +25,39 @@ logger = logging.getLogger(__name__)
25
25
 
26
26
 
27
27
  # =============================================================================
28
- # JSON Encoder pour Decimal et autres types non sérialisables
28
+ # JSON Encoder for Decimal and other non-serializable types
29
29
  # =============================================================================
30
30
 
31
31
  class DecimalEncoder(json.JSONEncoder):
32
- """Encoder JSON personnalisé qui gère les Decimal et autres types Python."""
32
+ """Custom JSON encoder that handles Decimal and other Python types."""
33
33
 
34
34
  def default(self, obj):
35
35
  if isinstance(obj, Decimal):
36
- # Convertir en string pour préserver la précision monétaire
36
+ # Convert to string to preserve monetary precision
37
37
  return str(obj)
38
38
  if hasattr(obj, "isoformat"):
39
39
  # datetime, date, time
40
40
  return obj.isoformat()
41
41
  if hasattr(obj, "to_dict"):
42
- # Modèles Pydantic ou dataclasses avec to_dict
42
+ # Pydantic models or dataclasses with to_dict
43
43
  return obj.to_dict()
44
44
  return super().default(obj)
45
45
 
46
46
 
47
47
  def json_dumps_safe(data: Any, **kwargs) -> str:
48
- """Sérialise en JSON en gérant les Decimal et autres types Python.
48
+ """Serialize to JSON handling Decimal and other Python types.
49
49
 
50
50
  Args:
51
- data: Données à sérialiser (dict, list, etc.)
52
- **kwargs: Arguments supplémentaires pour json.dumps
51
+ data: Data to serialize (dict, list, etc.)
52
+ **kwargs: Additional arguments for json.dumps
53
53
 
54
54
  Returns:
55
- String JSON
55
+ JSON string
56
56
 
57
57
  Example:
58
58
  >>> from decimal import Decimal
59
- >>> json_dumps_safe({"montant": Decimal("1234.56")})
60
- '{"montant": "1234.56"}'
59
+ >>> json_dumps_safe({"amount": Decimal("1234.56")})
60
+ '{"amount": "1234.56"}'
61
61
  """
62
62
  kwargs.setdefault("ensure_ascii", False)
63
63
  kwargs.setdefault("cls", DecimalEncoder)
@@ -65,21 +65,21 @@ def json_dumps_safe(data: Any, **kwargs) -> str:
65
65
 
66
66
 
67
67
  # =============================================================================
68
- # Credentials dataclasses - pour une configuration simplifiée
68
+ # Credentials dataclasses - for simplified configuration
69
69
  # =============================================================================
70
70
 
71
71
  @dataclass
72
72
  class ChorusProCredentials:
73
- """Credentials Chorus Pro pour le mode Zero-Trust.
73
+ """Chorus Pro credentials for Zero-Trust mode.
74
74
 
75
- Ces credentials sont passés dans chaque requête et ne sont jamais stockés côté serveur.
75
+ These credentials are passed in each request and never stored server-side.
76
76
 
77
77
  Attributes:
78
- piste_client_id: Client ID PISTE (portail API gouvernement)
79
- piste_client_secret: Client Secret PISTE
80
- chorus_pro_login: Login Chorus Pro
81
- chorus_pro_password: Mot de passe Chorus Pro
82
- sandbox: True pour l'environnement sandbox, False pour production
78
+ piste_client_id: PISTE Client ID (government API portal)
79
+ piste_client_secret: PISTE Client Secret
80
+ chorus_pro_login: Chorus Pro login
81
+ chorus_pro_password: Chorus Pro password
82
+ sandbox: True for sandbox environment, False for production
83
83
  """
84
84
  piste_client_id: str
85
85
  piste_client_secret: str
@@ -88,7 +88,7 @@ class ChorusProCredentials:
88
88
  sandbox: bool = True
89
89
 
90
90
  def to_dict(self) -> Dict[str, Any]:
91
- """Convertit en dictionnaire pour l'API."""
91
+ """Convert to dictionary for API."""
92
92
  return {
93
93
  "piste_client_id": self.piste_client_id,
94
94
  "piste_client_secret": self.piste_client_secret,
@@ -100,18 +100,18 @@ class ChorusProCredentials:
100
100
 
101
101
  @dataclass
102
102
  class AFNORCredentials:
103
- """Credentials AFNOR PDP pour le mode Zero-Trust.
103
+ """AFNOR PDP credentials for Zero-Trust mode.
104
104
 
105
- Ces credentials sont passés dans chaque requête et ne sont jamais stockés côté serveur.
106
- L'API FactPulse utilise ces credentials pour s'authentifier auprès de la PDP AFNOR
107
- et obtenir un token OAuth2 spécifique.
105
+ These credentials are passed in each request and never stored server-side.
106
+ The FactPulse API uses these credentials to authenticate with the AFNOR PDP
107
+ and obtain a specific OAuth2 token.
108
108
 
109
109
  Attributes:
110
- flow_service_url: URL du Flow Service de la PDP (ex: https://api.pdp.fr/flow/v1)
111
- token_url: URL du serveur OAuth2 de la PDP (ex: https://auth.pdp.fr/oauth/token)
112
- client_id: Client ID OAuth2 de la PDP
113
- client_secret: Client Secret OAuth2 de la PDP
114
- directory_service_url: URL du Directory Service (optionnel, déduit de flow_service_url)
110
+ flow_service_url: PDP Flow Service URL (e.g., https://api.pdp.fr/flow/v1)
111
+ token_url: PDP OAuth2 server URL (e.g., https://auth.pdp.fr/oauth/token)
112
+ client_id: PDP OAuth2 Client ID
113
+ client_secret: PDP OAuth2 Client Secret
114
+ directory_service_url: Directory Service URL (optional, derived from flow_service_url)
115
115
  """
116
116
  flow_service_url: str
117
117
  token_url: str
@@ -120,7 +120,7 @@ class AFNORCredentials:
120
120
  directory_service_url: Optional[str] = None
121
121
 
122
122
  def to_dict(self) -> Dict[str, Any]:
123
- """Convertit en dictionnaire pour l'API."""
123
+ """Convert to dictionary for API."""
124
124
  result = {
125
125
  "flow_service_url": self.flow_service_url,
126
126
  "token_url": self.token_url,
@@ -133,14 +133,14 @@ class AFNORCredentials:
133
133
 
134
134
 
135
135
  # =============================================================================
136
- # Helpers pour les types anyOf - évite la verbosité des wrappers générés
136
+ # Helpers for anyOf types - avoids verbosity of generated wrappers
137
137
  # =============================================================================
138
138
 
139
- def montant(value: Union[str, float, int, Decimal, None]) -> str:
140
- """Convertit une valeur en string de montant pour l'API.
139
+ def amount(value: Union[str, float, int, Decimal, None]) -> str:
140
+ """Convert a value to an amount string for the API.
141
141
 
142
- L'API FactPulse accepte les montants comme strings ou floats.
143
- Cette fonction normalise en string pour garantir la précision monétaire.
142
+ The FactPulse API accepts amounts as strings or floats.
143
+ This function normalizes to string to guarantee monetary precision.
144
144
  """
145
145
  if value is None:
146
146
  return "0.00"
@@ -153,351 +153,432 @@ def montant(value: Union[str, float, int, Decimal, None]) -> str:
153
153
  return "0.00"
154
154
 
155
155
 
156
- def montant_total(
157
- ht: Union[str, float, int, Decimal],
158
- tva: Union[str, float, int, Decimal],
159
- ttc: Union[str, float, int, Decimal],
160
- a_payer: Union[str, float, int, Decimal],
161
- remise_ttc: Union[str, float, int, Decimal, None] = None,
162
- motif_remise: Optional[str] = None,
163
- acompte: Union[str, float, int, Decimal, None] = None,
156
+ def invoice_totals(
157
+ total_excl_tax: Union[str, float, int, Decimal],
158
+ total_vat: Union[str, float, int, Decimal],
159
+ total_incl_tax: Union[str, float, int, Decimal],
160
+ amount_due: Union[str, float, int, Decimal],
161
+ discount_incl_tax: Union[str, float, int, Decimal, None] = None,
162
+ discount_reason: Optional[str] = None,
163
+ prepayment: Union[str, float, int, Decimal, None] = None,
164
164
  ) -> Dict[str, Any]:
165
- """Crée un objet MontantTotal simplifié.
165
+ """Create a simplified InvoiceTotals object.
166
166
 
167
- Évite d'avoir à utiliser les wrappers MontantHtTotal, MontantTvaTotal, etc.
167
+ Avoids having to use wrappers like TotalNetAmount, VatAmount, etc.
168
168
  """
169
169
  result = {
170
- "montantHtTotal": montant(ht),
171
- "montantTva": montant(tva),
172
- "montantTtcTotal": montant(ttc),
173
- "montantAPayer": montant(a_payer),
170
+ "totalNetAmount": amount(total_excl_tax),
171
+ "vatAmount": amount(total_vat),
172
+ "totalGrossAmount": amount(total_incl_tax),
173
+ "amountDue": amount(amount_due),
174
174
  }
175
- if remise_ttc is not None:
176
- result["montantRemiseGlobaleTtc"] = montant(remise_ttc)
177
- if motif_remise is not None:
178
- result["motifRemiseGlobaleTtc"] = motif_remise
179
- if acompte is not None:
180
- result["acompte"] = montant(acompte)
175
+ if discount_incl_tax is not None:
176
+ result["globalAllowanceAmount"] = amount(discount_incl_tax)
177
+ if discount_reason is not None:
178
+ result["globalAllowanceReason"] = discount_reason
179
+ if prepayment is not None:
180
+ result["prepayment"] = amount(prepayment)
181
181
  return result
182
182
 
183
183
 
184
- def ligne_de_poste(
185
- numero: int,
186
- denomination: str,
187
- quantite: Union[str, float, int, Decimal],
188
- montant_unitaire_ht: Union[str, float, int, Decimal],
189
- montant_total_ligne_ht: Union[str, float, int, Decimal],
190
- taux_tva: Optional[str] = None,
191
- taux_tva_manuel: Union[str, float, int, Decimal, None] = "20.00",
192
- categorie_tva: str = "S",
193
- unite: str = "FORFAIT",
184
+ def invoice_line(
185
+ line_number: int,
186
+ description: str,
187
+ quantity: Union[str, float, int, Decimal],
188
+ unit_price_excl_tax: Union[str, float, int, Decimal],
189
+ line_total_excl_tax: Union[str, float, int, Decimal],
190
+ vat_rate_code: Optional[str] = None,
191
+ vat_rate_value: Union[str, float, int, Decimal, None] = "20.00",
192
+ vat_category: str = "S",
193
+ unit: str = "FORFAIT",
194
194
  reference: Optional[str] = None,
195
- montant_remise_ht: Union[str, float, int, Decimal, None] = None,
196
- code_raison_reduction: Optional[str] = None,
197
- raison_reduction: Optional[str] = None,
198
- date_debut_periode: Optional[str] = None,
199
- date_fin_periode: Optional[str] = None,
195
+ discount_excl_tax: Union[str, float, int, Decimal, None] = None,
196
+ discount_reason_code: Optional[str] = None,
197
+ discount_reason: Optional[str] = None,
198
+ period_start_date: Optional[str] = None,
199
+ period_end_date: Optional[str] = None,
200
200
  ) -> Dict[str, Any]:
201
- """Crée une ligne de poste pour l'API FactPulse.
201
+ """Create an invoice line for the FactPulse API.
202
202
 
203
- Les clés JSON sont en camelCase (convention API FactPulse).
204
- Les champs correspondent exactement à LigneDePoste dans models.py.
203
+ JSON keys are in camelCase (FactPulse API convention).
204
+ Fields correspond exactly to LigneDePoste in models.py.
205
205
 
206
- Pour le taux de TVA, vous pouvez utiliser soit:
207
- - taux_tva: Code prédéfini (ex: "TVA20", "TVA10", "TVA5.5")
208
- - taux_tva_manuel: Valeur numérique (ex: "20.00", 20, 20.0)
206
+ For VAT rate, you can use either:
207
+ - vat_rate_code: Predefined code (e.g., "TVA20", "TVA10", "TVA5.5")
208
+ - vat_rate_value: Numeric value (e.g., "20.00", 20, 20.0)
209
209
 
210
210
  Args:
211
- numero: Numéro de la ligne
212
- denomination: Libellé du produit/service
213
- quantite: Quantité
214
- montant_unitaire_ht: Prix unitaire HT
215
- montant_total_ligne_ht: Montant total HT de la ligne
216
- taux_tva: Code TVA prédéfini (ex: "TVA20") - optionnel
217
- taux_tva_manuel: Taux de TVA en valeur (défaut: "20.00") - utilisé si taux_tva non fourni
218
- categorie_tva: Catégorie TVA - S (standard), Z (zéro), E (exonéré), AE (autoliquidation), K (intracommunautaire)
219
- unite: Unité de facturation (défaut: "FORFAIT")
220
- reference: Référence article
221
- montant_remise_ht: Montant de remise HT (optionnel)
222
- code_raison_reduction: Code raison de la réduction
223
- raison_reduction: Description textuelle de la réduction
224
- date_debut_periode: Date début période de facturation (YYYY-MM-DD)
225
- date_fin_periode: Date fin période de facturation (YYYY-MM-DD)
211
+ line_number: Line number
212
+ description: Product/service description
213
+ quantity: Quantity
214
+ unit_price_excl_tax: Unit price excl. tax
215
+ line_total_excl_tax: Line total excl. tax
216
+ vat_rate_code: Predefined VAT code (e.g., "TVA20") - optional
217
+ vat_rate_value: VAT rate value (default: "20.00") - used if vat_rate_code not provided
218
+ vat_category: VAT category - S (standard), Z (zero), E (exempt), AE (reverse charge), K (intra-community)
219
+ unit: Billing unit (default: "FORFAIT")
220
+ reference: Item reference
221
+ discount_excl_tax: Discount amount excl. tax (optional)
222
+ discount_reason_code: Discount reason code
223
+ discount_reason: Discount reason description
224
+ period_start_date: Billing period start date (YYYY-MM-DD)
225
+ period_end_date: Billing period end date (YYYY-MM-DD)
226
226
  """
227
227
  result = {
228
- "numero": numero,
229
- "denomination": denomination,
230
- "quantite": montant(quantite),
231
- "montantUnitaireHt": montant(montant_unitaire_ht),
232
- "montantTotalLigneHt": montant(montant_total_ligne_ht),
233
- "categorieTva": categorie_tva,
234
- "unite": unite,
228
+ "lineNumber": line_number,
229
+ "itemName": description,
230
+ "quantity": amount(quantity),
231
+ "unitNetPrice": amount(unit_price_excl_tax),
232
+ "lineNetAmount": amount(line_total_excl_tax),
233
+ "vatCategory": vat_category,
234
+ "unit": unit,
235
235
  }
236
- # Soit taux_tva (code) soit taux_tva_manuel (valeur)
237
- if taux_tva is not None:
238
- result["tauxTva"] = taux_tva
239
- elif taux_tva_manuel is not None:
240
- result["tauxTvaManuel"] = montant(taux_tva_manuel)
236
+ # Either vat_rate_code (code) or vat_rate_value (value)
237
+ if vat_rate_code is not None:
238
+ result["vatRate"] = vat_rate_code
239
+ elif vat_rate_value is not None:
240
+ result["manualVatRate"] = amount(vat_rate_value)
241
241
  if reference is not None:
242
242
  result["reference"] = reference
243
- if montant_remise_ht is not None:
244
- result["montantRemiseHt"] = montant(montant_remise_ht)
245
- if code_raison_reduction is not None:
246
- result["codeRaisonReduction"] = code_raison_reduction
247
- if raison_reduction is not None:
248
- result["raisonReduction"] = raison_reduction
249
- if date_debut_periode is not None:
250
- result["dateDebutPeriode"] = date_debut_periode
251
- if date_fin_periode is not None:
252
- result["dateFinPeriode"] = date_fin_periode
243
+ if discount_excl_tax is not None:
244
+ result["lineAllowanceAmount"] = amount(discount_excl_tax)
245
+ if discount_reason_code is not None:
246
+ result["allowanceReasonCode"] = discount_reason_code
247
+ if discount_reason is not None:
248
+ result["allowanceReason"] = discount_reason
249
+ if period_start_date is not None:
250
+ result["periodStartDate"] = period_start_date
251
+ if period_end_date is not None:
252
+ result["periodEndDate"] = period_end_date
253
253
  return result
254
254
 
255
255
 
256
- def ligne_de_tva(
257
- montant_base_ht: Union[str, float, int, Decimal],
258
- montant_tva: Union[str, float, int, Decimal],
259
- taux: Optional[str] = None,
260
- taux_manuel: Union[str, float, int, Decimal, None] = "20.00",
261
- categorie: str = "S",
256
+ def vat_line(
257
+ base_amount_excl_tax: Union[str, float, int, Decimal],
258
+ vat_amount: Union[str, float, int, Decimal],
259
+ rate_code: Optional[str] = None,
260
+ rate_value: Union[str, float, int, Decimal, None] = "20.00",
261
+ category: str = "S",
262
262
  ) -> Dict[str, Any]:
263
- """Crée une ligne de TVA pour l'API FactPulse.
263
+ """Create a VAT line for the FactPulse API.
264
264
 
265
- Les clés JSON sont en camelCase (convention API FactPulse).
266
- Les champs correspondent exactement à LigneDeTVA dans models.py.
265
+ JSON keys are in camelCase (FactPulse API convention).
266
+ Fields correspond exactly to LigneDeTVA in models.py.
267
267
 
268
- Pour le taux de TVA, vous pouvez utiliser soit:
269
- - taux: Code prédéfini (ex: "TVA20", "TVA10", "TVA5.5")
270
- - taux_manuel: Valeur numérique (ex: "20.00", 20, 20.0)
268
+ For VAT rate, you can use either:
269
+ - rate_code: Predefined code (e.g., "TVA20", "TVA10", "TVA5.5")
270
+ - rate_value: Numeric value (e.g., "20.00", 20, 20.0)
271
271
 
272
272
  Args:
273
- montant_base_ht: Montant de la base HT
274
- montant_tva: Montant de la TVA
275
- taux: Code TVA prédéfini (ex: "TVA20") - optionnel
276
- taux_manuel: Taux de TVA en valeur (défaut: "20.00") - utilisé si taux non fourni
277
- categorie: Catégorie de TVA (défaut: "S" pour standard)
273
+ base_amount_excl_tax: Base amount excl. tax
274
+ vat_amount: VAT amount
275
+ rate_code: Predefined VAT code (e.g., "TVA20") - optional
276
+ rate_value: VAT rate value (default: "20.00") - used if rate_code not provided
277
+ category: VAT category (default: "S" for standard)
278
278
  """
279
279
  result = {
280
- "montantBaseHt": montant(montant_base_ht),
281
- "montantTva": montant(montant_tva),
282
- "categorie": categorie,
280
+ "taxableAmount": amount(base_amount_excl_tax),
281
+ "vatAmount": amount(vat_amount),
282
+ "category": category,
283
283
  }
284
- # Soit taux (code) soit taux_manuel (valeur)
285
- if taux is not None:
286
- result["taux"] = taux
287
- elif taux_manuel is not None:
288
- result["tauxManuel"] = montant(taux_manuel)
284
+ # Either rate_code (code) or rate_value (value)
285
+ if rate_code is not None:
286
+ result["rate"] = rate_code
287
+ elif rate_value is not None:
288
+ result["manualRate"] = amount(rate_value)
289
289
  return result
290
290
 
291
291
 
292
- def adresse_postale(
293
- ligne1: str,
294
- code_postal: str,
295
- ville: str,
296
- pays: str = "FR",
297
- ligne2: Optional[str] = None,
298
- ligne3: Optional[str] = None,
292
+ def postal_address(
293
+ line1: str,
294
+ postal_code: str,
295
+ city: str,
296
+ country: str = "FR",
297
+ line2: Optional[str] = None,
298
+ line3: Optional[str] = None,
299
299
  ) -> Dict[str, Any]:
300
- """Crée une adresse postale pour l'API FactPulse.
300
+ """Create a postal address for the FactPulse API.
301
301
 
302
302
  Args:
303
- ligne1: Première ligne d'adresse (numéro, rue)
304
- code_postal: Code postal
305
- ville: Nom de la ville
306
- pays: Code pays ISO (défaut: "FR")
307
- ligne2: Deuxième ligne d'adresse (optionnel)
308
- ligne3: Troisième ligne d'adresse (optionnel)
303
+ line1: First address line (number, street)
304
+ postal_code: Postal code
305
+ city: City name
306
+ country: ISO country code (default: "FR")
307
+ line2: Second address line (optional)
308
+ line3: Third address line (optional)
309
309
 
310
310
  Example:
311
- >>> adresse = adresse_postale("123 rue Example", "75001", "Paris")
311
+ >>> address = postal_address("123 Example Street", "75001", "Paris")
312
312
  """
313
313
  result = {
314
- "ligneUn": ligne1,
315
- "codePostal": code_postal,
316
- "nomVille": ville,
317
- "paysCodeIso": pays,
314
+ "lineOne": line1,
315
+ "postalCode": postal_code,
316
+ "city": city,
317
+ "countryCode": country,
318
318
  }
319
- if ligne2:
320
- result["ligneDeux"] = ligne2
321
- if ligne3:
322
- result["ligneTrois"] = ligne3
319
+ if line2:
320
+ result["lineTwo"] = line2
321
+ if line3:
322
+ result["lineThree"] = line3
323
323
  return result
324
324
 
325
325
 
326
- def adresse_electronique(
327
- identifiant: str,
326
+ def electronic_address(
327
+ identifier: str,
328
328
  scheme_id: str = "0009",
329
329
  ) -> Dict[str, Any]:
330
- """Crée une adresse électronique pour l'API FactPulse.
330
+ """Create an electronic address for the FactPulse API.
331
331
 
332
332
  Args:
333
- identifiant: Identifiant de l'adresse (SIRET, SIREN, etc.)
334
- scheme_id: Schéma d'identification (défaut: "0009" pour SIREN)
333
+ identifier: Address identifier (SIRET, SIREN, etc.)
334
+ scheme_id: Identification scheme (default: "0009" for SIREN)
335
335
  - "0009": SIREN
336
336
  - "0088": EAN
337
337
  - "0096": DUNS
338
- - "0130": Codification propre
339
- - "0225": FR - SIRET (schéma français)
338
+ - "0130": Custom coding
339
+ - "0225": FR - SIRET (French scheme)
340
340
 
341
341
  Example:
342
- >>> adresse = adresse_electronique("12345678901234", "0225") # SIRET
342
+ >>> address = electronic_address("12345678901234", "0225") # SIRET
343
343
  """
344
344
  return {
345
- "identifiant": identifiant,
345
+ "identifier": identifier,
346
346
  "schemeId": scheme_id,
347
347
  }
348
348
 
349
349
 
350
- def fournisseur(
351
- nom: str,
350
+ def supplier(
351
+ name: str,
352
352
  siret: str,
353
- adresse_ligne1: str,
354
- code_postal: str,
355
- ville: str,
356
- id_fournisseur: int = 0,
353
+ address_line1: str,
354
+ postal_code: str,
355
+ city: str,
356
+ supplier_id: int = 0,
357
357
  siren: Optional[str] = None,
358
- numero_tva_intra: Optional[str] = None,
358
+ vat_number: Optional[str] = None,
359
359
  iban: Optional[str] = None,
360
- pays: str = "FR",
361
- adresse_ligne2: Optional[str] = None,
362
- code_service: Optional[int] = None,
363
- code_coordonnees_bancaires: Optional[int] = None,
360
+ country: str = "FR",
361
+ address_line2: Optional[str] = None,
362
+ service_code: Optional[int] = None,
363
+ bank_details_code: Optional[int] = None,
364
364
  ) -> Dict[str, Any]:
365
- """Crée un fournisseur (émetteur de la facture) pour l'API FactPulse.
365
+ """Create a supplier (invoice issuer) for the FactPulse API.
366
366
 
367
- Cette fonction simplifie la création d'un fournisseur en générant automatiquement:
368
- - L'adresse postale structurée
369
- - L'adresse électronique (basée sur le SIRET)
370
- - Le SIREN (extrait du SIRET si non fourni)
371
- - Le numéro de TVA intracommunautaire (calculé depuis le SIREN si non fourni)
367
+ This function simplifies supplier creation by automatically generating:
368
+ - Structured postal address
369
+ - Electronic address (based on SIRET)
370
+ - SIREN (extracted from SIRET if not provided)
371
+ - Intra-community VAT number (calculated from SIREN if not provided)
372
372
 
373
373
  Args:
374
- nom: Raison sociale / dénomination
375
- siret: Numéro SIRET (14 chiffres)
376
- adresse_ligne1: Première ligne d'adresse
377
- code_postal: Code postal
378
- ville: Ville
379
- id_fournisseur: ID Chorus Pro du fournisseur (défaut: 0)
380
- siren: Numéro SIREN (9 chiffres) - calculé depuis SIRET si absent
381
- numero_tva_intra: Numéro TVA intracommunautaire - calculé si absent
382
- iban: IBAN pour le paiement
383
- pays: Code pays ISO (défaut: "FR")
384
- adresse_ligne2: Deuxième ligne d'adresse (optionnel)
385
- code_service: ID du service fournisseur Chorus Pro (optionnel)
386
- code_coordonnees_bancaires: Code coordonnées bancaires Chorus Pro (optionnel)
374
+ name: Company name / trade name
375
+ siret: SIRET number (14 digits)
376
+ address_line1: First address line
377
+ postal_code: Postal code
378
+ city: City
379
+ supplier_id: Chorus Pro supplier ID (default: 0)
380
+ siren: SIREN number (9 digits) - calculated from SIRET if absent
381
+ vat_number: Intra-community VAT number - calculated if absent
382
+ iban: IBAN for payment
383
+ country: ISO country code (default: "FR")
384
+ address_line2: Second address line (optional)
385
+ service_code: Chorus Pro supplier service ID (optional)
386
+ bank_details_code: Chorus Pro bank details code (optional)
387
387
 
388
388
  Returns:
389
- Dict prêt à être utilisé dans une facture
389
+ Dict ready to be used in an invoice
390
390
 
391
391
  Example:
392
- >>> f = fournisseur(
393
- ... nom="Ma Société SAS",
392
+ >>> s = supplier(
393
+ ... name="My Company SAS",
394
394
  ... siret="12345678900001",
395
- ... adresse_ligne1="123 Rue de la République",
396
- ... code_postal="75001",
397
- ... ville="Paris",
395
+ ... address_line1="123 Republic Street",
396
+ ... postal_code="75001",
397
+ ... city="Paris",
398
398
  ... iban="FR7630006000011234567890189",
399
399
  ... )
400
400
  """
401
- # Auto-calcul SIREN depuis SIRET
401
+ # Auto-calculate SIREN from SIRET
402
402
  if not siren and len(siret) == 14:
403
403
  siren = siret[:9]
404
404
 
405
- # Auto-calcul TVA intracommunautaire française
406
- if not numero_tva_intra and siren and len(siren) == 9:
407
- # Clé TVA = (12 + 3 * (SIREN % 97)) % 97
405
+ # Auto-calculate French intra-community VAT number
406
+ if not vat_number and siren and len(siren) == 9:
407
+ # VAT key = (12 + 3 * (SIREN % 97)) % 97
408
408
  try:
409
- cle = (12 + 3 * (int(siren) % 97)) % 97
410
- numero_tva_intra = f"FR{cle:02d}{siren}"
409
+ key = (12 + 3 * (int(siren) % 97)) % 97
410
+ vat_number = f"FR{key:02d}{siren}"
411
411
  except ValueError:
412
- pass # SIREN non numérique, on skip
412
+ pass # Non-numeric SIREN, skip
413
413
 
414
414
  result: Dict[str, Any] = {
415
- "nom": nom,
416
- "idFournisseur": id_fournisseur,
415
+ "name": name,
416
+ "supplierId": supplier_id,
417
417
  "siret": siret,
418
- "adresseElectronique": adresse_electronique(siret, "0225"),
419
- "adressePostale": adresse_postale(adresse_ligne1, code_postal, ville, pays, adresse_ligne2),
418
+ "electronicAddress": electronic_address(siret, "0225"),
419
+ "postalAddress": postal_address(address_line1, postal_code, city, country, address_line2),
420
420
  }
421
421
 
422
422
  if siren:
423
423
  result["siren"] = siren
424
- if numero_tva_intra:
425
- result["numeroTvaIntra"] = numero_tva_intra
424
+ if vat_number:
425
+ result["vatNumber"] = vat_number
426
426
  if iban:
427
427
  result["iban"] = iban
428
- if code_service:
429
- result["idServiceFournisseur"] = code_service
430
- if code_coordonnees_bancaires:
431
- result["codeCoordonnesBancairesFournisseur"] = code_coordonnees_bancaires
428
+ if service_code:
429
+ result["supplierServiceId"] = service_code
430
+ if bank_details_code:
431
+ result["supplierBankDetailsCode"] = bank_details_code
432
432
 
433
433
  return result
434
434
 
435
435
 
436
- def destinataire(
437
- nom: str,
436
+ def recipient(
437
+ name: str,
438
438
  siret: str,
439
- adresse_ligne1: str,
440
- code_postal: str,
441
- ville: str,
439
+ address_line1: str,
440
+ postal_code: str,
441
+ city: str,
442
442
  siren: Optional[str] = None,
443
- pays: str = "FR",
444
- adresse_ligne2: Optional[str] = None,
445
- code_service_executant: Optional[str] = None,
443
+ country: str = "FR",
444
+ address_line2: Optional[str] = None,
445
+ service_code: Optional[str] = None,
446
446
  ) -> Dict[str, Any]:
447
- """Crée un destinataire (client de la facture) pour l'API FactPulse.
447
+ """Create a recipient (invoice customer) for the FactPulse API.
448
448
 
449
- Cette fonction simplifie la création d'un destinataire en générant automatiquement:
450
- - L'adresse postale structurée
451
- - L'adresse électronique (basée sur le SIRET)
452
- - Le SIREN (extrait du SIRET si non fourni)
449
+ This function simplifies recipient creation by automatically generating:
450
+ - Structured postal address
451
+ - Electronic address (based on SIRET)
452
+ - SIREN (extracted from SIRET if not provided)
453
453
 
454
454
  Args:
455
- nom: Raison sociale / dénomination
456
- siret: Numéro SIRET (14 chiffres)
457
- adresse_ligne1: Première ligne d'adresse
458
- code_postal: Code postal
459
- ville: Ville
460
- siren: Numéro SIREN (9 chiffres) - calculé depuis SIRET si absent
461
- pays: Code pays ISO (défaut: "FR")
462
- adresse_ligne2: Deuxième ligne d'adresse (optionnel)
463
- code_service_executant: Code du service destinataire (optionnel)
455
+ name: Company name / trade name
456
+ siret: SIRET number (14 digits)
457
+ address_line1: First address line
458
+ postal_code: Postal code
459
+ city: City
460
+ siren: SIREN number (9 digits) - calculated from SIRET if absent
461
+ country: ISO country code (default: "FR")
462
+ address_line2: Second address line (optional)
463
+ service_code: Recipient service code (optional)
464
464
 
465
465
  Returns:
466
- Dict prêt à être utilisé dans une facture
466
+ Dict ready to be used in an invoice
467
467
 
468
468
  Example:
469
- >>> d = destinataire(
470
- ... nom="Client SARL",
469
+ >>> r = recipient(
470
+ ... name="Client SARL",
471
471
  ... siret="98765432109876",
472
- ... adresse_ligne1="456 Avenue des Champs",
473
- ... code_postal="69001",
474
- ... ville="Lyon",
472
+ ... address_line1="456 Champs Avenue",
473
+ ... postal_code="69001",
474
+ ... city="Lyon",
475
475
  ... )
476
476
  """
477
- # Auto-calcul SIREN depuis SIRET
477
+ # Auto-calculate SIREN from SIRET
478
478
  if not siren and len(siret) == 14:
479
479
  siren = siret[:9]
480
480
 
481
481
  result: Dict[str, Any] = {
482
- "nom": nom,
482
+ "name": name,
483
483
  "siret": siret,
484
- "adresseElectronique": adresse_electronique(siret, "0225"),
485
- "adressePostale": adresse_postale(adresse_ligne1, code_postal, ville, pays, adresse_ligne2),
484
+ "electronicAddress": electronic_address(siret, "0225"),
485
+ "postalAddress": postal_address(address_line1, postal_code, city, country, address_line2),
486
486
  }
487
487
 
488
488
  if siren:
489
489
  result["siren"] = siren
490
- if code_service_executant:
491
- result["codeServiceExecutant"] = code_service_executant
490
+ if service_code:
491
+ result["executingServiceCode"] = service_code
492
492
 
493
493
  return result
494
494
 
495
495
 
496
+ def payee(
497
+ name: str,
498
+ siret: Optional[str] = None,
499
+ siren: Optional[str] = None,
500
+ iban: Optional[str] = None,
501
+ bic: Optional[str] = None,
502
+ ) -> Dict[str, Any]:
503
+ """Create a payee (factor) for factoring.
504
+
505
+ The payee (BG-10 / PayeeTradeParty) is used when payment must be made
506
+ to a third party different from the supplier, typically a factor
507
+ (factoring company).
508
+
509
+ For factored invoices, you must also:
510
+ - Use a factored document type (393, 396, 501, 502, 472, 473)
511
+ - Add an ACC note with the assignment clause
512
+ - The payee's IBAN will be used for payment
513
+
514
+ Args:
515
+ name: Factor's company name (BT-59)
516
+ siret: Factor's SIRET number (BT-60, schemeID 0009) - 14 digits
517
+ siren: Factor's SIREN number (BT-61, schemeID 0002) - calculated from SIRET if absent
518
+ iban: Factor's IBAN - to receive payment
519
+ bic: Factor's bank BIC (optional)
520
+
521
+ Returns:
522
+ Dict ready to be used in a factored invoice
523
+
524
+ Example:
525
+ >>> # Simple factored invoice
526
+ >>> factor = payee(
527
+ ... name="FACTOR SAS",
528
+ ... siret="30000000700033",
529
+ ... iban="FR76 3000 4000 0500 0012 3456 789",
530
+ ... )
531
+ >>> invoice = {
532
+ ... "invoiceNumber": "INV-2025-001-FACT",
533
+ ... "supplier": supplier(...),
534
+ ... "recipient": recipient(...),
535
+ ... "payee": factor, # Factor receives payment
536
+ ... "references": {
537
+ ... "invoiceType": "393", # Factored invoice
538
+ ... ...
539
+ ... },
540
+ ... "notes": [
541
+ ... {
542
+ ... "content": "This receivable has been assigned to FACTOR SAS. Contract n. FACT-2025",
543
+ ... "subjectCode": "ACC", # Mandatory assignment code
544
+ ... },
545
+ ... ...
546
+ ... ],
547
+ ... ...
548
+ ... }
549
+
550
+ See Also:
551
+ - Factoring guide: docs/factoring_guide.md
552
+ - Factored document types: 393 (invoice), 396 (credit note), 501, 502, 472, 473
553
+ - ACC note: Mandatory factoring assignment clause
554
+ """
555
+ # Auto-calculate SIREN from SIRET
556
+ if not siren and siret and len(siret) == 14:
557
+ siren = siret[:9]
558
+
559
+ result: Dict[str, Any] = {
560
+ "name": name,
561
+ }
562
+
563
+ if siret:
564
+ result["siret"] = siret
565
+ if siren:
566
+ result["siren"] = siren
567
+ if iban:
568
+ result["iban"] = iban
569
+ if bic:
570
+ result["bic"] = bic
571
+
572
+ return result
573
+
574
+
575
+
576
+
496
577
  class FactPulseClient:
497
- """Client simplifié pour l'API FactPulse.
578
+ """Simplified client for the FactPulse API.
498
579
 
499
- Gère l'authentification JWT, le polling des tâches asynchrones,
500
- et permet de configurer les credentials Chorus Pro / AFNOR à l'initialisation.
580
+ Handles JWT authentication, asynchronous task polling,
581
+ and allows configuring Chorus Pro / AFNOR credentials at initialization.
501
582
  """
502
583
 
503
584
  DEFAULT_API_URL = "https://factpulse.fr"
@@ -533,24 +614,24 @@ class FactPulseClient:
533
614
  self._api_client: Optional[ApiClient] = None
534
615
 
535
616
  def get_chorus_credentials_for_api(self) -> Optional[Dict[str, Any]]:
536
- """Retourne les credentials Chorus Pro au format API."""
617
+ """Return Chorus Pro credentials in API format."""
537
618
  return self.chorus_credentials.to_dict() if self.chorus_credentials else None
538
619
 
539
620
  def get_afnor_credentials_for_api(self) -> Optional[Dict[str, Any]]:
540
- """Retourne les credentials AFNOR au format API."""
621
+ """Return AFNOR credentials in API format."""
541
622
  return self.afnor_credentials.to_dict() if self.afnor_credentials else None
542
623
 
543
- # Alias plus courts pour faciliter l'usage
624
+ # Shorter aliases for convenience
544
625
  def get_chorus_pro_credentials(self) -> Optional[Dict[str, Any]]:
545
- """Alias pour get_chorus_credentials_for_api()."""
626
+ """Alias for get_chorus_credentials_for_api()."""
546
627
  return self.get_chorus_credentials_for_api()
547
628
 
548
629
  def get_afnor_credentials(self) -> Optional[Dict[str, Any]]:
549
- """Alias pour get_afnor_credentials_for_api()."""
630
+ """Alias for get_afnor_credentials_for_api()."""
550
631
  return self.get_afnor_credentials_for_api()
551
632
 
552
633
  def _obtain_token(self) -> Dict[str, str]:
553
- """Obtient un nouveau token JWT."""
634
+ """Obtain a new JWT token."""
554
635
  token_url = f"{self.api_url}/api/token/"
555
636
  payload = {"username": self.email, "password": self.password}
556
637
  if self.client_uid:
@@ -559,7 +640,7 @@ class FactPulseClient:
559
640
  try:
560
641
  response = requests.post(token_url, json=payload, timeout=30)
561
642
  response.raise_for_status()
562
- logger.info("Token JWT obtenu pour %s", self.email)
643
+ logger.info("JWT token obtained for %s", self.email)
563
644
  return response.json()
564
645
  except requests.RequestException as e:
565
646
  error_detail = ""
@@ -568,12 +649,12 @@ class FactPulseClient:
568
649
  error_detail = e.response.json().get("detail", str(e))
569
650
  except Exception:
570
651
  error_detail = str(e)
571
- raise FactPulseAuthError(f"Impossible d'obtenir le token JWT: {error_detail or e}")
652
+ raise FactPulseAuthError(f"Unable to obtain JWT token: {error_detail or e}")
572
653
 
573
654
  def _refresh_access_token(self) -> str:
574
- """Rafraîchit le token d'accès."""
655
+ """Refresh the access token."""
575
656
  if not self._refresh_token:
576
- raise FactPulseAuthError("Aucun refresh token disponible")
657
+ raise FactPulseAuthError("No refresh token available")
577
658
 
578
659
  refresh_url = f"{self.api_url}/api/token/refresh/"
579
660
  try:
@@ -581,16 +662,16 @@ class FactPulseClient:
581
662
  refresh_url, json={"refresh": self._refresh_token}, timeout=30
582
663
  )
583
664
  response.raise_for_status()
584
- logger.info("Token rafraîchi avec succès")
665
+ logger.info("Token refreshed successfully")
585
666
  return response.json()["access"]
586
667
  except requests.RequestException:
587
- logger.warning("Refresh échoué, ré-obtention d'un nouveau token")
668
+ logger.warning("Refresh failed, obtaining new token")
588
669
  tokens = self._obtain_token()
589
670
  self._refresh_token = tokens["refresh"]
590
671
  return tokens["access"]
591
672
 
592
673
  def ensure_authenticated(self, force_refresh: bool = False) -> None:
593
- """S'assure que le client est authentifié."""
674
+ """Ensure the client is authenticated."""
594
675
  now = datetime.now()
595
676
 
596
677
  if force_refresh or not self._access_token or not self._token_expires_at or now >= self._token_expires_at:
@@ -608,30 +689,77 @@ class FactPulseClient:
608
689
  self._token_expires_at = now + timedelta(minutes=28)
609
690
 
610
691
  def reset_auth(self) -> None:
611
- """Réinitialise l'authentification."""
692
+ """Reset authentication."""
612
693
  self._access_token = None
613
694
  self._refresh_token = None
614
695
  self._token_expires_at = None
615
696
  self._api_client = None
616
- logger.info("Authentification réinitialisée")
697
+ logger.info("Authentication reset")
698
+
699
+ def _request(
700
+ self,
701
+ method: str,
702
+ endpoint: str,
703
+ files: Optional[Dict] = None,
704
+ data: Optional[Dict] = None,
705
+ json_data: Optional[Dict] = None,
706
+ ) -> requests.Response:
707
+ """Perform an HTTP request to the FactPulse API.
708
+
709
+ Args:
710
+ method: HTTP method (GET, POST, etc.)
711
+ endpoint: Relative endpoint (e.g., /processing/validate-pdf)
712
+ files: Files for multipart/form-data
713
+ data: Form data
714
+ json_data: JSON data
617
715
 
618
- def get_traitement_api(self) -> TraitementFactureApi:
619
- """Retourne l'API de traitement de factures."""
716
+ Returns:
717
+ API response
718
+
719
+ Raises:
720
+ FactPulseValidationError: On API error
721
+ """
722
+ self.ensure_authenticated()
723
+ url = f"{self.api_url}/api/v1{endpoint}"
724
+ headers = {"Authorization": f"Bearer {self._access_token}"}
725
+
726
+ try:
727
+ if files:
728
+ response = requests.request(method, url, files=files, data=data, headers=headers, timeout=60)
729
+ elif json_data:
730
+ response = requests.request(method, url, json=json_data, headers=headers, timeout=30)
731
+ else:
732
+ response = requests.request(method, url, data=data, headers=headers, timeout=30)
733
+ except requests.RequestException as e:
734
+ raise FactPulseValidationError(f"Network error: {e}")
735
+
736
+ if response.status_code >= 400:
737
+ try:
738
+ error_json = response.json()
739
+ error_msg = error_json.get("detail", error_json.get("errorMessage", str(error_json)))
740
+ except Exception:
741
+ error_msg = response.text or f"HTTP error {response.status_code}"
742
+ raise FactPulseValidationError(f"API error: {error_msg}")
743
+
744
+ return response
745
+
746
+ def get_processing_api(self) -> InvoiceProcessingApi:
747
+ """Return the invoice processing API."""
620
748
  self.ensure_authenticated()
621
749
  config = Configuration(host=f"{self.api_url}/api/facturation")
622
750
  config.access_token = self._access_token
623
751
  self._api_client = ApiClient(configuration=config)
624
- return TraitementFactureApi(api_client=self._api_client)
752
+ return InvoiceProcessingApi(api_client=self._api_client)
625
753
 
626
754
  def poll_task(self, task_id: str, timeout: Optional[int] = None, interval: Optional[int] = None) -> Dict[str, Any]:
627
- """Effectue un polling sur une tâche jusqu'à son achèvement."""
755
+ """Poll a task until completion."""
628
756
  timeout_ms = timeout or self.polling_timeout
629
757
  interval_ms = interval or self.polling_interval
630
758
 
631
759
  start_time = time.time() * 1000
632
760
  current_interval = float(interval_ms)
633
761
 
634
- logger.info("Début du polling pour la tâche %s (timeout: %dms)", task_id, timeout_ms)
762
+ logger.info("Starting polling for task %s (timeout: %dms)", task_id, timeout_ms)
635
763
 
636
764
  while True:
637
765
  elapsed = (time.time() * 1000) - start_time
@@ -640,28 +768,28 @@ class FactPulseClient:
640
768
  raise FactPulsePollingTimeout(task_id, timeout_ms)
641
769
 
642
770
  try:
643
- logger.debug("Polling tâche %s (elapsed: %.0fms)...", task_id, elapsed)
644
- api = self.get_traitement_api()
645
- statut = api.obtenir_statut_tache_api_v1_traitement_taches_id_tache_statut_get(id_tache=task_id)
646
- logger.debug("Réponse statut reçue: %s", statut)
771
+ logger.debug("Polling task %s (elapsed: %.0fms)...", task_id, elapsed)
772
+ api = self.get_processing_api()
773
+ status = api.get_task_status_api_v1_processing_tasks_task_id_status_get(task_id=task_id)
774
+ logger.debug("Status response received: %s", status)
647
775
 
648
- status_value = statut.statut.value if hasattr(statut.statut, "value") else str(statut.statut)
649
- logger.info("Tâche %s: statut=%s (%.0fms)", task_id, status_value, elapsed)
776
+ status_value = status.status.value if hasattr(status.status, "value") else str(status.status)
777
+ logger.info("Task %s: status=%s (%.0fms)", task_id, status_value, elapsed)
650
778
 
651
779
  if status_value == "SUCCESS":
652
- logger.info("Tâche %s terminée avec succès", task_id)
653
- if statut.resultat:
654
- if hasattr(statut.resultat, "to_dict"):
655
- return statut.resultat.to_dict()
656
- return dict(statut.resultat)
780
+ logger.info("Task %s completed successfully", task_id)
781
+ if status.result:
782
+ if hasattr(status.result, "to_dict"):
783
+ return status.result.to_dict()
784
+ return dict(status.result)
657
785
  return {}
658
786
 
659
787
  if status_value == "FAILURE":
660
- error_msg = "Erreur inconnue"
788
+ error_msg = "Unknown error"
661
789
  errors = []
662
- if statut.resultat:
663
- result = statut.resultat.to_dict() if hasattr(statut.resultat, "to_dict") else dict(statut.resultat)
664
- # Format AFNOR: errorMessage, details
790
+ if status.result:
791
+ result = status.result.to_dict() if hasattr(status.result, "to_dict") else dict(status.result)
792
+ # AFNOR format: errorMessage, details
665
793
  error_msg = result.get("errorMessage", error_msg)
666
794
  for err in result.get("details", []):
667
795
  errors.append(ValidationErrorDetail(
@@ -671,80 +799,80 @@ class FactPulseClient:
671
799
  source=err.get("source"),
672
800
  code=err.get("code"),
673
801
  ))
674
- raise FactPulseValidationError(f"La tâche {task_id} a échoué: {error_msg}", errors)
802
+ raise FactPulseValidationError(f"Task {task_id} failed: {error_msg}", errors)
675
803
 
676
804
  except (FactPulseValidationError, FactPulsePollingTimeout):
677
805
  raise
678
806
  except Exception as e:
679
807
  error_str = str(e)
680
- logger.warning("Erreur lors du polling: %s", error_str)
808
+ logger.warning("Error during polling: %s", error_str)
681
809
 
682
- # Rate limit (429) - attendre et réessayer avec backoff
810
+ # Rate limit (429) - wait and retry with backoff
683
811
  if "429" in error_str:
684
812
  wait_time = min(current_interval * 2, 30000) # Max 30s
685
- logger.warning("Rate limit (429), attente de %.1fs avant retry...", wait_time / 1000)
813
+ logger.warning("Rate limit (429), waiting %.1fs before retry...", wait_time / 1000)
686
814
  time.sleep(wait_time / 1000)
687
815
  current_interval = wait_time
688
816
  continue
689
817
 
690
- # Token expiré (401) - re-authentification
818
+ # Token expired (401) - re-authenticate
691
819
  if "401" in error_str:
692
- logger.warning("Token expiré, re-authentification...")
820
+ logger.warning("Token expired, re-authenticating...")
693
821
  self.reset_auth()
694
822
  continue
695
823
 
696
- # Erreur serveur temporaire (502, 503, 504) - retry avec backoff
824
+ # Temporary server error (502, 503, 504) - retry with backoff
697
825
  if any(code in error_str for code in ("502", "503", "504")):
698
826
  wait_time = min(current_interval * 1.5, 15000)
699
- logger.warning("Erreur serveur temporaire, attente de %.1fs avant retry...", wait_time / 1000)
827
+ logger.warning("Temporary server error, waiting %.1fs before retry...", wait_time / 1000)
700
828
  time.sleep(wait_time / 1000)
701
829
  current_interval = wait_time
702
830
  continue
703
831
 
704
- raise FactPulseValidationError(f"Erreur API: {e}")
832
+ raise FactPulseValidationError(f"API error: {e}")
705
833
 
706
834
  time.sleep(current_interval / 1000)
707
835
  current_interval = min(current_interval * 1.5, 10000)
708
836
 
709
- def generer_facturx(
837
+ def generate_facturx(
710
838
  self,
711
- facture_data: Union[Dict, str, Any],
839
+ invoice_data: Union[Dict, str, Any],
712
840
  pdf_source: Union[bytes, str, Path],
713
- profil: str = "EN16931",
714
- format_sortie: str = "pdf",
841
+ profile: str = "EN16931",
842
+ output_format: str = "pdf",
715
843
  sync: bool = True,
716
844
  timeout: Optional[int] = None,
717
845
  ) -> bytes:
718
- """Génère une facture Factur-X.
846
+ """Generate a Factur-X invoice.
719
847
 
720
- Accepte les données de facture sous plusieurs formes :
721
- - Dict : dictionnaire Python (recommandé avec les helpers montant_total(), ligne_de_poste(), etc.)
722
- - str : JSON sérialisé
723
- - Modèle Pydantic : modèle généré par le SDK (sera converti via .to_dict())
848
+ Accepts invoice data in multiple forms:
849
+ - Dict: Python dictionary (recommended with helpers invoice_totals(), invoice_line(), etc.)
850
+ - str: Serialized JSON
851
+ - Pydantic model: SDK-generated model (will be converted via .to_dict())
724
852
 
725
853
  Args:
726
- facture_data: Données de la facture (dict, JSON string, ou modèle Pydantic)
727
- pdf_source: Chemin vers le PDF source, ou bytes du PDF
728
- profil: Profil Factur-X (MINIMUM, BASIC, EN16931, EXTENDED)
729
- format_sortie: Format de sortie (pdf, xml, both)
730
- sync: Si True, attend la fin de la tâche et retourne le résultat
731
- timeout: Timeout en ms pour le polling
854
+ invoice_data: Invoice data (dict, JSON string, or Pydantic model)
855
+ pdf_source: Path to source PDF, or PDF bytes
856
+ profile: Factur-X profile (MINIMUM, BASIC, EN16931, EXTENDED)
857
+ output_format: Output format (pdf, xml, both)
858
+ sync: If True, wait for task completion and return result
859
+ timeout: Polling timeout in ms
732
860
 
733
861
  Returns:
734
- bytes: Contenu du fichier généré (PDF ou XML)
862
+ bytes: Generated file content (PDF or XML)
735
863
  """
736
- # Conversion des données en JSON string (gère Decimal, datetime, etc.)
737
- if isinstance(facture_data, str):
738
- json_data = facture_data
739
- elif isinstance(facture_data, dict):
740
- json_data = json_dumps_safe(facture_data)
741
- elif hasattr(facture_data, "to_dict"):
742
- # Modèle Pydantic généré par le SDK
743
- json_data = json_dumps_safe(facture_data.to_dict())
864
+ # Convert data to JSON string (handles Decimal, datetime, etc.)
865
+ if isinstance(invoice_data, str):
866
+ json_data = invoice_data
867
+ elif isinstance(invoice_data, dict):
868
+ json_data = json_dumps_safe(invoice_data)
869
+ elif hasattr(invoice_data, "to_dict"):
870
+ # Pydantic model generated by SDK
871
+ json_data = json_dumps_safe(invoice_data.to_dict())
744
872
  else:
745
- raise FactPulseValidationError(f"Type de données non supporté: {type(facture_data)}")
873
+ raise FactPulseValidationError(f"Unsupported data type: {type(invoice_data)}")
746
874
 
747
- # Préparation du PDF
875
+ # Prepare PDF
748
876
  if isinstance(pdf_source, (str, Path)):
749
877
  pdf_path = Path(pdf_source)
750
878
  pdf_bytes = pdf_path.read_bytes()
@@ -753,40 +881,100 @@ class FactPulseClient:
753
881
  pdf_bytes = pdf_source
754
882
  pdf_filename = "source.pdf"
755
883
 
756
- # Envoi direct via requests (bypass des modèles Pydantic du SDK)
884
+ # Direct send via requests (bypass SDK Pydantic models)
757
885
  for attempt in range(self.max_retries + 1):
758
886
  self.ensure_authenticated()
759
887
  try:
760
- url = f"{self.api_url}/api/v1/traitement/generer-facture"
888
+ url = f"{self.api_url}/api/v1/processing/generate-invoice"
761
889
  files = {
762
- "donnees_facture": (None, json_data, "application/json"),
763
- "profil": (None, profil),
764
- "format_sortie": (None, format_sortie),
890
+ "invoice_data": (None, json_data, "application/json"),
891
+ "profile": (None, profile),
892
+ "output_format": (None, output_format),
765
893
  "source_pdf": (pdf_filename, pdf_bytes, "application/pdf"),
766
894
  }
767
895
  headers = {"Authorization": f"Bearer {self._access_token}"}
768
896
  response = requests.post(url, files=files, headers=headers, timeout=60)
769
897
 
770
898
  if response.status_code == 401 and attempt < self.max_retries:
771
- logger.warning("Erreur 401, réinitialisation du token (tentative %d/%d)", attempt + 1, self.max_retries + 1)
899
+ logger.warning("Error 401, resetting token (attempt %d/%d)", attempt + 1, self.max_retries + 1)
772
900
  self.reset_auth()
773
901
  continue
774
902
 
775
- response.raise_for_status()
903
+ # Handle HTTP errors with response body extraction
904
+ if response.status_code >= 400:
905
+ error_body = None
906
+ try:
907
+ error_body = response.json()
908
+ except Exception:
909
+ error_body = {"detail": response.text or f"HTTP {response.status_code}"}
910
+
911
+ # Detailed error logging
912
+ logger.error("API error %d: %s", response.status_code, error_body)
913
+
914
+ # Extract error details in standardized format
915
+ errors = []
916
+ error_msg = f"HTTP error {response.status_code}"
917
+
918
+ if isinstance(error_body, dict):
919
+ # FastAPI/Pydantic format: {"detail": [{"loc": [...], "msg": "...", "type": "..."}]}
920
+ if "detail" in error_body:
921
+ detail = error_body["detail"]
922
+ if isinstance(detail, list):
923
+ # Pydantic validation error list
924
+ error_msg = "Validation error"
925
+ for err in detail:
926
+ if isinstance(err, dict):
927
+ loc = err.get("loc", [])
928
+ loc_str = " -> ".join(str(l) for l in loc) if loc else ""
929
+ errors.append(ValidationErrorDetail(
930
+ level="ERROR",
931
+ item=loc_str,
932
+ reason=err.get("msg", str(err)),
933
+ source="validation",
934
+ code=err.get("type"),
935
+ ))
936
+ elif isinstance(detail, str):
937
+ error_msg = detail
938
+ # AFNOR format: {"errorMessage": "...", "details": [...]}
939
+ elif "errorMessage" in error_body:
940
+ error_msg = error_body["errorMessage"]
941
+ for err in error_body.get("details", []):
942
+ errors.append(ValidationErrorDetail(
943
+ level=err.get("level", "ERROR"),
944
+ item=err.get("item", ""),
945
+ reason=err.get("reason", ""),
946
+ source=err.get("source"),
947
+ code=err.get("code"),
948
+ ))
949
+
950
+ # For 422 errors (validation), don't retry
951
+ if response.status_code == 422:
952
+ raise FactPulseValidationError(error_msg, errors)
953
+
954
+ # For other client errors (4xx), don't retry either
955
+ if 400 <= response.status_code < 500:
956
+ raise FactPulseValidationError(error_msg, errors)
957
+
958
+ # For server errors (5xx), retry if possible
959
+ if attempt < self.max_retries:
960
+ logger.warning("Server error %d (attempt %d/%d)", response.status_code, attempt + 1, self.max_retries + 1)
961
+ continue
962
+ raise FactPulseValidationError(error_msg, errors)
963
+
776
964
  result = response.json()
777
- task_id = result.get("id_tache")
965
+ task_id = result.get("task_id")
778
966
 
779
967
  if not task_id:
780
- raise FactPulseValidationError("Pas d'ID de tâche dans la réponse")
968
+ raise FactPulseValidationError("No task ID in response")
781
969
 
782
970
  if not sync:
783
971
  return task_id.encode()
784
972
 
785
973
  poll_result = self.poll_task(task_id, timeout)
786
974
 
787
- if poll_result.get("statut") == "ERREUR":
788
- # Format AFNOR: errorMessage, details
789
- error_msg = poll_result.get("errorMessage", "Erreur de validation")
975
+ if poll_result.get("status") == "ERROR":
976
+ # AFNOR format: errorMessage, details
977
+ error_msg = poll_result.get("errorMessage", "Validation error")
790
978
  errors = [
791
979
  ValidationErrorDetail(
792
980
  level=e.get("level", ""),
@@ -799,71 +987,74 @@ class FactPulseClient:
799
987
  ]
800
988
  raise FactPulseValidationError(error_msg, errors)
801
989
 
802
- if "contenu_b64" in poll_result:
803
- return base64.b64decode(poll_result["contenu_b64"])
990
+ if "content_b64" in poll_result:
991
+ return base64.b64decode(poll_result["content_b64"])
804
992
 
805
- raise FactPulseValidationError("Le résultat ne contient pas de contenu")
993
+ raise FactPulseValidationError("Result does not contain content")
806
994
 
807
995
  except requests.RequestException as e:
996
+ # Network errors (connection, timeout, etc.) - no HTTP error
808
997
  if attempt < self.max_retries:
809
- logger.warning("Erreur réseau (tentative %d/%d): %s", attempt + 1, self.max_retries + 1, e)
998
+ logger.warning("Network error (attempt %d/%d): %s", attempt + 1, self.max_retries + 1, e)
810
999
  continue
811
- raise FactPulseValidationError(f"Erreur API: {e}")
1000
+ raise FactPulseValidationError(f"Network error: {e}")
1001
+
1002
+ raise FactPulseValidationError("Failed after all attempts")
812
1003
 
813
- raise FactPulseValidationError("Échec après toutes les tentatives")
814
1004
 
815
1005
  @staticmethod
816
- def format_montant(montant) -> str:
817
- """Formate un montant pour l'API FactPulse."""
818
- if montant is None:
1006
+ def format_amount(value) -> str:
1007
+ """Format an amount for the FactPulse API."""
1008
+ if value is None:
819
1009
  return "0.00"
820
- if isinstance(montant, Decimal):
821
- return f"{montant:.2f}"
822
- if isinstance(montant, (int, float)):
823
- return f"{montant:.2f}"
824
- if isinstance(montant, str):
825
- return montant
1010
+ if isinstance(value, Decimal):
1011
+ return f"{value:.2f}"
1012
+ if isinstance(value, (int, float)):
1013
+ return f"{value:.2f}"
1014
+ if isinstance(value, str):
1015
+ return value
826
1016
  return "0.00"
827
1017
 
828
1018
  # =========================================================================
829
1019
  # AFNOR PDP/PA - Flow Service
830
1020
  # =========================================================================
831
1021
  #
832
- # ARCHITECTURE GRAVÉE DANS LE MARBRE - NE PAS MODIFIER SANS COMPRENDRE
1022
+ # ARCHITECTURE SET IN STONE - DO NOT MODIFY WITHOUT UNDERSTANDING
833
1023
  #
834
- # Le proxy AFNOR est 100% TRANSPARENT. Il a le même OpenAPI que l'AFNOR.
835
- # Le SDK doit TOUJOURS :
836
- # 1. Obtenir les credentials AFNOR (mode stored: via /credentials, mode zero-trust: fournis)
837
- # 2. Faire l'OAuth AFNOR lui-même
838
- # 3. Appeler les endpoints avec le token AFNOR + header X-PDP-Base-URL
1024
+ # The AFNOR proxy is 100% TRANSPARENT. It has the same OpenAPI as AFNOR.
1025
+ # The SDK must ALWAYS:
1026
+ # 1. Obtain AFNOR credentials (stored mode: via /credentials, zero-trust mode: provided)
1027
+ # 2. Perform AFNOR OAuth itself
1028
+ # 3. Call endpoints with AFNOR token + X-PDP-Base-URL header
839
1029
  #
840
- # Le token JWT FactPulse n'est JAMAIS utilisé pour appeler la PDP !
1030
+ # The FactPulse JWT token is NEVER used to call the PDP!
1031
+ # It's only used to retrieve credentials in stored mode.
841
1032
  # =========================================================================
842
1033
 
843
1034
  def _get_afnor_credentials(self) -> "AFNORCredentials":
844
- """Obtient les credentials AFNOR (mode stored ou zero-trust).
1035
+ """Obtain AFNOR credentials (stored or zero-trust mode).
845
1036
 
846
- **Mode zero-trust** : Retourne les afnor_credentials fournis au constructeur.
847
- **Mode stored** : Récupère les credentials via GET /api/v1/afnor/credentials.
1037
+ **Zero-trust mode**: Returns afnor_credentials provided to constructor.
1038
+ **Stored mode**: Retrieves credentials via GET /api/v1/afnor/credentials.
848
1039
 
849
1040
  Returns:
850
- AFNORCredentials avec flow_service_url, token_url, client_id, client_secret
1041
+ AFNORCredentials with flow_service_url, token_url, client_id, client_secret
851
1042
 
852
1043
  Raises:
853
- FactPulseAuthError: Si pas de credentials disponibles
854
- FactPulseServiceUnavailableError: Si le serveur est indisponible
1044
+ FactPulseAuthError: If no credentials available
1045
+ FactPulseServiceUnavailableError: If server is unavailable
855
1046
  """
856
1047
  from .exceptions import FactPulseServiceUnavailableError
857
1048
 
858
- # Mode zero-trust : credentials fournis au constructeur
1049
+ # Zero-trust mode: credentials provided to constructor
859
1050
  if self.afnor_credentials:
860
- logger.info("Mode zero-trust: utilisation des AFNORCredentials fournis")
1051
+ logger.info("Zero-trust mode: using provided AFNORCredentials")
861
1052
  return self.afnor_credentials
862
1053
 
863
- # Mode stored : récupérer les credentials via l'API
864
- logger.info("Mode stored: récupération des credentials via /api/v1/afnor/credentials")
1054
+ # Stored mode: retrieve credentials via API
1055
+ logger.info("Stored mode: retrieving credentials via /api/v1/afnor/credentials")
865
1056
 
866
- self.ensure_authenticated() # S'assurer qu'on a un token JWT FactPulse
1057
+ self.ensure_authenticated() # Ensure we have a FactPulse JWT token
867
1058
 
868
1059
  url = f"{self.api_url}/api/v1/afnor/credentials"
869
1060
  headers = {"Authorization": f"Bearer {self._access_token}"}
@@ -878,12 +1069,12 @@ class FactPulseClient:
878
1069
  error_detail = error_json.get("detail", {})
879
1070
  if isinstance(error_detail, dict) and error_detail.get("error") == "NO_CLIENT_UID":
880
1071
  raise FactPulseAuthError(
881
- "Aucun client_uid dans le JWT. "
882
- "Pour utiliser les endpoints AFNOR, soit :\n"
883
- "1. Générez un token avec un client_uid (mode stored)\n"
884
- "2. Fournissez AFNORCredentials au constructeur du client (mode zero-trust)"
1072
+ "No client_uid in JWT. "
1073
+ "To use AFNOR endpoints, either:\n"
1074
+ "1. Generate a token with a client_uid (stored mode)\n"
1075
+ "2. Provide AFNORCredentials to the client constructor (zero-trust mode)"
885
1076
  )
886
- raise FactPulseAuthError(f"Erreur credentials AFNOR: {error_detail}")
1077
+ raise FactPulseAuthError(f"AFNOR credentials error: {error_detail}")
887
1078
 
888
1079
  if response.status_code != 200:
889
1080
  try:
@@ -891,12 +1082,12 @@ class FactPulseClient:
891
1082
  error_msg = error_json.get("detail", str(error_json))
892
1083
  except Exception:
893
1084
  error_msg = response.text or f"HTTP {response.status_code}"
894
- raise FactPulseAuthError(f"Échec récupération credentials AFNOR: {error_msg}")
1085
+ raise FactPulseAuthError(f"Failed to retrieve AFNOR credentials: {error_msg}")
895
1086
 
896
1087
  creds = response.json()
897
- logger.info(f"Credentials AFNOR récupérés pour PDP: {creds.get('flow_service_url')}")
1088
+ logger.info(f"AFNOR credentials retrieved for PDP: {creds.get('flow_service_url')}")
898
1089
 
899
- # Créer un AFNORCredentials temporaire
1090
+ # Create temporary AFNORCredentials
900
1091
  return AFNORCredentials(
901
1092
  flow_service_url=creds["flow_service_url"],
902
1093
  token_url=creds["token_url"],
@@ -905,27 +1096,27 @@ class FactPulseClient:
905
1096
  )
906
1097
 
907
1098
  def _get_afnor_token_and_url(self) -> Tuple[str, str]:
908
- """Obtient le token OAuth2 AFNOR et l'URL de la PDP.
1099
+ """Obtain AFNOR OAuth2 token and PDP URL.
909
1100
 
910
- Cette méthode :
911
- 1. Récupère les credentials AFNOR (mode stored ou zero-trust)
912
- 2. Fait l'OAuth AFNOR pour obtenir un token
913
- 3. Retourne le token et l'URL de la PDP
1101
+ This method:
1102
+ 1. Retrieves AFNOR credentials (stored or zero-trust mode)
1103
+ 2. Performs AFNOR OAuth to obtain a token
1104
+ 3. Returns the token and PDP URL
914
1105
 
915
1106
  Returns:
916
1107
  Tuple (afnor_token, pdp_base_url)
917
1108
 
918
1109
  Raises:
919
- FactPulseAuthError: Si l'authentification échoue
920
- FactPulseServiceUnavailableError: Si le service est indisponible
1110
+ FactPulseAuthError: If authentication fails
1111
+ FactPulseServiceUnavailableError: If service is unavailable
921
1112
  """
922
1113
  from .exceptions import FactPulseServiceUnavailableError
923
1114
 
924
- # Étape 1: Obtenir les credentials AFNOR
1115
+ # Step 1: Get AFNOR credentials
925
1116
  credentials = self._get_afnor_credentials()
926
1117
 
927
- # Étape 2: Faire l'OAuth AFNOR
928
- logger.info(f"OAuth AFNOR vers: {credentials.token_url}")
1118
+ # Step 2: Perform AFNOR OAuth
1119
+ logger.info(f"AFNOR OAuth to: {credentials.token_url}")
929
1120
 
930
1121
  url = f"{self.api_url}/api/v1/afnor/oauth/token"
931
1122
  oauth_data = {
@@ -948,15 +1139,15 @@ class FactPulseClient:
948
1139
  error_msg = error_json.get("detail", error_json.get("error", str(error_json)))
949
1140
  except Exception:
950
1141
  error_msg = response.text or f"HTTP {response.status_code}"
951
- raise FactPulseAuthError(f"Échec OAuth2 AFNOR: {error_msg}")
1142
+ raise FactPulseAuthError(f"AFNOR OAuth2 failed: {error_msg}")
952
1143
 
953
1144
  token_data = response.json()
954
1145
  afnor_token = token_data.get("access_token")
955
1146
 
956
1147
  if not afnor_token:
957
- raise FactPulseAuthError("Réponse OAuth2 AFNOR invalide: access_token manquant")
1148
+ raise FactPulseAuthError("Invalid AFNOR OAuth2 response: missing access_token")
958
1149
 
959
- logger.info("Token OAuth2 AFNOR obtenu avec succès")
1150
+ logger.info("AFNOR OAuth2 token obtained successfully")
960
1151
  return afnor_token, credentials.flow_service_url
961
1152
 
962
1153
  def _make_afnor_request(
@@ -967,54 +1158,54 @@ class FactPulseClient:
967
1158
  files: Optional[Dict] = None,
968
1159
  params: Optional[Dict] = None,
969
1160
  ) -> requests.Response:
970
- """Effectue une requête vers l'API AFNOR avec gestion d'auth et d'erreurs.
1161
+ """Perform a request to the AFNOR API with auth and error handling.
971
1162
 
972
1163
  ================================================================================
973
- ARCHITECTURE GRAVÉE DANS LE MARBRE
1164
+ ARCHITECTURE SET IN STONE
974
1165
  ================================================================================
975
1166
 
976
- Cette méthode :
977
- 1. Récupère les credentials AFNOR (mode stored: API, mode zero-trust: fournis)
978
- 2. Fait l'OAuth AFNOR pour obtenir un token AFNOR
979
- 3. Appelle l'endpoint avec :
980
- - Authorization: Bearer {token_afnor} TOKEN AFNOR, PAS JWT FACTPULSE !
981
- - X-PDP-Base-URL: {url_pdp} Pour que le proxy route vers la bonne PDP
1167
+ This method:
1168
+ 1. Retrieves AFNOR credentials (stored mode: API, zero-trust mode: provided)
1169
+ 2. Performs AFNOR OAuth to obtain an AFNOR token
1170
+ 3. Calls the endpoint with:
1171
+ - Authorization: Bearer {afnor_token} <- AFNOR TOKEN, NOT FACTPULSE JWT!
1172
+ - X-PDP-Base-URL: {pdp_url} <- For proxy to route to correct PDP
982
1173
 
983
- Le token JWT FactPulse n'est JAMAIS utilisé pour appeler la PDP.
984
- Il sert uniquement à récupérer les credentials en mode stored.
1174
+ The FactPulse JWT token is NEVER used to call the PDP.
1175
+ It's only used to retrieve credentials in stored mode.
985
1176
 
986
1177
  ================================================================================
987
1178
 
988
1179
  Args:
989
- method: Méthode HTTP (GET, POST, etc.)
990
- endpoint: Endpoint relatif (ex: /flow/v1/flows)
991
- json_data: Données JSON (optionnel)
992
- files: Fichiers multipart (optionnel)
993
- params: Query params (optionnel)
1180
+ method: HTTP method (GET, POST, etc.)
1181
+ endpoint: Relative endpoint (e.g., /flow/v1/flows)
1182
+ json_data: JSON data (optional)
1183
+ files: Multipart files (optional)
1184
+ params: Query params (optional)
994
1185
 
995
1186
  Returns:
996
- Response de l'API
1187
+ API response
997
1188
 
998
1189
  Raises:
999
- FactPulseAuthError: Si 401 ou credentials manquants
1000
- FactPulseNotFoundError: Si 404
1001
- FactPulseServiceUnavailableError: Si 503
1002
- FactPulseValidationError: Si 400/422
1003
- FactPulseAPIError: Autres erreurs
1190
+ FactPulseAuthError: If 401 or missing credentials
1191
+ FactPulseNotFoundError: If 404
1192
+ FactPulseServiceUnavailableError: If 503
1193
+ FactPulseValidationError: If 400/422
1194
+ FactPulseAPIError: Other errors
1004
1195
  """
1005
1196
  from .exceptions import (
1006
1197
  parse_api_error,
1007
1198
  FactPulseServiceUnavailableError,
1008
1199
  )
1009
1200
 
1010
- # Obtenir le token AFNOR et l'URL de la PDP
1011
- # (mode stored: récupère credentials via API, mode zero-trust: utilise credentials fournis)
1201
+ # Get AFNOR token and PDP URL
1202
+ # (stored mode: retrieves credentials via API, zero-trust mode: uses provided credentials)
1012
1203
  afnor_token, pdp_base_url = self._get_afnor_token_and_url()
1013
1204
 
1014
1205
  url = f"{self.api_url}/api/v1/afnor{endpoint}"
1015
1206
 
1016
- # TOUJOURS utiliser le token AFNOR + header X-PDP-Base-URL
1017
- # Le token JWT FactPulse n'est JAMAIS utilisé pour appeler la PDP !
1207
+ # ALWAYS use AFNOR token + X-PDP-Base-URL header
1208
+ # The FactPulse JWT token is NEVER used to call the PDP!
1018
1209
  headers = {
1019
1210
  "Authorization": f"Bearer {afnor_token}",
1020
1211
  "X-PDP-Base-URL": pdp_base_url,
@@ -1036,65 +1227,65 @@ class FactPulseClient:
1036
1227
  try:
1037
1228
  error_json = response.json()
1038
1229
  except Exception:
1039
- error_json = {"errorMessage": response.text or f"Erreur HTTP {response.status_code}"}
1230
+ error_json = {"errorMessage": response.text or f"HTTP error {response.status_code}"}
1040
1231
  raise parse_api_error(error_json, response.status_code)
1041
1232
 
1042
1233
  return response
1043
1234
 
1044
- def soumettre_facture_afnor(
1235
+ def submit_invoice_afnor(
1045
1236
  self,
1046
1237
  flow_name: str,
1047
1238
  pdf_path: Optional[Union[str, Path]] = None,
1048
1239
  pdf_bytes: Optional[bytes] = None,
1049
- pdf_filename: str = "facture.pdf",
1240
+ pdf_filename: str = "invoice.pdf",
1050
1241
  flow_syntax: str = "CII",
1051
1242
  flow_profile: str = "EN16931",
1052
1243
  tracking_id: Optional[str] = None,
1053
1244
  sha256: Optional[str] = None,
1054
1245
  ) -> Dict[str, Any]:
1055
- """Soumet une facture Factur-X à une PDP via l'API AFNOR.
1246
+ """Submit a Factur-X invoice to a PDP via the AFNOR API.
1056
1247
 
1057
- L'authentification utilise soit le client_uid du JWT (mode stocké),
1058
- soit les afnor_credentials fournis au constructeur (mode zero-trust).
1248
+ Authentication uses either the client_uid from JWT (stored mode),
1249
+ or afnor_credentials provided to constructor (zero-trust mode).
1059
1250
 
1060
1251
  Args:
1061
- flow_name: Nom du flux (ex: "Facture FAC-2025-001")
1062
- pdf_path: Chemin vers le fichier PDF/A-3 (exclusif avec pdf_bytes)
1063
- pdf_bytes: Contenu PDF en bytes (exclusif avec pdf_path)
1064
- pdf_filename: Nom du fichier pour les bytes (défaut: "facture.pdf")
1065
- flow_syntax: Syntaxe du flux (CII ou UBL)
1066
- flow_profile: Profil Factur-X (MINIMUM, BASIC, EN16931, EXTENDED)
1067
- tracking_id: Identifiant de suivi métier (optionnel)
1068
- sha256: Empreinte SHA-256 du fichier (calculée auto si absent)
1252
+ flow_name: Flow name (e.g., "Invoice INV-2025-001")
1253
+ pdf_path: Path to PDF/A-3 file (exclusive with pdf_bytes)
1254
+ pdf_bytes: PDF content as bytes (exclusive with pdf_path)
1255
+ pdf_filename: Filename for bytes (default: "invoice.pdf")
1256
+ flow_syntax: Flow syntax (CII or UBL)
1257
+ flow_profile: Factur-X profile (MINIMUM, BASIC, EN16931, EXTENDED)
1258
+ tracking_id: Business tracking identifier (optional)
1259
+ sha256: SHA-256 hash of file (calculated automatically if absent)
1069
1260
 
1070
1261
  Returns:
1071
- Dict avec flowId, trackingId, status, sha256, etc.
1262
+ Dict with flowId, trackingId, status, sha256, etc.
1072
1263
 
1073
1264
  Raises:
1074
- FactPulseValidationError: Si le PDF n'est pas valide
1075
- FactPulseServiceUnavailableError: Si la PDP est indisponible
1076
- ValueError: Si ni pdf_path ni pdf_bytes n'est fourni
1265
+ FactPulseValidationError: If PDF is not valid
1266
+ FactPulseServiceUnavailableError: If PDP is unavailable
1267
+ ValueError: If neither pdf_path nor pdf_bytes is provided
1077
1268
 
1078
1269
  Example:
1079
- >>> # Avec un chemin de fichier
1080
- >>> result = client.soumettre_facture_afnor(
1081
- ... flow_name="Facture FAC-2025-001",
1082
- ... pdf_path="facture.pdf",
1083
- ... tracking_id="FAC-2025-001",
1270
+ >>> # With a file path
1271
+ >>> result = client.submit_invoice_afnor(
1272
+ ... flow_name="Invoice INV-2025-001",
1273
+ ... pdf_path="invoice.pdf",
1274
+ ... tracking_id="INV-2025-001",
1084
1275
  ... )
1085
1276
  >>> print(result["flowId"])
1086
1277
 
1087
- >>> # Avec des bytes (ex: après génération Factur-X)
1088
- >>> result = client.soumettre_facture_afnor(
1089
- ... flow_name="Facture FAC-2025-001",
1278
+ >>> # With bytes (e.g., after Factur-X generation)
1279
+ >>> result = client.submit_invoice_afnor(
1280
+ ... flow_name="Invoice INV-2025-001",
1090
1281
  ... pdf_bytes=pdf_content,
1091
- ... pdf_filename="FAC-2025-001.pdf",
1092
- ... tracking_id="FAC-2025-001",
1282
+ ... pdf_filename="INV-2025-001.pdf",
1283
+ ... tracking_id="INV-2025-001",
1093
1284
  ... )
1094
1285
  """
1095
1286
  import hashlib
1096
1287
 
1097
- # Charger le PDF depuis le chemin si fourni
1288
+ # Load PDF from path if provided
1098
1289
  filename = pdf_filename
1099
1290
  if pdf_path:
1100
1291
  pdf_path = Path(pdf_path)
@@ -1102,13 +1293,13 @@ class FactPulseClient:
1102
1293
  filename = pdf_path.name
1103
1294
 
1104
1295
  if not pdf_bytes:
1105
- raise ValueError("pdf_path ou pdf_bytes requis")
1296
+ raise ValueError("pdf_path or pdf_bytes required")
1106
1297
 
1107
- # Calculer SHA-256 si non fourni
1298
+ # Calculate SHA-256 if not provided
1108
1299
  if not sha256:
1109
1300
  sha256 = hashlib.sha256(pdf_bytes).hexdigest()
1110
1301
 
1111
- # Préparer flowInfo
1302
+ # Prepare flowInfo
1112
1303
  flow_info = {
1113
1304
  "name": flow_name,
1114
1305
  "flowSyntax": flow_syntax,
@@ -1126,28 +1317,29 @@ class FactPulseClient:
1126
1317
  response = self._make_afnor_request("POST", "/flow/v1/flows", files=files)
1127
1318
  return response.json()
1128
1319
 
1129
- def rechercher_flux_afnor(
1320
+
1321
+ def search_flows_afnor(
1130
1322
  self,
1131
1323
  tracking_id: Optional[str] = None,
1132
1324
  status: Optional[str] = None,
1133
1325
  offset: int = 0,
1134
1326
  limit: int = 25,
1135
1327
  ) -> Dict[str, Any]:
1136
- """Recherche des flux de facturation AFNOR.
1328
+ """Search AFNOR invoice flows.
1137
1329
 
1138
1330
  Args:
1139
- tracking_id: Filtrer par trackingId
1140
- status: Filtrer par status (submitted, processing, delivered, etc.)
1141
- offset: Index de début (pagination)
1142
- limit: Nombre max de résultats
1331
+ tracking_id: Filter by trackingId
1332
+ status: Filter by status (submitted, processing, delivered, etc.)
1333
+ offset: Start index (pagination)
1334
+ limit: Max number of results
1143
1335
 
1144
1336
  Returns:
1145
- Dict avec flows (liste), total, offset, limit
1337
+ Dict with flows (list), total, offset, limit
1146
1338
 
1147
1339
  Example:
1148
- >>> results = client.rechercher_flux_afnor(tracking_id="FAC-2025-001")
1149
- >>> for flux in results["flows"]:
1150
- ... print(flux["flowId"], flux["status"])
1340
+ >>> results = client.search_flows_afnor(tracking_id="INV-2025-001")
1341
+ >>> for flow in results["flows"]:
1342
+ ... print(flow["flowId"], flow["status"])
1151
1343
  """
1152
1344
  search_body = {
1153
1345
  "offset": offset,
@@ -1162,85 +1354,87 @@ class FactPulseClient:
1162
1354
  response = self._make_afnor_request("POST", "/flow/v1/flows/search", json_data=search_body)
1163
1355
  return response.json()
1164
1356
 
1165
- def telecharger_flux_afnor(self, flow_id: str) -> bytes:
1166
- """Télécharge le fichier PDF d'un flux AFNOR.
1357
+
1358
+ def download_flow_afnor(self, flow_id: str) -> bytes:
1359
+ """Download the PDF file of an AFNOR flow.
1167
1360
 
1168
1361
  Args:
1169
- flow_id: Identifiant du flux (UUID)
1362
+ flow_id: Flow identifier (UUID)
1170
1363
 
1171
1364
  Returns:
1172
- Contenu du fichier PDF
1365
+ PDF file content
1173
1366
 
1174
1367
  Raises:
1175
- FactPulseNotFoundError: Si le flux n'existe pas
1368
+ FactPulseNotFoundError: If flow doesn't exist
1176
1369
 
1177
1370
  Example:
1178
- >>> pdf_bytes = client.telecharger_flux_afnor("550e8400-e29b-41d4-a716-446655440000")
1179
- >>> with open("facture.pdf", "wb") as f:
1371
+ >>> pdf_bytes = client.download_flow_afnor("550e8400-e29b-41d4-a716-446655440000")
1372
+ >>> with open("invoice.pdf", "wb") as f:
1180
1373
  ... f.write(pdf_bytes)
1181
1374
  """
1182
1375
  response = self._make_afnor_request("GET", f"/flow/v1/flows/{flow_id}")
1183
1376
  return response.content
1184
1377
 
1185
- def obtenir_facture_entrante_afnor(
1378
+
1379
+ def get_incoming_invoice_afnor(
1186
1380
  self,
1187
1381
  flow_id: str,
1188
1382
  include_document: bool = False,
1189
1383
  ) -> Dict[str, Any]:
1190
- """Récupère les métadonnées JSON d'un flux entrant (facture fournisseur).
1384
+ """Retrieve JSON metadata of an incoming flow (supplier invoice).
1191
1385
 
1192
- Télécharge un flux entrant depuis la PDP AFNOR et extrait les métadonnées
1193
- de la facture vers un format JSON unifié. Supporte les formats Factur-X, CII et UBL.
1386
+ Downloads an incoming flow from the AFNOR PDP and extracts invoice
1387
+ metadata to a unified JSON format. Supports Factur-X, CII and UBL formats.
1194
1388
 
1195
- Note: Cet endpoint utilise l'authentification JWT FactPulse (pas OAuth AFNOR).
1196
- Le serveur FactPulse se charge d'appeler la PDP avec les credentials stockés.
1389
+ Note: This endpoint uses FactPulse JWT authentication (not AFNOR OAuth).
1390
+ The FactPulse server handles calling the PDP with stored credentials.
1197
1391
 
1198
1392
  Args:
1199
- flow_id: Identifiant du flux (UUID)
1200
- include_document: Si True, inclut le document original encodé en base64
1393
+ flow_id: Flow identifier (UUID)
1394
+ include_document: If True, include original document encoded in base64
1201
1395
 
1202
1396
  Returns:
1203
- Dict avec les métadonnées de la facture:
1204
- - flow_id: Identifiant du flux
1205
- - format_source: Format détecté (Factur-X, CII, UBL)
1206
- - ref_fournisseur: Numéro de facture fournisseur
1207
- - type_document: Code type (380=facture, 381=avoir, etc.)
1208
- - fournisseur: Dict avec nom, siret, numero_tva_intra
1209
- - site_facturation_nom: Nom du destinataire
1210
- - site_facturation_siret: SIRET du destinataire
1211
- - date_de_piece: Date de la facture (YYYY-MM-DD)
1212
- - date_reglement: Date d'échéance (YYYY-MM-DD)
1213
- - devise: Code devise (EUR, USD, etc.)
1214
- - montant_ht: Montant HT
1215
- - montant_tva: Montant TVA
1216
- - montant_ttc: Montant TTC
1217
- - document_base64: (si include_document=True) Document encodé
1218
- - document_content_type: (si include_document=True) Type MIME
1219
- - document_filename: (si include_document=True) Nom de fichier
1397
+ Dict with invoice metadata:
1398
+ - flow_id: Flow identifier
1399
+ - source_format: Detected format (Factur-X, CII, UBL)
1400
+ - supplier_reference: Supplier invoice number
1401
+ - document_type: Type code (380=invoice, 381=credit note, etc.)
1402
+ - supplier: Dict with name, siret, vat_number
1403
+ - billing_site_name: Recipient name
1404
+ - billing_site_siret: Recipient SIRET
1405
+ - document_date: Invoice date (YYYY-MM-DD)
1406
+ - due_date: Due date (YYYY-MM-DD)
1407
+ - currency: Currency code (EUR, USD, etc.)
1408
+ - total_excl_tax: Total excl. tax
1409
+ - total_vat: VAT amount
1410
+ - total_incl_tax: Total incl. tax
1411
+ - document_base64: (if include_document=True) Encoded document
1412
+ - document_content_type: (if include_document=True) MIME type
1413
+ - document_filename: (if include_document=True) Filename
1220
1414
 
1221
1415
  Raises:
1222
- FactPulseNotFoundError: Si le flux n'existe pas
1223
- FactPulseValidationError: Si le format n'est pas supporté
1416
+ FactPulseNotFoundError: If flow doesn't exist
1417
+ FactPulseValidationError: If format is not supported
1224
1418
 
1225
1419
  Example:
1226
- >>> # Récupérer les métadonnées d'une facture entrante
1227
- >>> facture = client.obtenir_facture_entrante_afnor("550e8400-e29b-41d4-a716-446655440000")
1228
- >>> print(f"Fournisseur: {facture['fournisseur']['nom']}")
1229
- >>> print(f"Montant TTC: {facture['montant_ttc']} {facture['devise']}")
1230
-
1231
- >>> # Avec le document original
1232
- >>> facture = client.obtenir_facture_entrante_afnor(flow_id, include_document=True)
1233
- >>> if facture.get('document_base64'):
1420
+ >>> # Retrieve incoming invoice metadata
1421
+ >>> invoice = client.get_incoming_invoice_afnor("550e8400-e29b-41d4-a716-446655440000")
1422
+ >>> print(f"Supplier: {invoice['supplier']['name']}")
1423
+ >>> print(f"Total incl. tax: {invoice['total_incl_tax']} {invoice['currency']}")
1424
+
1425
+ >>> # With original document
1426
+ >>> invoice = client.get_incoming_invoice_afnor(flow_id, include_document=True)
1427
+ >>> if invoice.get('document_base64'):
1234
1428
  ... import base64
1235
- ... pdf_bytes = base64.b64decode(facture['document_base64'])
1236
- ... with open(facture['document_filename'], 'wb') as f:
1429
+ ... pdf_bytes = base64.b64decode(invoice['document_base64'])
1430
+ ... with open(invoice['document_filename'], 'wb') as f:
1237
1431
  ... f.write(pdf_bytes)
1238
1432
  """
1239
1433
  from .exceptions import FactPulseNotFoundError, FactPulseServiceUnavailableError, parse_api_error
1240
1434
 
1241
1435
  self.ensure_authenticated()
1242
1436
 
1243
- url = f"{self.api_url}/api/v1/afnor/flux-entrants/{flow_id}"
1437
+ url = f"{self.api_url}/api/v1/afnor/incoming-flows/{flow_id}"
1244
1438
  params = {}
1245
1439
  if include_document:
1246
1440
  params["include_document"] = "true"
@@ -1250,22 +1444,23 @@ class FactPulseClient:
1250
1444
  try:
1251
1445
  response = requests.get(url, headers=headers, params=params if params else None, timeout=60)
1252
1446
  except requests.RequestException as e:
1253
- raise FactPulseServiceUnavailableError("FactPulse AFNOR flux-entrants", e)
1447
+ raise FactPulseServiceUnavailableError("FactPulse AFNOR incoming flows", e)
1254
1448
 
1255
1449
  if response.status_code >= 400:
1256
1450
  try:
1257
1451
  error_json = response.json()
1258
1452
  except Exception:
1259
- error_json = {"detail": response.text or f"Erreur HTTP {response.status_code}"}
1453
+ error_json = {"detail": response.text or f"HTTP error {response.status_code}"}
1260
1454
  raise parse_api_error(error_json, response.status_code)
1261
1455
 
1262
1456
  return response.json()
1263
1457
 
1458
+
1264
1459
  def healthcheck_afnor(self) -> Dict[str, Any]:
1265
- """Vérifie la disponibilité du Flow Service AFNOR.
1460
+ """Check AFNOR Flow Service availability.
1266
1461
 
1267
1462
  Returns:
1268
- Dict avec status et service
1463
+ Dict with status and service
1269
1464
 
1270
1465
  Example:
1271
1466
  >>> status = client.healthcheck_afnor()
@@ -1285,16 +1480,16 @@ class FactPulseClient:
1285
1480
  json_data: Optional[Dict] = None,
1286
1481
  params: Optional[Dict] = None,
1287
1482
  ) -> requests.Response:
1288
- """Effectue une requête vers l'API Chorus Pro avec gestion d'auth et d'erreurs.
1483
+ """Perform a request to the Chorus Pro API with auth and error handling.
1289
1484
 
1290
1485
  Args:
1291
- method: Méthode HTTP (GET, POST, etc.)
1292
- endpoint: Endpoint relatif (ex: /structures/rechercher)
1293
- json_data: Données JSON (optionnel)
1294
- params: Query params (optionnel)
1486
+ method: HTTP method (GET, POST, etc.)
1487
+ endpoint: Relative endpoint (e.g., /structures/rechercher)
1488
+ json_data: JSON data (optional)
1489
+ params: Query params (optional)
1295
1490
 
1296
1491
  Returns:
1297
- Response de l'API
1492
+ API response
1298
1493
  """
1299
1494
  from .exceptions import (
1300
1495
  parse_api_error,
@@ -1306,7 +1501,7 @@ class FactPulseClient:
1306
1501
 
1307
1502
  headers = {"Authorization": f"Bearer {self._access_token}"}
1308
1503
 
1309
- # Ajouter credentials dans le body si mode zero-trust
1504
+ # Add credentials to body if zero-trust mode
1310
1505
  if json_data is None:
1311
1506
  json_data = {}
1312
1507
  if self.chorus_credentials:
@@ -1323,371 +1518,398 @@ class FactPulseClient:
1323
1518
  try:
1324
1519
  error_json = response.json()
1325
1520
  except Exception:
1326
- error_json = {"errorMessage": response.text or f"Erreur HTTP {response.status_code}"}
1521
+ error_json = {"errorMessage": response.text or f"HTTP error {response.status_code}"}
1327
1522
  raise parse_api_error(error_json, response.status_code)
1328
1523
 
1329
1524
  return response
1330
1525
 
1331
- def rechercher_structure_chorus(
1526
+ def search_structure_chorus(
1332
1527
  self,
1333
- identifiant_structure: Optional[str] = None,
1334
- raison_sociale: Optional[str] = None,
1335
- type_identifiant: str = "SIRET",
1336
- restreindre_privees: bool = True,
1528
+ structure_identifier: Optional[str] = None,
1529
+ company_name: Optional[str] = None,
1530
+ identifier_type: str = "SIRET",
1531
+ restrict_to_private: bool = True,
1337
1532
  ) -> Dict[str, Any]:
1338
- """Recherche des structures sur Chorus Pro.
1533
+ """Search structures on Chorus Pro.
1339
1534
 
1340
1535
  Args:
1341
- identifiant_structure: SIRET ou SIREN de la structure
1342
- raison_sociale: Raison sociale (recherche partielle)
1343
- type_identifiant: Type d'identifiant (SIRET, SIREN, etc.)
1344
- restreindre_privees: Si True, limite aux structures privées
1536
+ structure_identifier: Structure SIRET or SIREN
1537
+ company_name: Company name (partial search)
1538
+ identifier_type: Identifier type (SIRET, SIREN, etc.)
1539
+ restrict_to_private: If True, limit to private structures
1345
1540
 
1346
1541
  Returns:
1347
- Dict avec liste_structures, total, code_retour, libelle
1542
+ Dict with liste_structures, total, code_retour, libelle
1348
1543
 
1349
1544
  Example:
1350
- >>> result = client.rechercher_structure_chorus(identifiant_structure="12345678901234")
1545
+ >>> result = client.search_structure_chorus(structure_identifier="12345678901234")
1351
1546
  >>> for struct in result["liste_structures"]:
1352
1547
  ... print(struct["id_structure_cpp"], struct["designation_structure"])
1353
1548
  """
1354
1549
  body = {
1355
- "restreindre_structures_privees": restreindre_privees,
1550
+ "restreindre_structures_privees": restrict_to_private,
1356
1551
  }
1357
- if identifiant_structure:
1358
- body["identifiant_structure"] = identifiant_structure
1359
- if raison_sociale:
1360
- body["raison_sociale_structure"] = raison_sociale
1361
- if type_identifiant:
1362
- body["type_identifiant_structure"] = type_identifiant
1552
+ if structure_identifier:
1553
+ body["identifiant_structure"] = structure_identifier
1554
+ if company_name:
1555
+ body["raison_sociale_structure"] = company_name
1556
+ if identifier_type:
1557
+ body["type_identifiant_structure"] = identifier_type
1363
1558
 
1364
1559
  response = self._make_chorus_request("POST", "/structures/rechercher", json_data=body)
1365
1560
  return response.json()
1366
1561
 
1367
- def consulter_structure_chorus(self, id_structure_cpp: int) -> Dict[str, Any]:
1368
- """Consulte les détails d'une structure Chorus Pro.
1369
1562
 
1370
- Retourne notamment les paramètres obligatoires pour soumettre une facture :
1563
+ def get_structure_details_chorus(self, structure_cpp_id: int) -> Dict[str, Any]:
1564
+ """Get details of a Chorus Pro structure.
1565
+
1566
+ Returns mandatory parameters for submitting an invoice:
1371
1567
  - code_service_doit_etre_renseigne
1372
1568
  - numero_ej_doit_etre_renseigne
1373
1569
 
1374
1570
  Args:
1375
- id_structure_cpp: ID Chorus Pro de la structure
1571
+ structure_cpp_id: Chorus Pro structure ID
1376
1572
 
1377
1573
  Returns:
1378
- Dict avec les détails de la structure et ses paramètres
1574
+ Dict with structure details and parameters
1379
1575
 
1380
1576
  Example:
1381
- >>> details = client.consulter_structure_chorus(12345)
1577
+ >>> details = client.get_structure_details_chorus(12345)
1382
1578
  >>> if details["parametres"]["code_service_doit_etre_renseigne"]:
1383
- ... print("Code service obligatoire")
1579
+ ... print("Service code required")
1384
1580
  """
1385
- body = {"id_structure_cpp": id_structure_cpp}
1581
+ body = {"id_structure_cpp": structure_cpp_id}
1386
1582
  response = self._make_chorus_request("POST", "/structures/consulter", json_data=body)
1387
1583
  return response.json()
1388
1584
 
1389
- def obtenir_id_chorus_depuis_siret(
1585
+ def get_chorus_id_from_siret(
1390
1586
  self,
1391
1587
  siret: str,
1392
- type_identifiant: str = "SIRET",
1588
+ identifier_type: str = "SIRET",
1393
1589
  ) -> Dict[str, Any]:
1394
- """Obtient l'ID Chorus Pro d'une structure depuis son SIRET.
1590
+ """Get Chorus Pro ID from SIRET.
1395
1591
 
1396
- Raccourci pratique pour obtenir l'id_structure_cpp avant de soumettre une facture.
1592
+ Convenient shortcut to get id_structure_cpp before submitting an invoice.
1397
1593
 
1398
1594
  Args:
1399
- siret: Numéro SIRET ou SIREN
1400
- type_identifiant: Type d'identifiant (SIRET ou SIREN)
1595
+ siret: SIRET or SIREN number
1596
+ identifier_type: Identifier type (SIRET or SIREN)
1401
1597
 
1402
1598
  Returns:
1403
- Dict avec id_structure_cpp, designation_structure, message
1599
+ Dict with id_structure_cpp, designation_structure, message
1404
1600
 
1405
1601
  Example:
1406
- >>> result = client.obtenir_id_chorus_depuis_siret("12345678901234")
1602
+ >>> result = client.get_chorus_id_from_siret("12345678901234")
1407
1603
  >>> id_cpp = result["id_structure_cpp"]
1408
1604
  >>> if id_cpp > 0:
1409
- ... print(f"Structure trouvée: {result['designation_structure']}")
1605
+ ... print(f"Structure found: {result['designation_structure']}")
1410
1606
  """
1411
1607
  body = {
1412
1608
  "siret": siret,
1413
- "type_identifiant": type_identifiant,
1609
+ "type_identifiant": identifier_type,
1414
1610
  }
1415
1611
  response = self._make_chorus_request("POST", "/structures/obtenir-id-depuis-siret", json_data=body)
1416
1612
  return response.json()
1417
1613
 
1418
- def lister_services_structure_chorus(self, id_structure_cpp: int) -> Dict[str, Any]:
1419
- """Liste les services d'une structure Chorus Pro.
1614
+ def list_structure_services_chorus(self, structure_cpp_id: int) -> Dict[str, Any]:
1615
+ """List services of a Chorus Pro structure.
1420
1616
 
1421
1617
  Args:
1422
- id_structure_cpp: ID Chorus Pro de la structure
1618
+ structure_cpp_id: Chorus Pro structure ID
1423
1619
 
1424
1620
  Returns:
1425
- Dict avec liste_services, total, code_retour, libelle
1621
+ Dict with liste_services, total, code_retour, libelle
1426
1622
 
1427
1623
  Example:
1428
- >>> services = client.lister_services_structure_chorus(12345)
1624
+ >>> services = client.list_structure_services_chorus(12345)
1429
1625
  >>> for svc in services["liste_services"]:
1430
1626
  ... if svc["est_actif"]:
1431
1627
  ... print(svc["code_service"], svc["libelle_service"])
1432
1628
  """
1433
- response = self._make_chorus_request("GET", f"/structures/{id_structure_cpp}/services")
1629
+ response = self._make_chorus_request("GET", f"/structures/{structure_cpp_id}/services")
1434
1630
  return response.json()
1435
1631
 
1436
- def soumettre_facture_chorus(
1632
+ def submit_invoice_chorus(
1437
1633
  self,
1438
- numero_facture: str,
1439
- date_facture: str,
1440
- date_echeance_paiement: str,
1441
- id_structure_cpp: int,
1442
- montant_ht_total: str,
1443
- montant_tva: str,
1444
- montant_ttc_total: str,
1445
- piece_jointe_principale_id: Optional[int] = None,
1446
- piece_jointe_principale_designation: str = "Facture",
1447
- code_service: Optional[str] = None,
1448
- numero_engagement: Optional[str] = None,
1449
- numero_bon_commande: Optional[str] = None,
1450
- numero_marche: Optional[str] = None,
1451
- commentaire: Optional[str] = None,
1634
+ invoice_number: str,
1635
+ invoice_date: str,
1636
+ due_date: str,
1637
+ structure_cpp_id: int,
1638
+ total_excl_tax: str,
1639
+ total_vat: str,
1640
+ total_incl_tax: str,
1641
+ main_attachment_id: Optional[int] = None,
1642
+ main_attachment_name: str = "Invoice",
1643
+ service_code: Optional[str] = None,
1644
+ commitment_number: Optional[str] = None,
1645
+ purchase_order_number: Optional[str] = None,
1646
+ contract_number: Optional[str] = None,
1647
+ comment: Optional[str] = None,
1452
1648
  ) -> Dict[str, Any]:
1453
- """Soumet une facture à Chorus Pro.
1649
+ """Submit an invoice to Chorus Pro.
1454
1650
 
1455
- **Workflow complet** :
1456
- 1. Obtenir l'id_structure_cpp via rechercher_structure_chorus()
1457
- 2. Vérifier les paramètres obligatoires via consulter_structure_chorus()
1458
- 3. Uploader le PDF via l'API /transverses/ajouter-fichier
1459
- 4. Soumettre la facture avec cette méthode
1651
+ **Complete workflow**:
1652
+ 1. Get id_structure_cpp via search_structure_chorus()
1653
+ 2. Check mandatory parameters via get_structure_details_chorus()
1654
+ 3. Upload PDF via /transverses/ajouter-fichier API
1655
+ 4. Submit invoice with this method
1460
1656
 
1461
1657
  Args:
1462
- numero_facture: Numéro de la facture
1463
- date_facture: Date de la facture (YYYY-MM-DD)
1464
- date_echeance_paiement: Date d'échéance (YYYY-MM-DD)
1465
- id_structure_cpp: ID Chorus Pro du destinataire
1466
- montant_ht_total: Montant HT total (ex: "1000.00")
1467
- montant_tva: Montant TVA (ex: "200.00")
1468
- montant_ttc_total: Montant TTC total (ex: "1200.00")
1469
- piece_jointe_principale_id: ID de la pièce jointe (optionnel)
1470
- piece_jointe_principale_designation: Désignation (défaut: "Facture")
1471
- code_service: Code service (si requis par la structure)
1472
- numero_engagement: Numéro d'engagement (si requis)
1473
- numero_bon_commande: Numéro de bon de commande
1474
- numero_marche: Numéro de marché
1475
- commentaire: Commentaire libre
1658
+ invoice_number: Invoice number
1659
+ invoice_date: Invoice date (YYYY-MM-DD)
1660
+ due_date: Due date (YYYY-MM-DD)
1661
+ structure_cpp_id: Chorus Pro recipient ID
1662
+ total_excl_tax: Total excl. tax (e.g., "1000.00")
1663
+ total_vat: VAT amount (e.g., "200.00")
1664
+ total_incl_tax: Total incl. tax (e.g., "1200.00")
1665
+ main_attachment_id: Attachment ID (optional)
1666
+ main_attachment_name: Attachment name (default: "Invoice")
1667
+ service_code: Service code (if required by structure)
1668
+ commitment_number: Commitment number (if required)
1669
+ purchase_order_number: Purchase order number
1670
+ contract_number: Contract number
1671
+ comment: Free comment
1476
1672
 
1477
1673
  Returns:
1478
- Dict avec identifiant_facture_cpp, numero_flux_depot, code_retour, libelle
1674
+ Dict with identifiant_facture_cpp, numero_flux_depot, code_retour, libelle
1479
1675
 
1480
1676
  Example:
1481
- >>> result = client.soumettre_facture_chorus(
1482
- ... numero_facture="FAC-2025-001",
1483
- ... date_facture="2025-01-15",
1484
- ... date_echeance_paiement="2025-02-15",
1485
- ... id_structure_cpp=12345,
1486
- ... montant_ht_total="1000.00",
1487
- ... montant_tva="200.00",
1488
- ... montant_ttc_total="1200.00",
1677
+ >>> result = client.submit_invoice_chorus(
1678
+ ... invoice_number="INV-2025-001",
1679
+ ... invoice_date="2025-01-15",
1680
+ ... due_date="2025-02-15",
1681
+ ... structure_cpp_id=12345,
1682
+ ... total_excl_tax="1000.00",
1683
+ ... total_vat="200.00",
1684
+ ... total_incl_tax="1200.00",
1489
1685
  ... )
1490
- >>> print(f"Facture soumise: {result['identifiant_facture_cpp']}")
1686
+ >>> print(f"Invoice submitted: {result['identifiant_facture_cpp']}")
1491
1687
  """
1492
1688
  body = {
1493
- "numero_facture": numero_facture,
1494
- "date_facture": date_facture,
1495
- "date_echeance_paiement": date_echeance_paiement,
1496
- "id_structure_cpp": id_structure_cpp,
1497
- "montant_ht_total": montant_ht_total,
1498
- "montant_tva": montant_tva,
1499
- "montant_ttc_total": montant_ttc_total,
1689
+ "numero_facture": invoice_number,
1690
+ "date_facture": invoice_date,
1691
+ "date_echeance_paiement": due_date,
1692
+ "id_structure_cpp": structure_cpp_id,
1693
+ "montant_ht_total": total_excl_tax,
1694
+ "montant_tva": total_vat,
1695
+ "montant_ttc_total": total_incl_tax,
1500
1696
  }
1501
- if piece_jointe_principale_id:
1502
- body["piece_jointe_principale_id"] = piece_jointe_principale_id
1503
- body["piece_jointe_principale_designation"] = piece_jointe_principale_designation
1504
- if code_service:
1505
- body["code_service"] = code_service
1506
- if numero_engagement:
1507
- body["numero_engagement"] = numero_engagement
1508
- if numero_bon_commande:
1509
- body["numero_bon_commande"] = numero_bon_commande
1510
- if numero_marche:
1511
- body["numero_marche"] = numero_marche
1512
- if commentaire:
1513
- body["commentaire"] = commentaire
1697
+ if main_attachment_id:
1698
+ body["piece_jointe_principale_id"] = main_attachment_id
1699
+ body["piece_jointe_principale_designation"] = main_attachment_name
1700
+ if service_code:
1701
+ body["code_service"] = service_code
1702
+ if commitment_number:
1703
+ body["numero_engagement"] = commitment_number
1704
+ if purchase_order_number:
1705
+ body["numero_bon_commande"] = purchase_order_number
1706
+ if contract_number:
1707
+ body["numero_marche"] = contract_number
1708
+ if comment:
1709
+ body["commentaire"] = comment
1514
1710
 
1515
1711
  response = self._make_chorus_request("POST", "/factures/soumettre", json_data=body)
1516
1712
  return response.json()
1517
1713
 
1518
- def consulter_facture_chorus(self, identifiant_facture_cpp: int) -> Dict[str, Any]:
1519
- """Consulte le statut d'une facture Chorus Pro.
1714
+ def get_invoice_status_chorus(self, invoice_cpp_id: int) -> Dict[str, Any]:
1715
+ """Get status of a Chorus Pro invoice.
1520
1716
 
1521
1717
  Args:
1522
- identifiant_facture_cpp: ID Chorus Pro de la facture
1718
+ invoice_cpp_id: Chorus Pro invoice ID
1523
1719
 
1524
1720
  Returns:
1525
- Dict avec statut_courant, numero_facture, date_facture, montant_ttc_total, etc.
1721
+ Dict with statut_courant, numero_facture, date_facture, montant_ttc_total, etc.
1526
1722
 
1527
1723
  Example:
1528
- >>> status = client.consulter_facture_chorus(12345)
1529
- >>> print(f"Statut: {status['statut_courant']['code']}")
1724
+ >>> status = client.get_invoice_status_chorus(12345)
1725
+ >>> print(f"Status: {status['statut_courant']['code']}")
1530
1726
  """
1531
- body = {"identifiant_facture_cpp": identifiant_facture_cpp}
1727
+ body = {"identifiant_facture_cpp": invoice_cpp_id}
1532
1728
  response = self._make_chorus_request("POST", "/factures/consulter", json_data=body)
1533
1729
  return response.json()
1534
1730
 
1535
1731
  # ==================== AFNOR Directory ====================
1536
1732
 
1537
- def rechercher_siret_afnor(self, siret: str) -> Dict[str, Any]:
1538
- """Recherche une entreprise par SIRET dans l'annuaire AFNOR.
1733
+ def search_siret_afnor(self, siret: str) -> Dict[str, Any]:
1734
+ """Search a company by SIRET in the AFNOR directory.
1539
1735
 
1540
1736
  Args:
1541
- siret: Numéro SIRET (14 chiffres)
1737
+ siret: SIRET number (14 digits)
1542
1738
 
1543
1739
  Returns:
1544
- Dict avec informations entreprise: raison_sociale, adresse, etc.
1740
+ Dict with company info: company_name, address, etc.
1545
1741
 
1546
1742
  Example:
1547
- >>> result = client.rechercher_siret_afnor("12345678901234")
1548
- >>> print(f"Entreprise: {result['raison_sociale']}")
1743
+ >>> result = client.search_siret_afnor("12345678901234")
1744
+ >>> print(f"Company: {result['raison_sociale']}")
1549
1745
  """
1550
1746
  response = self._make_afnor_request("GET", f"/directory/siret/{siret}")
1551
1747
  return response.json()
1552
1748
 
1553
- def rechercher_siren_afnor(self, siren: str) -> Dict[str, Any]:
1554
- """Recherche une entreprise par SIREN dans l'annuaire AFNOR.
1749
+
1750
+ def search_siren_afnor(self, siren: str) -> Dict[str, Any]:
1751
+ """Search a company by SIREN in the AFNOR directory.
1555
1752
 
1556
1753
  Args:
1557
- siren: Numéro SIREN (9 chiffres)
1754
+ siren: SIREN number (9 digits)
1558
1755
 
1559
1756
  Returns:
1560
- Dict avec informations entreprise et liste des établissements
1757
+ Dict with company info and list of establishments
1561
1758
 
1562
1759
  Example:
1563
- >>> result = client.rechercher_siren_afnor("123456789")
1564
- >>> for etab in result.get('etablissements', []):
1565
- ... print(f"SIRET: {etab['siret']}")
1760
+ >>> result = client.search_siren_afnor("123456789")
1761
+ >>> for estab in result.get('etablissements', []):
1762
+ ... print(f"SIRET: {estab['siret']}")
1566
1763
  """
1567
1764
  response = self._make_afnor_request("GET", f"/directory/siren/{siren}")
1568
1765
  return response.json()
1569
1766
 
1570
- def lister_codes_routage_afnor(self, siren: str) -> List[Dict[str, Any]]:
1571
- """Liste les codes de routage disponibles pour un SIREN.
1767
+
1768
+ def list_routing_codes_afnor(self, siren: str) -> List[Dict[str, Any]]:
1769
+ """List available routing codes for a SIREN.
1572
1770
 
1573
1771
  Args:
1574
- siren: Numéro SIREN (9 chiffres)
1772
+ siren: SIREN number (9 digits)
1575
1773
 
1576
1774
  Returns:
1577
- Liste des codes de routage avec leurs paramètres
1775
+ List of routing codes with their parameters
1578
1776
 
1579
1777
  Example:
1580
- >>> codes = client.lister_codes_routage_afnor("123456789")
1778
+ >>> codes = client.list_routing_codes_afnor("123456789")
1581
1779
  >>> for code in codes:
1582
1780
  ... print(f"Code: {code['code_routage']}")
1583
1781
  """
1584
1782
  response = self._make_afnor_request("GET", f"/directory/siren/{siren}/routing-codes")
1585
1783
  return response.json()
1586
1784
 
1785
+
1587
1786
  # ==================== Validation ====================
1588
1787
 
1589
- def valider_pdf_facturx(
1788
+ def validate_facturx_pdf(
1590
1789
  self,
1591
1790
  pdf_path: Optional[str] = None,
1592
1791
  pdf_bytes: Optional[bytes] = None,
1593
- profil: str = "EN16931"
1792
+ profile: Optional[str] = None,
1793
+ use_verapdf: bool = False,
1594
1794
  ) -> Dict[str, Any]:
1595
- """Valide un PDF Factur-X.
1795
+ """Validate a Factur-X PDF.
1596
1796
 
1597
1797
  Args:
1598
- pdf_path: Chemin vers le fichier PDF (exclusif avec pdf_bytes)
1599
- pdf_bytes: Contenu PDF en bytes (exclusif avec pdf_path)
1600
- profil: Profil Factur-X attendu (MINIMUM, BASIC, EN16931, EXTENDED)
1798
+ pdf_path: Path to PDF file (exclusive with pdf_bytes)
1799
+ pdf_bytes: PDF content as bytes (exclusive with pdf_path)
1800
+ profile: Expected Factur-X profile (MINIMUM, BASIC, EN16931, EXTENDED).
1801
+ If None, profile is auto-detected from embedded XML.
1802
+ use_verapdf: Enable strict PDF/A validation with VeraPDF (default: False).
1803
+ - False: Fast metadata validation (~100ms)
1804
+ - True: Strict ISO 19005 validation with 146+ rules (2-10s, recommended in production)
1601
1805
 
1602
1806
  Returns:
1603
- Dict avec: est_conforme (bool), erreurs (list), avertissements (list), profil_detecte
1807
+ Dict with:
1808
+ - is_compliant (bool): True if PDF is compliant
1809
+ - xml_present (bool): True if Factur-X XML is embedded
1810
+ - xml_compliant (bool): True if XML is valid according to Schematron
1811
+ - detected_profile (str): Detected profile (MINIMUM, BASIC, EN16931, EXTENDED)
1812
+ - xml_errors (list): XML validation errors
1813
+ - pdfa_compliant (bool): True if PDF/A compliant
1814
+ - pdfa_version (str): Detected PDF/A version (e.g., "PDF/A-3B")
1815
+ - pdfa_validation_method (str): "metadata" or "verapdf"
1816
+ - pdfa_errors (list): PDF/A compliance errors
1604
1817
 
1605
1818
  Example:
1606
- >>> result = client.valider_pdf_facturx("facture.pdf")
1607
- >>> if result['est_conforme']:
1608
- ... print("PDF Factur-X valide!")
1819
+ >>> # Validation with auto-detected profile
1820
+ >>> result = client.validate_facturx_pdf("invoice.pdf")
1821
+ >>> print(f"Detected profile: {result['detected_profile']}")
1822
+
1823
+ >>> # Strict validation with VeraPDF (recommended in production)
1824
+ >>> result = client.validate_facturx_pdf("invoice.pdf", use_verapdf=True)
1825
+ >>> if result['is_compliant']:
1826
+ ... print("Valid Factur-X PDF!")
1609
1827
  >>> else:
1610
- ... for err in result['erreurs']:
1611
- ... print(f"Erreur: {err}")
1828
+ ... for err in result.get('pdfa_errors', []):
1829
+ ... print(f"PDF/A error: {err}")
1612
1830
  """
1613
1831
  if pdf_path:
1614
1832
  with open(pdf_path, "rb") as f:
1615
1833
  pdf_bytes = f.read()
1616
1834
  if not pdf_bytes:
1617
- raise ValueError("pdf_path ou pdf_bytes requis")
1835
+ raise ValueError("pdf_path or pdf_bytes required")
1618
1836
 
1619
- files = {"fichier_pdf": ("facture.pdf", pdf_bytes, "application/pdf")}
1620
- data = {"profil": profil}
1621
- response = self._request("POST", "/traitement/valider-pdf-facturx", files=files, data=data)
1837
+ files = {"pdf_file": ("invoice.pdf", pdf_bytes, "application/pdf")}
1838
+ data: Dict[str, Any] = {"use_verapdf": str(use_verapdf).lower()}
1839
+ if profile:
1840
+ data["profile"] = profile
1841
+ response = self._request("POST", "/processing/validate-facturx-pdf", files=files, data=data)
1622
1842
  return response.json()
1623
1843
 
1624
- def valider_signature_pdf(
1844
+
1845
+ def validate_pdf_signature(
1625
1846
  self,
1626
1847
  pdf_path: Optional[str] = None,
1627
1848
  pdf_bytes: Optional[bytes] = None
1628
1849
  ) -> Dict[str, Any]:
1629
- """Valide la signature d'un PDF signé.
1850
+ """Validate the signature of a signed PDF.
1630
1851
 
1631
1852
  Args:
1632
- pdf_path: Chemin vers le fichier PDF signé
1633
- pdf_bytes: Contenu PDF en bytes
1853
+ pdf_path: Path to signed PDF file
1854
+ pdf_bytes: PDF content as bytes
1634
1855
 
1635
1856
  Returns:
1636
- Dict avec: is_signed (bool), signatures (list), etc.
1857
+ Dict with: is_signed (bool), signatures (list), etc.
1637
1858
 
1638
1859
  Example:
1639
- >>> result = client.valider_signature_pdf("facture_signee.pdf")
1860
+ >>> result = client.validate_pdf_signature("signed_invoice.pdf")
1640
1861
  >>> if result['is_signed']:
1641
- ... print("PDF signé!")
1862
+ ... print("PDF is signed!")
1642
1863
  ... for sig in result.get('signatures', []):
1643
- ... print(f"Signé par: {sig.get('signer_cn')}")
1864
+ ... print(f"Signed by: {sig.get('signer_cn')}")
1644
1865
  """
1645
1866
  if pdf_path:
1646
1867
  with open(pdf_path, "rb") as f:
1647
1868
  pdf_bytes = f.read()
1648
1869
  if not pdf_bytes:
1649
- raise ValueError("pdf_path ou pdf_bytes requis")
1870
+ raise ValueError("pdf_path or pdf_bytes required")
1650
1871
 
1651
- files = {"fichier_pdf": ("document.pdf", pdf_bytes, "application/pdf")}
1652
- response = self._request("POST", "/traitement/valider-signature-pdf", files=files)
1872
+ files = {"pdf_file": ("document.pdf", pdf_bytes, "application/pdf")}
1873
+ response = self._request("POST", "/processing/validate-pdf-signature", files=files)
1653
1874
  return response.json()
1654
1875
 
1876
+
1655
1877
  # ==================== Signature ====================
1656
1878
 
1657
- def signer_pdf(
1879
+ def sign_pdf(
1658
1880
  self,
1659
1881
  pdf_path: Optional[str] = None,
1660
1882
  pdf_bytes: Optional[bytes] = None,
1661
- raison: Optional[str] = None,
1662
- localisation: Optional[str] = None,
1883
+ reason: Optional[str] = None,
1884
+ location: Optional[str] = None,
1663
1885
  contact: Optional[str] = None,
1664
1886
  use_pades_lt: bool = False,
1665
1887
  use_timestamp: bool = True,
1666
1888
  output_path: Optional[str] = None
1667
1889
  ) -> Union[bytes, str]:
1668
- """Signe un PDF avec le certificat configuré côté serveur.
1890
+ """Sign a PDF with the server-side configured certificate.
1669
1891
 
1670
- Le certificat doit être préalablement configuré dans Django Admin
1671
- pour le client identifié par le client_uid du JWT.
1892
+ The certificate must be pre-configured in Django Admin
1893
+ for the client identified by the JWT client_uid.
1672
1894
 
1673
1895
  Args:
1674
- pdf_path: Chemin vers le PDF à signer
1675
- pdf_bytes: Contenu PDF en bytes
1676
- raison: Raison de la signature (optionnel)
1677
- localisation: Lieu de signature (optionnel)
1678
- contact: Email de contact (optionnel)
1679
- use_pades_lt: Activer PAdES-B-LT archivage long terme (défaut: False)
1680
- use_timestamp: Activer l'horodatage RFC 3161 (défaut: True)
1681
- output_path: Si fourni, sauvegarde le PDF signé à ce chemin
1896
+ pdf_path: Path to PDF to sign
1897
+ pdf_bytes: PDF content as bytes
1898
+ reason: Signature reason (optional)
1899
+ location: Signature location (optional)
1900
+ contact: Contact email (optional)
1901
+ use_pades_lt: Enable PAdES-B-LT for long-term archiving (default: False)
1902
+ use_timestamp: Enable RFC 3161 timestamping (default: True)
1903
+ output_path: If provided, save signed PDF to this path
1682
1904
 
1683
1905
  Returns:
1684
- bytes du PDF signé, ou chemin si output_path fourni
1906
+ Signed PDF bytes, or path if output_path provided
1685
1907
 
1686
1908
  Example:
1687
- >>> pdf_signe = client.signer_pdf(
1688
- ... pdf_path="facture.pdf",
1689
- ... raison="Conformité Factur-X",
1690
- ... output_path="facture_signee.pdf"
1909
+ >>> signed_pdf = client.sign_pdf(
1910
+ ... pdf_path="invoice.pdf",
1911
+ ... reason="Factur-X Compliance",
1912
+ ... output_path="signed_invoice.pdf"
1691
1913
  ... )
1692
1914
  """
1693
1915
  if pdf_path:
@@ -1695,67 +1917,68 @@ class FactPulseClient:
1695
1917
  pdf_bytes = f.read()
1696
1918
 
1697
1919
  if not pdf_bytes:
1698
- raise ValueError("pdf_path ou pdf_bytes requis")
1920
+ raise ValueError("pdf_path or pdf_bytes required")
1699
1921
 
1700
1922
  files = {
1701
- "fichier_pdf": ("document.pdf", pdf_bytes, "application/pdf"),
1923
+ "pdf_file": ("document.pdf", pdf_bytes, "application/pdf"),
1702
1924
  }
1703
1925
  data: Dict[str, Any] = {
1704
1926
  "use_pades_lt": str(use_pades_lt).lower(),
1705
1927
  "use_timestamp": str(use_timestamp).lower(),
1706
1928
  }
1707
- if raison:
1708
- data["raison"] = raison
1709
- if localisation:
1710
- data["localisation"] = localisation
1929
+ if reason:
1930
+ data["reason"] = reason
1931
+ if location:
1932
+ data["location"] = location
1711
1933
  if contact:
1712
1934
  data["contact"] = contact
1713
1935
 
1714
- response = self._request("POST", "/traitement/signer-pdf", files=files, data=data)
1936
+ response = self._request("POST", "/processing/sign-pdf", files=files, data=data)
1715
1937
  result = response.json()
1716
1938
 
1717
- # L'API retourne du JSON avec pdf_signe_base64
1718
- pdf_signe_b64 = result.get("pdf_signe_base64")
1719
- if not pdf_signe_b64:
1720
- raise FactPulseValidationError("Réponse de signature invalide")
1939
+ # API returns JSON with signed_pdf_base64
1940
+ pdf_signed_b64 = result.get("signed_pdf_base64")
1941
+ if not pdf_signed_b64:
1942
+ raise FactPulseValidationError("Invalid signature response")
1721
1943
 
1722
1944
  import base64
1723
- pdf_signe = base64.b64decode(pdf_signe_b64)
1945
+ pdf_signed = base64.b64decode(pdf_signed_b64)
1724
1946
 
1725
1947
  if output_path:
1726
1948
  with open(output_path, "wb") as f:
1727
- f.write(pdf_signe)
1949
+ f.write(pdf_signed)
1728
1950
  return output_path
1729
1951
 
1730
- return pdf_signe
1952
+ return pdf_signed
1731
1953
 
1732
- def generer_certificat_test(
1954
+
1955
+ def generate_test_certificate(
1733
1956
  self,
1734
1957
  cn: str = "Test Organisation",
1735
1958
  organisation: str = "Test Organisation",
1736
1959
  email: str = "test@example.com",
1737
- duree_jours: int = 365,
1738
- taille_cle: int = 2048,
1960
+ validity_days: int = 365,
1961
+ key_size: int = 2048,
1739
1962
  ) -> Dict[str, Any]:
1740
- """Génère un certificat de test pour la signature (NON PRODUCTION).
1963
+ """Generate a test certificate for signing (NOT FOR PRODUCTION).
1741
1964
 
1742
- Le certificat généré doit ensuite être configuré dans Django Admin.
1965
+ The generated certificate must then be configured in Django Admin.
1743
1966
 
1744
1967
  Args:
1745
- cn: Common Name du certificat
1746
- organisation: Nom de l'organisation
1747
- email: Email associé au certificat
1748
- duree_jours: Durée de validité en jours (défaut: 365)
1749
- taille_cle: Taille de la clé RSA (2048 ou 4096)
1968
+ cn: Certificate Common Name
1969
+ organisation: Organisation name
1970
+ email: Email associated with certificate
1971
+ validity_days: Validity duration in days (default: 365)
1972
+ key_size: RSA key size (2048 or 4096)
1750
1973
 
1751
1974
  Returns:
1752
- Dict avec certificat_pem, cle_privee_pem, pkcs12_base64, etc.
1975
+ Dict with certificat_pem, cle_privee_pem, pkcs12_base64, etc.
1753
1976
 
1754
1977
  Example:
1755
- >>> result = client.generer_certificat_test(
1756
- ... cn="Ma Société - Cachet",
1757
- ... organisation="Ma Société SAS",
1758
- ... email="contact@masociete.fr",
1978
+ >>> result = client.generate_test_certificate(
1979
+ ... cn="My Company - Seal",
1980
+ ... organisation="My Company SAS",
1981
+ ... email="contact@mycompany.com",
1759
1982
  ... )
1760
1983
  >>> print(result["certificat_pem"])
1761
1984
  """
@@ -1763,122 +1986,123 @@ class FactPulseClient:
1763
1986
  "cn": cn,
1764
1987
  "organisation": organisation,
1765
1988
  "email": email,
1766
- "duree_jours": duree_jours,
1767
- "taille_cle": taille_cle,
1989
+ "validity_days": validity_days,
1990
+ "key_size": key_size,
1768
1991
  }
1769
- response = self._request("POST", "/traitement/generer-certificat-test", json_data=data)
1992
+ response = self._request("POST", "/processing/generate-test-certificate", json_data=data)
1770
1993
  return response.json()
1771
1994
 
1772
- # ==================== Workflow complet ====================
1773
1995
 
1774
- def generer_facturx_complet(
1996
+ # ==================== Complete workflow ====================
1997
+
1998
+ def generate_complete_facturx(
1775
1999
  self,
1776
- facture: Dict[str, Any],
2000
+ invoice: Dict[str, Any],
1777
2001
  pdf_source_path: Optional[str] = None,
1778
2002
  pdf_source_bytes: Optional[bytes] = None,
1779
- profil: str = "EN16931",
1780
- valider: bool = True,
1781
- signer: bool = False,
1782
- soumettre_afnor: bool = False,
2003
+ profile: str = "EN16931",
2004
+ validate: bool = True,
2005
+ sign: bool = False,
2006
+ submit_afnor: bool = False,
1783
2007
  afnor_flow_name: Optional[str] = None,
1784
2008
  afnor_tracking_id: Optional[str] = None,
1785
2009
  output_path: Optional[str] = None,
1786
2010
  timeout: int = 120000
1787
2011
  ) -> Dict[str, Any]:
1788
- """Génère un PDF Factur-X complet avec validation, signature et soumission optionnelles.
2012
+ """Generate a complete Factur-X PDF with optional validation, signing and submission.
1789
2013
 
1790
- Cette méthode enchaîne automatiquement:
1791
- 1. Génération du PDF Factur-X
1792
- 2. Validation (optionnelle)
1793
- 3. Signature (optionnelle, utilise le certificat côté serveur)
1794
- 4. Soumission à la PDP AFNOR (optionnelle)
2014
+ This method automatically chains:
2015
+ 1. Factur-X PDF generation
2016
+ 2. Validation (optional)
2017
+ 3. Signing (optional, uses server-side certificate)
2018
+ 4. AFNOR PDP submission (optional)
1795
2019
 
1796
- Note: La signature utilise le certificat configuré dans Django Admin
1797
- pour le client identifié par le client_uid du JWT.
2020
+ Note: Signing uses the certificate configured in Django Admin
2021
+ for the client identified by the JWT client_uid.
1798
2022
 
1799
2023
  Args:
1800
- facture: Données de la facture (format FactureFacturX)
1801
- pdf_source_path: Chemin vers le PDF source
1802
- pdf_source_bytes: PDF source en bytes
1803
- profil: Profil Factur-X (MINIMUM, BASIC, EN16931, EXTENDED)
1804
- valider: Si True, valide le PDF généré
1805
- signer: Si True, signe le PDF (certificat côté serveur)
1806
- soumettre_afnor: Si True, soumet le PDF à la PDP AFNOR
1807
- afnor_flow_name: Nom du flux AFNOR (défaut: "Facture {numero_facture}")
1808
- afnor_tracking_id: Tracking ID AFNOR (défaut: numero_facture)
1809
- output_path: Chemin de sortie pour le PDF final
1810
- timeout: Timeout en ms pour le polling
2024
+ invoice: Invoice data (FactureFacturX format)
2025
+ pdf_source_path: Path to source PDF
2026
+ pdf_source_bytes: Source PDF as bytes
2027
+ profile: Factur-X profile (MINIMUM, BASIC, EN16931, EXTENDED)
2028
+ validate: If True, validate generated PDF
2029
+ sign: If True, sign PDF (server-side certificate)
2030
+ submit_afnor: If True, submit PDF to AFNOR PDP
2031
+ afnor_flow_name: AFNOR flow name (default: "Invoice {invoice_number}")
2032
+ afnor_tracking_id: AFNOR tracking ID (default: invoice_number)
2033
+ output_path: Output path for final PDF
2034
+ timeout: Polling timeout in ms
1811
2035
 
1812
2036
  Returns:
1813
- Dict avec:
1814
- - pdf_bytes: bytes du PDF final
1815
- - pdf_path: chemin si output_path fourni
1816
- - validation: résultat de validation si valider=True
1817
- - signature: infos signature si signer=True
1818
- - afnor: résultat soumission AFNOR si soumettre_afnor=True
2037
+ Dict with:
2038
+ - pdf_bytes: Final PDF bytes
2039
+ - pdf_path: Path if output_path provided
2040
+ - validation: Validation result if validate=True
2041
+ - signature: Signature info if sign=True
2042
+ - afnor: AFNOR submission result if submit_afnor=True
1819
2043
 
1820
2044
  Example:
1821
- >>> result = client.generer_facturx_complet(
1822
- ... facture=ma_facture,
1823
- ... pdf_source_path="devis.pdf",
1824
- ... profil="EN16931",
1825
- ... valider=True,
1826
- ... signer=True,
1827
- ... soumettre_afnor=True,
1828
- ... output_path="facture_finale.pdf"
2045
+ >>> result = client.generate_complete_facturx(
2046
+ ... invoice=my_invoice,
2047
+ ... pdf_source_path="quote.pdf",
2048
+ ... profile="EN16931",
2049
+ ... validate=True,
2050
+ ... sign=True,
2051
+ ... submit_afnor=True,
2052
+ ... output_path="final_invoice.pdf"
1829
2053
  ... )
1830
- >>> if result['validation']['valide']:
1831
- ... print(f"Facture soumise! Flow ID: {result['afnor']['flowId']}")
2054
+ >>> if result['validation']['is_compliant']:
2055
+ ... print(f"Invoice submitted! Flow ID: {result['afnor']['flowId']}")
1832
2056
  """
1833
2057
  result: Dict[str, Any] = {}
1834
2058
 
1835
- # 1. Génération
2059
+ # 1. Generation
1836
2060
  if pdf_source_path:
1837
2061
  with open(pdf_source_path, "rb") as f:
1838
2062
  pdf_source_bytes = f.read()
1839
2063
 
1840
- pdf_bytes = self.generer_facturx(
1841
- facture_data=facture,
2064
+ pdf_bytes = self.generate_facturx(
2065
+ invoice_data=invoice,
1842
2066
  pdf_source=pdf_source_bytes,
1843
- profil=profil,
2067
+ profile=profile,
1844
2068
  timeout=timeout
1845
2069
  )
1846
2070
  result["pdf_bytes"] = pdf_bytes
1847
2071
 
1848
2072
  # 2. Validation
1849
- if valider:
1850
- validation = self.valider_pdf_facturx(pdf_bytes=pdf_bytes, profil=profil)
2073
+ if validate:
2074
+ validation = self.validate_facturx_pdf(pdf_bytes=pdf_bytes, profile=profile)
1851
2075
  result["validation"] = validation
1852
- if not validation.get("est_conforme", False):
1853
- # Retourne quand même le résultat mais avec les erreurs
2076
+ if not validation.get("est_conforme", False) and not validation.get("is_compliant", False):
2077
+ # Return result with errors
1854
2078
  if output_path:
1855
2079
  with open(output_path, "wb") as f:
1856
2080
  f.write(pdf_bytes)
1857
2081
  result["pdf_path"] = output_path
1858
2082
  return result
1859
2083
 
1860
- # 3. Signature (utilise le certificat côté serveur)
1861
- if signer:
1862
- pdf_bytes = self.signer_pdf(pdf_bytes=pdf_bytes)
2084
+ # 3. Signing (uses server-side certificate)
2085
+ if sign:
2086
+ pdf_bytes = self.sign_pdf(pdf_bytes=pdf_bytes)
1863
2087
  result["pdf_bytes"] = pdf_bytes
1864
- result["signature"] = {"signe": True}
2088
+ result["signature"] = {"signed": True}
1865
2089
 
1866
- # 4. Soumission AFNOR
1867
- if soumettre_afnor:
1868
- numero_facture = facture.get("numeroFacture", facture.get("numero_facture", "FACTURE"))
1869
- flow_name = afnor_flow_name or f"Facture {numero_facture}"
1870
- tracking_id = afnor_tracking_id or numero_facture
2090
+ # 4. AFNOR submission
2091
+ if submit_afnor:
2092
+ invoice_number = invoice.get("invoiceNumber", invoice.get("numeroFacture", invoice.get("numero_facture", "INVOICE")))
2093
+ flow_name = afnor_flow_name or f"Invoice {invoice_number}"
2094
+ tracking_id = afnor_tracking_id or invoice_number
1871
2095
 
1872
- # Soumission directe avec bytes (plus de fichier temporaire nécessaire)
1873
- afnor_result = self.soumettre_facture_afnor(
2096
+ # Direct submission with bytes (no temp file needed)
2097
+ afnor_result = self.submit_invoice_afnor(
1874
2098
  flow_name=flow_name,
1875
2099
  pdf_bytes=pdf_bytes,
1876
- pdf_filename=f"{numero_facture}.pdf",
2100
+ pdf_filename=f"{invoice_number}.pdf",
1877
2101
  tracking_id=tracking_id,
1878
2102
  )
1879
2103
  result["afnor"] = afnor_result
1880
2104
 
1881
- # Sauvegarde finale
2105
+ # Final save
1882
2106
  if output_path:
1883
2107
  with open(output_path, "wb") as f:
1884
2108
  f.write(pdf_bytes)