factpulse 2.0.37__py3-none-any.whl → 3.0.23__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 (400) hide show
  1. factpulse/__init__.py +542 -197
  2. factpulse/api/__init__.py +7 -4
  3. factpulse/api/afnorpdppa_api.py +49 -47
  4. factpulse/api/afnorpdppa_directory_service_api.py +1225 -310
  5. factpulse/api/afnorpdppa_flow_service_api.py +212 -81
  6. factpulse/api/chorus_pro_api.py +271 -218
  7. factpulse/api/document_conversion_api.py +1222 -0
  8. factpulse/api/downloads_api.py +1171 -0
  9. factpulse/api/e_reporting_api.py +3254 -0
  10. factpulse/api/{sant_api.py → health_api.py} +29 -22
  11. factpulse/api/invoice_processing_api.py +3634 -0
  12. factpulse/api/{vrification_pdfxml_api.py → pdfxml_verification_api.py} +331 -252
  13. factpulse/api/{utilisateur_api.py → user_api.py} +21 -17
  14. factpulse/api_client.py +4 -3
  15. factpulse/configuration.py +11 -6
  16. factpulse/exceptions.py +3 -2
  17. factpulse/models/__init__.py +265 -95
  18. factpulse/models/accept_language.py +38 -0
  19. factpulse/models/acknowledgment_status.py +39 -0
  20. factpulse/models/additional_document.py +116 -0
  21. factpulse/models/afnor_acknowledgement.py +100 -0
  22. factpulse/models/afnor_acknowledgement_detail.py +105 -0
  23. factpulse/models/afnor_address_edit.py +111 -0
  24. factpulse/models/afnor_address_patch.py +141 -0
  25. factpulse/models/afnor_address_put.py +129 -0
  26. factpulse/models/afnor_address_read.py +113 -0
  27. factpulse/models/afnor_algorithm.py +41 -0
  28. factpulse/models/afnor_contains_operator.py +37 -0
  29. factpulse/models/afnor_create_directory_line_body.py +98 -0
  30. factpulse/models/afnor_create_directory_line_body_addressing_information.py +122 -0
  31. factpulse/models/afnor_create_directory_line_body_period.py +91 -0
  32. factpulse/models/afnor_create_routing_code_body.py +153 -0
  33. factpulse/models/afnor_credentials.py +107 -0
  34. factpulse/models/afnor_destination.py +127 -0
  35. factpulse/models/afnor_diffusion_status.py +38 -0
  36. factpulse/models/afnor_directory_line_field.py +42 -0
  37. factpulse/models/afnor_directory_line_payload_history_legal_unit_facility_routing_code.py +139 -0
  38. factpulse/models/afnor_directory_line_payload_history_legal_unit_facility_routing_code_platform.py +92 -0
  39. factpulse/models/afnor_directory_line_payload_history_legal_unit_facility_routing_code_routing_code.py +134 -0
  40. factpulse/models/afnor_directory_line_post201_response.py +94 -0
  41. factpulse/models/afnor_directory_line_search_post200_response.py +104 -0
  42. factpulse/models/afnor_entity_type.py +38 -0
  43. factpulse/models/afnor_error.py +97 -0
  44. factpulse/models/afnor_facility_administrative_status.py +38 -0
  45. factpulse/models/afnor_facility_nature.py +38 -0
  46. factpulse/models/afnor_facility_payload_history.py +140 -0
  47. factpulse/models/afnor_facility_payload_history_ule_b2g_additional_data.py +98 -0
  48. factpulse/models/afnor_facility_payload_included.py +134 -0
  49. factpulse/models/afnor_facility_type.py +38 -0
  50. factpulse/models/afnor_flow.py +129 -0
  51. factpulse/models/afnor_flow_ack_status.py +39 -0
  52. factpulse/models/afnor_flow_direction.py +38 -0
  53. factpulse/models/afnor_flow_info.py +112 -0
  54. factpulse/models/afnor_flow_profile.py +39 -0
  55. factpulse/models/afnor_flow_syntax.py +41 -0
  56. factpulse/models/afnor_flow_type.py +49 -0
  57. factpulse/models/afnor_full_flow_info.py +117 -0
  58. factpulse/models/afnor_health_check_response.py +92 -0
  59. factpulse/models/afnor_legal_unit_administrative_status.py +38 -0
  60. factpulse/models/afnor_legal_unit_payload_history.py +107 -0
  61. factpulse/models/afnor_legal_unit_payload_included.py +107 -0
  62. factpulse/models/afnor_legal_unit_payload_included_no_siren.py +95 -0
  63. factpulse/models/afnor_platform_status.py +38 -0
  64. factpulse/models/afnor_processing_rule.py +42 -0
  65. factpulse/models/afnor_reason_code.py +141 -0
  66. factpulse/models/afnor_reason_code_enum.py +51 -0
  67. factpulse/models/afnor_recipient_platform_type.py +38 -0
  68. factpulse/models/afnor_result.py +127 -0
  69. factpulse/models/afnor_routing_code_administrative_status.py +38 -0
  70. factpulse/models/afnor_routing_code_field.py +44 -0
  71. factpulse/models/afnor_routing_code_payload_history_legal_unit_facility.py +158 -0
  72. factpulse/models/afnor_routing_code_post201_response.py +113 -0
  73. factpulse/models/afnor_routing_code_search.py +122 -0
  74. factpulse/models/afnor_routing_code_search_filters.py +128 -0
  75. factpulse/models/afnor_routing_code_search_filters_administrative_status.py +92 -0
  76. factpulse/models/afnor_routing_code_search_filters_routing_code_name.py +102 -0
  77. factpulse/models/afnor_routing_code_search_filters_routing_identifier.py +102 -0
  78. factpulse/models/afnor_routing_code_search_post200_response.py +104 -0
  79. factpulse/models/afnor_routing_code_search_sorting_inner.py +92 -0
  80. factpulse/models/afnor_search_directory_line.py +109 -0
  81. factpulse/models/afnor_search_directory_line_filters.py +116 -0
  82. factpulse/models/afnor_search_directory_line_filters_addressing_identifier.py +92 -0
  83. factpulse/models/afnor_search_directory_line_filters_addressing_suffix.py +92 -0
  84. factpulse/models/afnor_search_directory_line_sorting_inner.py +92 -0
  85. factpulse/models/afnor_search_flow_content.py +104 -0
  86. factpulse/models/afnor_search_flow_filters.py +106 -0
  87. factpulse/models/afnor_search_flow_params.py +95 -0
  88. factpulse/models/afnor_search_siren.py +109 -0
  89. factpulse/models/afnor_search_siren_filters.py +110 -0
  90. factpulse/models/afnor_search_siren_filters_administrative_status.py +92 -0
  91. factpulse/models/afnor_search_siren_filters_business_name.py +92 -0
  92. factpulse/models/afnor_search_siren_filters_entity_type.py +92 -0
  93. factpulse/models/afnor_search_siren_filters_siren.py +102 -0
  94. factpulse/models/afnor_search_siren_sorting_inner.py +92 -0
  95. factpulse/models/afnor_search_siret.py +122 -0
  96. factpulse/models/afnor_search_siret_filters.py +140 -0
  97. factpulse/models/afnor_search_siret_filters_address_lines.py +92 -0
  98. factpulse/models/afnor_search_siret_filters_administrative_status.py +92 -0
  99. factpulse/models/afnor_search_siret_filters_country_subdivision.py +92 -0
  100. factpulse/models/afnor_search_siret_filters_facility_type.py +92 -0
  101. factpulse/models/afnor_search_siret_filters_locality.py +92 -0
  102. factpulse/models/afnor_search_siret_filters_name.py +92 -0
  103. factpulse/models/afnor_search_siret_filters_postal_code.py +102 -0
  104. factpulse/models/afnor_search_siret_filters_siret.py +102 -0
  105. factpulse/models/afnor_search_siret_sorting_inner.py +92 -0
  106. factpulse/models/afnor_siren_field.py +41 -0
  107. factpulse/models/afnor_siren_search_post200_response.py +104 -0
  108. factpulse/models/afnor_siret_field.py +50 -0
  109. factpulse/models/afnor_siret_search_post200_response.py +104 -0
  110. factpulse/models/afnor_sorting_order.py +38 -0
  111. factpulse/models/afnor_strict_operator.py +37 -0
  112. factpulse/models/afnor_update_patch_directory_line_body.py +89 -0
  113. factpulse/models/afnor_update_patch_routing_code_body.py +120 -0
  114. factpulse/models/afnor_update_put_routing_code_body.py +114 -0
  115. factpulse/models/afnor_webhook_callback_content.py +92 -0
  116. factpulse/models/aggregated_payment_input.py +106 -0
  117. factpulse/models/aggregated_transaction_input.py +136 -0
  118. factpulse/models/allowance_charge.py +150 -0
  119. factpulse/models/allowance_charge_reason_code.py +74 -0
  120. factpulse/models/allowance_reason_code.py +43 -0
  121. factpulse/models/allowance_total_amount.py +146 -0
  122. factpulse/models/amount.py +140 -0
  123. factpulse/models/amount1.py +140 -0
  124. factpulse/models/amount_due.py +140 -0
  125. factpulse/models/api_error.py +6 -5
  126. factpulse/models/api_profile.py +41 -0
  127. factpulse/models/async_task_status.py +98 -0
  128. factpulse/models/base_amount.py +146 -0
  129. factpulse/models/bounding_box_schema.py +11 -10
  130. factpulse/models/buyercountry.py +137 -0
  131. factpulse/models/celery_status.py +41 -0
  132. factpulse/models/certificate_info_response.py +25 -24
  133. factpulse/models/charge_total_amount.py +146 -0
  134. factpulse/models/chorus_pro_credentials.py +14 -13
  135. factpulse/models/chorus_pro_destination.py +109 -0
  136. factpulse/models/chorus_pro_result.py +102 -0
  137. factpulse/models/contact.py +114 -0
  138. factpulse/models/convert_resume_request.py +88 -0
  139. factpulse/models/convert_success_response.py +127 -0
  140. factpulse/models/convert_validation_failed_response.py +121 -0
  141. factpulse/models/country_code.py +206 -0
  142. factpulse/models/create_aggregated_report_request.py +170 -0
  143. factpulse/models/create_e_reporting_request.py +173 -0
  144. factpulse/models/currency.py +137 -0
  145. factpulse/models/currency_code.py +89 -0
  146. factpulse/models/delivery_party.py +122 -0
  147. factpulse/models/destination.py +28 -27
  148. factpulse/models/directory_line_include.py +40 -0
  149. factpulse/models/doc_type.py +40 -0
  150. factpulse/models/document_type_info.py +92 -0
  151. factpulse/models/e_reporting_flow_type.py +40 -0
  152. factpulse/models/e_reporting_validation_error.py +97 -0
  153. factpulse/models/electronic_address.py +91 -0
  154. factpulse/models/enriched_invoice_info.py +134 -0
  155. factpulse/models/error_level.py +3 -2
  156. factpulse/models/error_source.py +3 -2
  157. factpulse/models/extraction_info.py +94 -0
  158. factpulse/models/factur_x_invoice.py +321 -0
  159. factpulse/models/factur_xpdf_info.py +92 -0
  160. factpulse/models/facture_electronique_rest_api_schemas_ereporting_invoice_type_code.py +41 -0
  161. factpulse/models/facture_electronique_rest_api_schemas_processing_chorus_pro_credentials.py +116 -0
  162. factpulse/models/field_status.py +41 -0
  163. factpulse/models/file_info.py +95 -0
  164. factpulse/models/files_info.py +107 -0
  165. factpulse/models/flow_direction.py +38 -0
  166. factpulse/models/flow_profile.py +39 -0
  167. factpulse/models/flow_summary.py +132 -0
  168. factpulse/models/flow_syntax.py +41 -0
  169. factpulse/models/flow_type.py +49 -0
  170. factpulse/models/generate_aggregated_report_response.py +100 -0
  171. factpulse/models/generate_certificate_request.py +27 -26
  172. factpulse/models/generate_certificate_response.py +16 -15
  173. factpulse/models/generate_e_reporting_response.py +96 -0
  174. factpulse/models/get_chorus_pro_id_request.py +101 -0
  175. factpulse/models/get_chorus_pro_id_response.py +99 -0
  176. factpulse/models/get_invoice_request.py +99 -0
  177. factpulse/models/get_invoice_response.py +143 -0
  178. factpulse/models/get_structure_request.py +101 -0
  179. factpulse/models/get_structure_response.py +143 -0
  180. factpulse/models/global_allowance_amount.py +140 -0
  181. factpulse/models/gross_unit_price.py +146 -0
  182. factpulse/models/http_validation_error.py +3 -2
  183. factpulse/models/incoming_invoice.py +175 -0
  184. factpulse/models/incoming_supplier.py +145 -0
  185. factpulse/models/invoice_format.py +39 -0
  186. factpulse/models/invoice_input.py +179 -0
  187. factpulse/models/invoice_line.py +370 -0
  188. factpulse/models/invoice_line_allowance_amount.py +146 -0
  189. factpulse/models/invoice_note.py +95 -0
  190. factpulse/models/invoice_payment_input.py +110 -0
  191. factpulse/models/invoice_references.py +195 -0
  192. factpulse/models/invoice_status.py +97 -0
  193. factpulse/models/invoice_totals.py +178 -0
  194. factpulse/models/invoice_totals_prepayment.py +146 -0
  195. factpulse/models/invoice_type_code.py +52 -0
  196. factpulse/models/invoice_type_code_output.py +52 -0
  197. factpulse/models/invoicing_framework.py +111 -0
  198. factpulse/models/invoicing_framework_code.py +40 -0
  199. factpulse/models/line_net_amount.py +146 -0
  200. factpulse/models/line_sub_type.py +39 -0
  201. factpulse/models/line_total_amount.py +146 -0
  202. factpulse/models/location_inner.py +139 -0
  203. factpulse/models/mandatory_note_schema.py +125 -0
  204. factpulse/models/manual_rate.py +140 -0
  205. factpulse/models/manual_vat_rate.py +140 -0
  206. factpulse/models/missing_field.py +108 -0
  207. factpulse/models/operation_nature.py +50 -0
  208. factpulse/models/output_format.py +38 -0
  209. factpulse/models/page_dimensions_schema.py +90 -0
  210. factpulse/models/payee.py +169 -0
  211. factpulse/models/payment_amount_by_rate.py +98 -0
  212. factpulse/models/payment_card.py +100 -0
  213. factpulse/models/payment_means.py +42 -0
  214. factpulse/models/pdf_validation_result_api.py +170 -0
  215. factpulse/models/pdp_credentials.py +16 -15
  216. factpulse/models/percentage.py +146 -0
  217. factpulse/models/postal_address.py +135 -0
  218. factpulse/models/price_allowance_amount.py +146 -0
  219. factpulse/models/price_basis_quantity.py +146 -0
  220. factpulse/models/processing_options.py +95 -0
  221. factpulse/models/processing_rule.py +42 -0
  222. factpulse/models/product_characteristic.py +90 -0
  223. factpulse/models/product_classification.py +102 -0
  224. factpulse/models/quantity.py +140 -0
  225. factpulse/models/rate.py +140 -0
  226. factpulse/models/rate1.py +140 -0
  227. factpulse/models/recipient.py +168 -0
  228. factpulse/models/report_period.py +91 -0
  229. factpulse/models/report_sender.py +98 -0
  230. factpulse/models/rounding_amount.py +146 -0
  231. factpulse/models/routing_code_include.py +38 -0
  232. factpulse/models/schematron_validation_error.py +128 -0
  233. factpulse/models/scheme_id.py +18 -4
  234. factpulse/models/search_flow_request.py +144 -0
  235. factpulse/models/search_flow_response.py +102 -0
  236. factpulse/models/search_services_response.py +102 -0
  237. factpulse/models/search_structure_request.py +120 -0
  238. factpulse/models/search_structure_response.py +102 -0
  239. factpulse/models/sellercountry.py +137 -0
  240. factpulse/models/signature_info.py +7 -6
  241. factpulse/models/signature_info_api.py +123 -0
  242. factpulse/models/signature_parameters.py +134 -0
  243. factpulse/models/simplified_invoice_data.py +151 -0
  244. factpulse/models/siret_include.py +37 -0
  245. factpulse/models/structure_info.py +15 -14
  246. factpulse/models/structure_parameters.py +92 -0
  247. factpulse/models/structure_service.py +94 -0
  248. factpulse/models/submission_mode.py +39 -0
  249. factpulse/models/submit_aggregated_report_request.py +127 -0
  250. factpulse/models/submit_complete_invoice_request.py +117 -0
  251. factpulse/models/submit_complete_invoice_response.py +146 -0
  252. factpulse/models/submit_e_reporting_request.py +127 -0
  253. factpulse/models/submit_e_reporting_response.py +117 -0
  254. factpulse/models/submit_flow_request.py +124 -0
  255. factpulse/models/submit_flow_response.py +110 -0
  256. factpulse/models/submit_gross_amount.py +140 -0
  257. factpulse/models/submit_invoice_request.py +177 -0
  258. factpulse/models/submit_invoice_response.py +104 -0
  259. factpulse/models/submit_net_amount.py +140 -0
  260. factpulse/models/submit_vat_amount.py +140 -0
  261. factpulse/models/supplementary_attachment.py +96 -0
  262. factpulse/models/supplier.py +226 -0
  263. factpulse/models/task_response.py +88 -0
  264. factpulse/models/tax_breakdown_input.py +104 -0
  265. factpulse/models/tax_due_date_type.py +42 -0
  266. factpulse/models/tax_representative.py +96 -0
  267. factpulse/models/taxable_amount.py +140 -0
  268. factpulse/models/taxableamount.py +140 -0
  269. factpulse/models/taxamount.py +140 -0
  270. factpulse/models/taxamount1.py +140 -0
  271. factpulse/models/taxamount2.py +140 -0
  272. factpulse/models/taxexclusiveamount.py +140 -0
  273. factpulse/models/taxexclusiveamount1.py +140 -0
  274. factpulse/models/total_gross_amount.py +140 -0
  275. factpulse/models/total_net_amount.py +140 -0
  276. factpulse/models/total_vat_amount.py +140 -0
  277. factpulse/models/transaction_category.py +40 -0
  278. factpulse/models/transmission_type_code.py +38 -0
  279. factpulse/models/unit_net_price.py +140 -0
  280. factpulse/models/unit_of_measure.py +42 -0
  281. factpulse/models/validate_e_reporting_request.py +92 -0
  282. factpulse/models/validate_e_reporting_response.py +113 -0
  283. factpulse/models/validation_error.py +6 -5
  284. factpulse/models/validation_error_detail.py +7 -6
  285. factpulse/models/validation_error_response.py +88 -0
  286. factpulse/models/validation_info.py +106 -0
  287. factpulse/models/validation_success_response.py +88 -0
  288. factpulse/models/vat_accounting_code.py +40 -0
  289. factpulse/models/vat_amount.py +140 -0
  290. factpulse/models/vat_category.py +45 -0
  291. factpulse/models/vat_line.py +141 -0
  292. factpulse/models/vat_point_date_code.py +39 -0
  293. factpulse/models/vat_rate.py +146 -0
  294. factpulse/models/verification_success_response.py +136 -0
  295. factpulse/models/verified_field_schema.py +130 -0
  296. factpulse/rest.py +3 -2
  297. factpulse-3.0.23.dist-info/METADATA +294 -0
  298. factpulse-3.0.23.dist-info/RECORD +306 -0
  299. {factpulse-2.0.37.dist-info → factpulse-3.0.23.dist-info}/licenses/LICENSE +1 -1
  300. factpulse_helpers/__init__.py +34 -34
  301. factpulse_helpers/client.py +1020 -795
  302. factpulse_helpers/exceptions.py +68 -68
  303. factpulse/api/traitement_facture_api.py +0 -3437
  304. factpulse/models/adresse_electronique.py +0 -90
  305. factpulse/models/adresse_postale.py +0 -120
  306. factpulse/models/cadre_de_facturation.py +0 -110
  307. factpulse/models/categorie_tva.py +0 -44
  308. factpulse/models/champ_verifie_schema.py +0 -129
  309. factpulse/models/code_cadre_facturation.py +0 -39
  310. factpulse/models/code_raison_reduction.py +0 -42
  311. factpulse/models/consulter_facture_request.py +0 -98
  312. factpulse/models/consulter_facture_response.py +0 -142
  313. factpulse/models/consulter_structure_request.py +0 -100
  314. factpulse/models/consulter_structure_response.py +0 -142
  315. factpulse/models/credentials_afnor.py +0 -106
  316. factpulse/models/credentials_chorus_pro.py +0 -115
  317. factpulse/models/destinataire.py +0 -130
  318. factpulse/models/destination_afnor.py +0 -127
  319. factpulse/models/destination_chorus_pro.py +0 -108
  320. factpulse/models/dimension_page_schema.py +0 -89
  321. factpulse/models/direction_flux.py +0 -37
  322. factpulse/models/donnees_facture_simplifiees.py +0 -124
  323. factpulse/models/facture_enrichie_info.py +0 -133
  324. factpulse/models/facture_entrante.py +0 -196
  325. factpulse/models/facture_factur_x.py +0 -183
  326. factpulse/models/flux_resume.py +0 -131
  327. factpulse/models/format_facture.py +0 -38
  328. factpulse/models/format_sortie.py +0 -37
  329. factpulse/models/fournisseur.py +0 -153
  330. factpulse/models/fournisseur_entrant.py +0 -144
  331. factpulse/models/information_signature_api.py +0 -122
  332. factpulse/models/ligne_de_poste.py +0 -183
  333. factpulse/models/ligne_de_poste_montant_remise_ht.py +0 -145
  334. factpulse/models/ligne_de_poste_taux_tva_manuel.py +0 -145
  335. factpulse/models/ligne_de_tva.py +0 -132
  336. factpulse/models/mode_depot.py +0 -38
  337. factpulse/models/mode_paiement.py +0 -41
  338. factpulse/models/montant_a_payer.py +0 -139
  339. factpulse/models/montant_base_ht.py +0 -139
  340. factpulse/models/montant_ht_total.py +0 -139
  341. factpulse/models/montant_remise_globale_ttc.py +0 -139
  342. factpulse/models/montant_total.py +0 -133
  343. factpulse/models/montant_total_acompte.py +0 -145
  344. factpulse/models/montant_total_ligne_ht.py +0 -139
  345. factpulse/models/montant_ttc_total.py +0 -139
  346. factpulse/models/montant_tva.py +0 -139
  347. factpulse/models/montant_tva_ligne.py +0 -139
  348. factpulse/models/montant_tva_total.py +0 -139
  349. factpulse/models/montant_unitaire_ht.py +0 -139
  350. factpulse/models/nature_operation.py +0 -49
  351. factpulse/models/note.py +0 -94
  352. factpulse/models/note_obligatoire_schema.py +0 -124
  353. factpulse/models/obtenir_id_chorus_pro_request.py +0 -100
  354. factpulse/models/obtenir_id_chorus_pro_response.py +0 -98
  355. factpulse/models/options_processing.py +0 -94
  356. factpulse/models/parametres_signature.py +0 -133
  357. factpulse/models/parametres_structure.py +0 -91
  358. factpulse/models/pdf_factur_x_info.py +0 -91
  359. factpulse/models/piece_jointe_complementaire.py +0 -95
  360. factpulse/models/profil_api.py +0 -39
  361. factpulse/models/profil_flux.py +0 -38
  362. factpulse/models/quantite.py +0 -139
  363. factpulse/models/rechercher_services_response.py +0 -101
  364. factpulse/models/rechercher_structure_request.py +0 -119
  365. factpulse/models/rechercher_structure_response.py +0 -101
  366. factpulse/models/references.py +0 -124
  367. factpulse/models/reponse_healthcheck_afnor.py +0 -91
  368. factpulse/models/reponse_recherche_flux.py +0 -101
  369. factpulse/models/reponse_soumission_flux.py +0 -109
  370. factpulse/models/reponse_tache.py +0 -87
  371. factpulse/models/reponse_validation_erreur.py +0 -87
  372. factpulse/models/reponse_validation_succes.py +0 -87
  373. factpulse/models/reponse_verification_succes.py +0 -135
  374. factpulse/models/requete_recherche_flux.py +0 -143
  375. factpulse/models/requete_soumission_flux.py +0 -123
  376. factpulse/models/resultat_afnor.py +0 -105
  377. factpulse/models/resultat_chorus_pro.py +0 -101
  378. factpulse/models/resultat_validation_pdfapi.py +0 -169
  379. factpulse/models/service_structure.py +0 -93
  380. factpulse/models/soumettre_facture_complete_request.py +0 -116
  381. factpulse/models/soumettre_facture_complete_response.py +0 -145
  382. factpulse/models/soumettre_facture_request.py +0 -176
  383. factpulse/models/soumettre_facture_response.py +0 -103
  384. factpulse/models/statut_acquittement.py +0 -38
  385. factpulse/models/statut_celery.py +0 -40
  386. factpulse/models/statut_champ_api.py +0 -40
  387. factpulse/models/statut_facture.py +0 -96
  388. factpulse/models/statut_tache.py +0 -97
  389. factpulse/models/syntaxe_flux.py +0 -40
  390. factpulse/models/tauxmanuel.py +0 -139
  391. factpulse/models/type_document.py +0 -40
  392. factpulse/models/type_facture.py +0 -37
  393. factpulse/models/type_flux.py +0 -40
  394. factpulse/models/type_tva.py +0 -39
  395. factpulse/models/unite.py +0 -41
  396. factpulse/models/validation_error_loc_inner.py +0 -138
  397. factpulse-2.0.37.dist-info/METADATA +0 -292
  398. factpulse-2.0.37.dist-info/RECORD +0 -134
  399. {factpulse-2.0.37.dist-info → factpulse-3.0.23.dist-info}/WHEEL +0 -0
  400. {factpulse-2.0.37.dist-info → factpulse-3.0.23.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,101 @@ 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
+ # API returns camelCase: taskId (or snake_case for backwards compat)
966
+ task_id = result.get("taskId") or result.get("task_id")
778
967
 
779
968
  if not task_id:
780
- raise FactPulseValidationError("Pas d'ID de tâche dans la réponse")
969
+ raise FactPulseValidationError("No task ID in response")
781
970
 
782
971
  if not sync:
783
972
  return task_id.encode()
784
973
 
785
974
  poll_result = self.poll_task(task_id, timeout)
786
975
 
787
- if poll_result.get("statut") == "ERREUR":
788
- # Format AFNOR: errorMessage, details
789
- error_msg = poll_result.get("errorMessage", "Erreur de validation")
976
+ if poll_result.get("status") == "ERROR":
977
+ # AFNOR format: errorMessage, details
978
+ error_msg = poll_result.get("errorMessage", "Validation error")
790
979
  errors = [
791
980
  ValidationErrorDetail(
792
981
  level=e.get("level", ""),
@@ -799,71 +988,74 @@ class FactPulseClient:
799
988
  ]
800
989
  raise FactPulseValidationError(error_msg, errors)
801
990
 
802
- if "contenu_b64" in poll_result:
803
- return base64.b64decode(poll_result["contenu_b64"])
991
+ if "content_b64" in poll_result:
992
+ return base64.b64decode(poll_result["content_b64"])
804
993
 
805
- raise FactPulseValidationError("Le résultat ne contient pas de contenu")
994
+ raise FactPulseValidationError("Result does not contain content")
806
995
 
807
996
  except requests.RequestException as e:
997
+ # Network errors (connection, timeout, etc.) - no HTTP error
808
998
  if attempt < self.max_retries:
809
- logger.warning("Erreur réseau (tentative %d/%d): %s", attempt + 1, self.max_retries + 1, e)
999
+ logger.warning("Network error (attempt %d/%d): %s", attempt + 1, self.max_retries + 1, e)
810
1000
  continue
811
- raise FactPulseValidationError(f"Erreur API: {e}")
1001
+ raise FactPulseValidationError(f"Network error: {e}")
1002
+
1003
+ raise FactPulseValidationError("Failed after all attempts")
812
1004
 
813
- raise FactPulseValidationError("Échec après toutes les tentatives")
814
1005
 
815
1006
  @staticmethod
816
- def format_montant(montant) -> str:
817
- """Formate un montant pour l'API FactPulse."""
818
- if montant is None:
1007
+ def format_amount(value) -> str:
1008
+ """Format an amount for the FactPulse API."""
1009
+ if value is None:
819
1010
  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
1011
+ if isinstance(value, Decimal):
1012
+ return f"{value:.2f}"
1013
+ if isinstance(value, (int, float)):
1014
+ return f"{value:.2f}"
1015
+ if isinstance(value, str):
1016
+ return value
826
1017
  return "0.00"
827
1018
 
828
1019
  # =========================================================================
829
1020
  # AFNOR PDP/PA - Flow Service
830
1021
  # =========================================================================
831
1022
  #
832
- # ARCHITECTURE GRAVÉE DANS LE MARBRE - NE PAS MODIFIER SANS COMPRENDRE
1023
+ # ARCHITECTURE SET IN STONE - DO NOT MODIFY WITHOUT UNDERSTANDING
833
1024
  #
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
1025
+ # The AFNOR proxy is 100% TRANSPARENT. It has the same OpenAPI as AFNOR.
1026
+ # The SDK must ALWAYS:
1027
+ # 1. Obtain AFNOR credentials (stored mode: via /credentials, zero-trust mode: provided)
1028
+ # 2. Perform AFNOR OAuth itself
1029
+ # 3. Call endpoints with AFNOR token + X-PDP-Base-URL header
839
1030
  #
840
- # Le token JWT FactPulse n'est JAMAIS utilisé pour appeler la PDP !
1031
+ # The FactPulse JWT token is NEVER used to call the PDP!
1032
+ # It's only used to retrieve credentials in stored mode.
841
1033
  # =========================================================================
842
1034
 
843
1035
  def _get_afnor_credentials(self) -> "AFNORCredentials":
844
- """Obtient les credentials AFNOR (mode stored ou zero-trust).
1036
+ """Obtain AFNOR credentials (stored or zero-trust mode).
845
1037
 
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.
1038
+ **Zero-trust mode**: Returns afnor_credentials provided to constructor.
1039
+ **Stored mode**: Retrieves credentials via GET /api/v1/afnor/credentials.
848
1040
 
849
1041
  Returns:
850
- AFNORCredentials avec flow_service_url, token_url, client_id, client_secret
1042
+ AFNORCredentials with flow_service_url, token_url, client_id, client_secret
851
1043
 
852
1044
  Raises:
853
- FactPulseAuthError: Si pas de credentials disponibles
854
- FactPulseServiceUnavailableError: Si le serveur est indisponible
1045
+ FactPulseAuthError: If no credentials available
1046
+ FactPulseServiceUnavailableError: If server is unavailable
855
1047
  """
856
1048
  from .exceptions import FactPulseServiceUnavailableError
857
1049
 
858
- # Mode zero-trust : credentials fournis au constructeur
1050
+ # Zero-trust mode: credentials provided to constructor
859
1051
  if self.afnor_credentials:
860
- logger.info("Mode zero-trust: utilisation des AFNORCredentials fournis")
1052
+ logger.info("Zero-trust mode: using provided AFNORCredentials")
861
1053
  return self.afnor_credentials
862
1054
 
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")
1055
+ # Stored mode: retrieve credentials via API
1056
+ logger.info("Stored mode: retrieving credentials via /api/v1/afnor/credentials")
865
1057
 
866
- self.ensure_authenticated() # S'assurer qu'on a un token JWT FactPulse
1058
+ self.ensure_authenticated() # Ensure we have a FactPulse JWT token
867
1059
 
868
1060
  url = f"{self.api_url}/api/v1/afnor/credentials"
869
1061
  headers = {"Authorization": f"Bearer {self._access_token}"}
@@ -878,12 +1070,12 @@ class FactPulseClient:
878
1070
  error_detail = error_json.get("detail", {})
879
1071
  if isinstance(error_detail, dict) and error_detail.get("error") == "NO_CLIENT_UID":
880
1072
  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)"
1073
+ "No client_uid in JWT. "
1074
+ "To use AFNOR endpoints, either:\n"
1075
+ "1. Generate a token with a client_uid (stored mode)\n"
1076
+ "2. Provide AFNORCredentials to the client constructor (zero-trust mode)"
885
1077
  )
886
- raise FactPulseAuthError(f"Erreur credentials AFNOR: {error_detail}")
1078
+ raise FactPulseAuthError(f"AFNOR credentials error: {error_detail}")
887
1079
 
888
1080
  if response.status_code != 200:
889
1081
  try:
@@ -891,12 +1083,12 @@ class FactPulseClient:
891
1083
  error_msg = error_json.get("detail", str(error_json))
892
1084
  except Exception:
893
1085
  error_msg = response.text or f"HTTP {response.status_code}"
894
- raise FactPulseAuthError(f"Échec récupération credentials AFNOR: {error_msg}")
1086
+ raise FactPulseAuthError(f"Failed to retrieve AFNOR credentials: {error_msg}")
895
1087
 
896
1088
  creds = response.json()
897
- logger.info(f"Credentials AFNOR récupérés pour PDP: {creds.get('flow_service_url')}")
1089
+ logger.info(f"AFNOR credentials retrieved for PDP: {creds.get('flow_service_url')}")
898
1090
 
899
- # Créer un AFNORCredentials temporaire
1091
+ # Create temporary AFNORCredentials
900
1092
  return AFNORCredentials(
901
1093
  flow_service_url=creds["flow_service_url"],
902
1094
  token_url=creds["token_url"],
@@ -905,27 +1097,27 @@ class FactPulseClient:
905
1097
  )
906
1098
 
907
1099
  def _get_afnor_token_and_url(self) -> Tuple[str, str]:
908
- """Obtient le token OAuth2 AFNOR et l'URL de la PDP.
1100
+ """Obtain AFNOR OAuth2 token and PDP URL.
909
1101
 
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
1102
+ This method:
1103
+ 1. Retrieves AFNOR credentials (stored or zero-trust mode)
1104
+ 2. Performs AFNOR OAuth to obtain a token
1105
+ 3. Returns the token and PDP URL
914
1106
 
915
1107
  Returns:
916
1108
  Tuple (afnor_token, pdp_base_url)
917
1109
 
918
1110
  Raises:
919
- FactPulseAuthError: Si l'authentification échoue
920
- FactPulseServiceUnavailableError: Si le service est indisponible
1111
+ FactPulseAuthError: If authentication fails
1112
+ FactPulseServiceUnavailableError: If service is unavailable
921
1113
  """
922
1114
  from .exceptions import FactPulseServiceUnavailableError
923
1115
 
924
- # Étape 1: Obtenir les credentials AFNOR
1116
+ # Step 1: Get AFNOR credentials
925
1117
  credentials = self._get_afnor_credentials()
926
1118
 
927
- # Étape 2: Faire l'OAuth AFNOR
928
- logger.info(f"OAuth AFNOR vers: {credentials.token_url}")
1119
+ # Step 2: Perform AFNOR OAuth
1120
+ logger.info(f"AFNOR OAuth to: {credentials.token_url}")
929
1121
 
930
1122
  url = f"{self.api_url}/api/v1/afnor/oauth/token"
931
1123
  oauth_data = {
@@ -948,15 +1140,15 @@ class FactPulseClient:
948
1140
  error_msg = error_json.get("detail", error_json.get("error", str(error_json)))
949
1141
  except Exception:
950
1142
  error_msg = response.text or f"HTTP {response.status_code}"
951
- raise FactPulseAuthError(f"Échec OAuth2 AFNOR: {error_msg}")
1143
+ raise FactPulseAuthError(f"AFNOR OAuth2 failed: {error_msg}")
952
1144
 
953
1145
  token_data = response.json()
954
1146
  afnor_token = token_data.get("access_token")
955
1147
 
956
1148
  if not afnor_token:
957
- raise FactPulseAuthError("Réponse OAuth2 AFNOR invalide: access_token manquant")
1149
+ raise FactPulseAuthError("Invalid AFNOR OAuth2 response: missing access_token")
958
1150
 
959
- logger.info("Token OAuth2 AFNOR obtenu avec succès")
1151
+ logger.info("AFNOR OAuth2 token obtained successfully")
960
1152
  return afnor_token, credentials.flow_service_url
961
1153
 
962
1154
  def _make_afnor_request(
@@ -967,54 +1159,54 @@ class FactPulseClient:
967
1159
  files: Optional[Dict] = None,
968
1160
  params: Optional[Dict] = None,
969
1161
  ) -> requests.Response:
970
- """Effectue une requête vers l'API AFNOR avec gestion d'auth et d'erreurs.
1162
+ """Perform a request to the AFNOR API with auth and error handling.
971
1163
 
972
1164
  ================================================================================
973
- ARCHITECTURE GRAVÉE DANS LE MARBRE
1165
+ ARCHITECTURE SET IN STONE
974
1166
  ================================================================================
975
1167
 
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
1168
+ This method:
1169
+ 1. Retrieves AFNOR credentials (stored mode: API, zero-trust mode: provided)
1170
+ 2. Performs AFNOR OAuth to obtain an AFNOR token
1171
+ 3. Calls the endpoint with:
1172
+ - Authorization: Bearer {afnor_token} <- AFNOR TOKEN, NOT FACTPULSE JWT!
1173
+ - X-PDP-Base-URL: {pdp_url} <- For proxy to route to correct PDP
982
1174
 
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.
1175
+ The FactPulse JWT token is NEVER used to call the PDP.
1176
+ It's only used to retrieve credentials in stored mode.
985
1177
 
986
1178
  ================================================================================
987
1179
 
988
1180
  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)
1181
+ method: HTTP method (GET, POST, etc.)
1182
+ endpoint: Relative endpoint (e.g., /flow/v1/flows)
1183
+ json_data: JSON data (optional)
1184
+ files: Multipart files (optional)
1185
+ params: Query params (optional)
994
1186
 
995
1187
  Returns:
996
- Response de l'API
1188
+ API response
997
1189
 
998
1190
  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
1191
+ FactPulseAuthError: If 401 or missing credentials
1192
+ FactPulseNotFoundError: If 404
1193
+ FactPulseServiceUnavailableError: If 503
1194
+ FactPulseValidationError: If 400/422
1195
+ FactPulseAPIError: Other errors
1004
1196
  """
1005
1197
  from .exceptions import (
1006
1198
  parse_api_error,
1007
1199
  FactPulseServiceUnavailableError,
1008
1200
  )
1009
1201
 
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)
1202
+ # Get AFNOR token and PDP URL
1203
+ # (stored mode: retrieves credentials via API, zero-trust mode: uses provided credentials)
1012
1204
  afnor_token, pdp_base_url = self._get_afnor_token_and_url()
1013
1205
 
1014
1206
  url = f"{self.api_url}/api/v1/afnor{endpoint}"
1015
1207
 
1016
- # TOUJOURS utiliser le token AFNOR + header X-PDP-Base-URL
1017
- # Le token JWT FactPulse n'est JAMAIS utilisé pour appeler la PDP !
1208
+ # ALWAYS use AFNOR token + X-PDP-Base-URL header
1209
+ # The FactPulse JWT token is NEVER used to call the PDP!
1018
1210
  headers = {
1019
1211
  "Authorization": f"Bearer {afnor_token}",
1020
1212
  "X-PDP-Base-URL": pdp_base_url,
@@ -1036,65 +1228,65 @@ class FactPulseClient:
1036
1228
  try:
1037
1229
  error_json = response.json()
1038
1230
  except Exception:
1039
- error_json = {"errorMessage": response.text or f"Erreur HTTP {response.status_code}"}
1231
+ error_json = {"errorMessage": response.text or f"HTTP error {response.status_code}"}
1040
1232
  raise parse_api_error(error_json, response.status_code)
1041
1233
 
1042
1234
  return response
1043
1235
 
1044
- def soumettre_facture_afnor(
1236
+ def submit_invoice_afnor(
1045
1237
  self,
1046
1238
  flow_name: str,
1047
1239
  pdf_path: Optional[Union[str, Path]] = None,
1048
1240
  pdf_bytes: Optional[bytes] = None,
1049
- pdf_filename: str = "facture.pdf",
1241
+ pdf_filename: str = "invoice.pdf",
1050
1242
  flow_syntax: str = "CII",
1051
1243
  flow_profile: str = "EN16931",
1052
1244
  tracking_id: Optional[str] = None,
1053
1245
  sha256: Optional[str] = None,
1054
1246
  ) -> Dict[str, Any]:
1055
- """Soumet une facture Factur-X à une PDP via l'API AFNOR.
1247
+ """Submit a Factur-X invoice to a PDP via the AFNOR API.
1056
1248
 
1057
- L'authentification utilise soit le client_uid du JWT (mode stocké),
1058
- soit les afnor_credentials fournis au constructeur (mode zero-trust).
1249
+ Authentication uses either the client_uid from JWT (stored mode),
1250
+ or afnor_credentials provided to constructor (zero-trust mode).
1059
1251
 
1060
1252
  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)
1253
+ flow_name: Flow name (e.g., "Invoice INV-2025-001")
1254
+ pdf_path: Path to PDF/A-3 file (exclusive with pdf_bytes)
1255
+ pdf_bytes: PDF content as bytes (exclusive with pdf_path)
1256
+ pdf_filename: Filename for bytes (default: "invoice.pdf")
1257
+ flow_syntax: Flow syntax (CII or UBL)
1258
+ flow_profile: Factur-X profile (MINIMUM, BASIC, EN16931, EXTENDED)
1259
+ tracking_id: Business tracking identifier (optional)
1260
+ sha256: SHA-256 hash of file (calculated automatically if absent)
1069
1261
 
1070
1262
  Returns:
1071
- Dict avec flowId, trackingId, status, sha256, etc.
1263
+ Dict with flowId, trackingId, status, sha256, etc.
1072
1264
 
1073
1265
  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
1266
+ FactPulseValidationError: If PDF is not valid
1267
+ FactPulseServiceUnavailableError: If PDP is unavailable
1268
+ ValueError: If neither pdf_path nor pdf_bytes is provided
1077
1269
 
1078
1270
  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",
1271
+ >>> # With a file path
1272
+ >>> result = client.submit_invoice_afnor(
1273
+ ... flow_name="Invoice INV-2025-001",
1274
+ ... pdf_path="invoice.pdf",
1275
+ ... tracking_id="INV-2025-001",
1084
1276
  ... )
1085
1277
  >>> print(result["flowId"])
1086
1278
 
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",
1279
+ >>> # With bytes (e.g., after Factur-X generation)
1280
+ >>> result = client.submit_invoice_afnor(
1281
+ ... flow_name="Invoice INV-2025-001",
1090
1282
  ... pdf_bytes=pdf_content,
1091
- ... pdf_filename="FAC-2025-001.pdf",
1092
- ... tracking_id="FAC-2025-001",
1283
+ ... pdf_filename="INV-2025-001.pdf",
1284
+ ... tracking_id="INV-2025-001",
1093
1285
  ... )
1094
1286
  """
1095
1287
  import hashlib
1096
1288
 
1097
- # Charger le PDF depuis le chemin si fourni
1289
+ # Load PDF from path if provided
1098
1290
  filename = pdf_filename
1099
1291
  if pdf_path:
1100
1292
  pdf_path = Path(pdf_path)
@@ -1102,13 +1294,13 @@ class FactPulseClient:
1102
1294
  filename = pdf_path.name
1103
1295
 
1104
1296
  if not pdf_bytes:
1105
- raise ValueError("pdf_path ou pdf_bytes requis")
1297
+ raise ValueError("pdf_path or pdf_bytes required")
1106
1298
 
1107
- # Calculer SHA-256 si non fourni
1299
+ # Calculate SHA-256 if not provided
1108
1300
  if not sha256:
1109
1301
  sha256 = hashlib.sha256(pdf_bytes).hexdigest()
1110
1302
 
1111
- # Préparer flowInfo
1303
+ # Prepare flowInfo
1112
1304
  flow_info = {
1113
1305
  "name": flow_name,
1114
1306
  "flowSyntax": flow_syntax,
@@ -1126,28 +1318,29 @@ class FactPulseClient:
1126
1318
  response = self._make_afnor_request("POST", "/flow/v1/flows", files=files)
1127
1319
  return response.json()
1128
1320
 
1129
- def rechercher_flux_afnor(
1321
+
1322
+ def search_flows_afnor(
1130
1323
  self,
1131
1324
  tracking_id: Optional[str] = None,
1132
1325
  status: Optional[str] = None,
1133
1326
  offset: int = 0,
1134
1327
  limit: int = 25,
1135
1328
  ) -> Dict[str, Any]:
1136
- """Recherche des flux de facturation AFNOR.
1329
+ """Search AFNOR invoice flows.
1137
1330
 
1138
1331
  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
1332
+ tracking_id: Filter by trackingId
1333
+ status: Filter by status (submitted, processing, delivered, etc.)
1334
+ offset: Start index (pagination)
1335
+ limit: Max number of results
1143
1336
 
1144
1337
  Returns:
1145
- Dict avec flows (liste), total, offset, limit
1338
+ Dict with flows (list), total, offset, limit
1146
1339
 
1147
1340
  Example:
1148
- >>> results = client.rechercher_flux_afnor(tracking_id="FAC-2025-001")
1149
- >>> for flux in results["flows"]:
1150
- ... print(flux["flowId"], flux["status"])
1341
+ >>> results = client.search_flows_afnor(tracking_id="INV-2025-001")
1342
+ >>> for flow in results["flows"]:
1343
+ ... print(flow["flowId"], flow["status"])
1151
1344
  """
1152
1345
  search_body = {
1153
1346
  "offset": offset,
@@ -1162,85 +1355,87 @@ class FactPulseClient:
1162
1355
  response = self._make_afnor_request("POST", "/flow/v1/flows/search", json_data=search_body)
1163
1356
  return response.json()
1164
1357
 
1165
- def telecharger_flux_afnor(self, flow_id: str) -> bytes:
1166
- """Télécharge le fichier PDF d'un flux AFNOR.
1358
+
1359
+ def download_flow_afnor(self, flow_id: str) -> bytes:
1360
+ """Download the PDF file of an AFNOR flow.
1167
1361
 
1168
1362
  Args:
1169
- flow_id: Identifiant du flux (UUID)
1363
+ flow_id: Flow identifier (UUID)
1170
1364
 
1171
1365
  Returns:
1172
- Contenu du fichier PDF
1366
+ PDF file content
1173
1367
 
1174
1368
  Raises:
1175
- FactPulseNotFoundError: Si le flux n'existe pas
1369
+ FactPulseNotFoundError: If flow doesn't exist
1176
1370
 
1177
1371
  Example:
1178
- >>> pdf_bytes = client.telecharger_flux_afnor("550e8400-e29b-41d4-a716-446655440000")
1179
- >>> with open("facture.pdf", "wb") as f:
1372
+ >>> pdf_bytes = client.download_flow_afnor("550e8400-e29b-41d4-a716-446655440000")
1373
+ >>> with open("invoice.pdf", "wb") as f:
1180
1374
  ... f.write(pdf_bytes)
1181
1375
  """
1182
1376
  response = self._make_afnor_request("GET", f"/flow/v1/flows/{flow_id}")
1183
1377
  return response.content
1184
1378
 
1185
- def obtenir_facture_entrante_afnor(
1379
+
1380
+ def get_incoming_invoice_afnor(
1186
1381
  self,
1187
1382
  flow_id: str,
1188
1383
  include_document: bool = False,
1189
1384
  ) -> Dict[str, Any]:
1190
- """Récupère les métadonnées JSON d'un flux entrant (facture fournisseur).
1385
+ """Retrieve JSON metadata of an incoming flow (supplier invoice).
1191
1386
 
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.
1387
+ Downloads an incoming flow from the AFNOR PDP and extracts invoice
1388
+ metadata to a unified JSON format. Supports Factur-X, CII and UBL formats.
1194
1389
 
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.
1390
+ Note: This endpoint uses FactPulse JWT authentication (not AFNOR OAuth).
1391
+ The FactPulse server handles calling the PDP with stored credentials.
1197
1392
 
1198
1393
  Args:
1199
- flow_id: Identifiant du flux (UUID)
1200
- include_document: Si True, inclut le document original encodé en base64
1394
+ flow_id: Flow identifier (UUID)
1395
+ include_document: If True, include original document encoded in base64
1201
1396
 
1202
1397
  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
1398
+ Dict with invoice metadata:
1399
+ - flow_id: Flow identifier
1400
+ - source_format: Detected format (Factur-X, CII, UBL)
1401
+ - supplier_reference: Supplier invoice number
1402
+ - document_type: Type code (380=invoice, 381=credit note, etc.)
1403
+ - supplier: Dict with name, siret, vat_number
1404
+ - billing_site_name: Recipient name
1405
+ - billing_site_siret: Recipient SIRET
1406
+ - document_date: Invoice date (YYYY-MM-DD)
1407
+ - due_date: Due date (YYYY-MM-DD)
1408
+ - currency: Currency code (EUR, USD, etc.)
1409
+ - total_excl_tax: Total excl. tax
1410
+ - total_vat: VAT amount
1411
+ - total_incl_tax: Total incl. tax
1412
+ - document_base64: (if include_document=True) Encoded document
1413
+ - document_content_type: (if include_document=True) MIME type
1414
+ - document_filename: (if include_document=True) Filename
1220
1415
 
1221
1416
  Raises:
1222
- FactPulseNotFoundError: Si le flux n'existe pas
1223
- FactPulseValidationError: Si le format n'est pas supporté
1417
+ FactPulseNotFoundError: If flow doesn't exist
1418
+ FactPulseValidationError: If format is not supported
1224
1419
 
1225
1420
  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'):
1421
+ >>> # Retrieve incoming invoice metadata
1422
+ >>> invoice = client.get_incoming_invoice_afnor("550e8400-e29b-41d4-a716-446655440000")
1423
+ >>> print(f"Supplier: {invoice['supplier']['name']}")
1424
+ >>> print(f"Total incl. tax: {invoice['total_incl_tax']} {invoice['currency']}")
1425
+
1426
+ >>> # With original document
1427
+ >>> invoice = client.get_incoming_invoice_afnor(flow_id, include_document=True)
1428
+ >>> if invoice.get('document_base64'):
1234
1429
  ... import base64
1235
- ... pdf_bytes = base64.b64decode(facture['document_base64'])
1236
- ... with open(facture['document_filename'], 'wb') as f:
1430
+ ... pdf_bytes = base64.b64decode(invoice['document_base64'])
1431
+ ... with open(invoice['document_filename'], 'wb') as f:
1237
1432
  ... f.write(pdf_bytes)
1238
1433
  """
1239
1434
  from .exceptions import FactPulseNotFoundError, FactPulseServiceUnavailableError, parse_api_error
1240
1435
 
1241
1436
  self.ensure_authenticated()
1242
1437
 
1243
- url = f"{self.api_url}/api/v1/afnor/flux-entrants/{flow_id}"
1438
+ url = f"{self.api_url}/api/v1/afnor/incoming-flows/{flow_id}"
1244
1439
  params = {}
1245
1440
  if include_document:
1246
1441
  params["include_document"] = "true"
@@ -1250,22 +1445,23 @@ class FactPulseClient:
1250
1445
  try:
1251
1446
  response = requests.get(url, headers=headers, params=params if params else None, timeout=60)
1252
1447
  except requests.RequestException as e:
1253
- raise FactPulseServiceUnavailableError("FactPulse AFNOR flux-entrants", e)
1448
+ raise FactPulseServiceUnavailableError("FactPulse AFNOR incoming flows", e)
1254
1449
 
1255
1450
  if response.status_code >= 400:
1256
1451
  try:
1257
1452
  error_json = response.json()
1258
1453
  except Exception:
1259
- error_json = {"detail": response.text or f"Erreur HTTP {response.status_code}"}
1454
+ error_json = {"detail": response.text or f"HTTP error {response.status_code}"}
1260
1455
  raise parse_api_error(error_json, response.status_code)
1261
1456
 
1262
1457
  return response.json()
1263
1458
 
1459
+
1264
1460
  def healthcheck_afnor(self) -> Dict[str, Any]:
1265
- """Vérifie la disponibilité du Flow Service AFNOR.
1461
+ """Check AFNOR Flow Service availability.
1266
1462
 
1267
1463
  Returns:
1268
- Dict avec status et service
1464
+ Dict with status and service
1269
1465
 
1270
1466
  Example:
1271
1467
  >>> status = client.healthcheck_afnor()
@@ -1285,16 +1481,16 @@ class FactPulseClient:
1285
1481
  json_data: Optional[Dict] = None,
1286
1482
  params: Optional[Dict] = None,
1287
1483
  ) -> requests.Response:
1288
- """Effectue une requête vers l'API Chorus Pro avec gestion d'auth et d'erreurs.
1484
+ """Perform a request to the Chorus Pro API with auth and error handling.
1289
1485
 
1290
1486
  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)
1487
+ method: HTTP method (GET, POST, etc.)
1488
+ endpoint: Relative endpoint (e.g., /structures/rechercher)
1489
+ json_data: JSON data (optional)
1490
+ params: Query params (optional)
1295
1491
 
1296
1492
  Returns:
1297
- Response de l'API
1493
+ API response
1298
1494
  """
1299
1495
  from .exceptions import (
1300
1496
  parse_api_error,
@@ -1306,7 +1502,7 @@ class FactPulseClient:
1306
1502
 
1307
1503
  headers = {"Authorization": f"Bearer {self._access_token}"}
1308
1504
 
1309
- # Ajouter credentials dans le body si mode zero-trust
1505
+ # Add credentials to body if zero-trust mode
1310
1506
  if json_data is None:
1311
1507
  json_data = {}
1312
1508
  if self.chorus_credentials:
@@ -1323,371 +1519,398 @@ class FactPulseClient:
1323
1519
  try:
1324
1520
  error_json = response.json()
1325
1521
  except Exception:
1326
- error_json = {"errorMessage": response.text or f"Erreur HTTP {response.status_code}"}
1522
+ error_json = {"errorMessage": response.text or f"HTTP error {response.status_code}"}
1327
1523
  raise parse_api_error(error_json, response.status_code)
1328
1524
 
1329
1525
  return response
1330
1526
 
1331
- def rechercher_structure_chorus(
1527
+ def search_structure_chorus(
1332
1528
  self,
1333
- identifiant_structure: Optional[str] = None,
1334
- raison_sociale: Optional[str] = None,
1335
- type_identifiant: str = "SIRET",
1336
- restreindre_privees: bool = True,
1529
+ structure_identifier: Optional[str] = None,
1530
+ company_name: Optional[str] = None,
1531
+ identifier_type: str = "SIRET",
1532
+ restrict_to_private: bool = True,
1337
1533
  ) -> Dict[str, Any]:
1338
- """Recherche des structures sur Chorus Pro.
1534
+ """Search structures on Chorus Pro.
1339
1535
 
1340
1536
  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
1537
+ structure_identifier: Structure SIRET or SIREN
1538
+ company_name: Company name (partial search)
1539
+ identifier_type: Identifier type (SIRET, SIREN, etc.)
1540
+ restrict_to_private: If True, limit to private structures
1345
1541
 
1346
1542
  Returns:
1347
- Dict avec liste_structures, total, code_retour, libelle
1543
+ Dict with liste_structures, total, code_retour, libelle
1348
1544
 
1349
1545
  Example:
1350
- >>> result = client.rechercher_structure_chorus(identifiant_structure="12345678901234")
1546
+ >>> result = client.search_structure_chorus(structure_identifier="12345678901234")
1351
1547
  >>> for struct in result["liste_structures"]:
1352
1548
  ... print(struct["id_structure_cpp"], struct["designation_structure"])
1353
1549
  """
1354
1550
  body = {
1355
- "restreindre_structures_privees": restreindre_privees,
1551
+ "restreindre_structures_privees": restrict_to_private,
1356
1552
  }
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
1553
+ if structure_identifier:
1554
+ body["identifiant_structure"] = structure_identifier
1555
+ if company_name:
1556
+ body["raison_sociale_structure"] = company_name
1557
+ if identifier_type:
1558
+ body["type_identifiant_structure"] = identifier_type
1363
1559
 
1364
1560
  response = self._make_chorus_request("POST", "/structures/rechercher", json_data=body)
1365
1561
  return response.json()
1366
1562
 
1367
- def consulter_structure_chorus(self, id_structure_cpp: int) -> Dict[str, Any]:
1368
- """Consulte les détails d'une structure Chorus Pro.
1369
1563
 
1370
- Retourne notamment les paramètres obligatoires pour soumettre une facture :
1564
+ def get_structure_details_chorus(self, structure_cpp_id: int) -> Dict[str, Any]:
1565
+ """Get details of a Chorus Pro structure.
1566
+
1567
+ Returns mandatory parameters for submitting an invoice:
1371
1568
  - code_service_doit_etre_renseigne
1372
1569
  - numero_ej_doit_etre_renseigne
1373
1570
 
1374
1571
  Args:
1375
- id_structure_cpp: ID Chorus Pro de la structure
1572
+ structure_cpp_id: Chorus Pro structure ID
1376
1573
 
1377
1574
  Returns:
1378
- Dict avec les détails de la structure et ses paramètres
1575
+ Dict with structure details and parameters
1379
1576
 
1380
1577
  Example:
1381
- >>> details = client.consulter_structure_chorus(12345)
1578
+ >>> details = client.get_structure_details_chorus(12345)
1382
1579
  >>> if details["parametres"]["code_service_doit_etre_renseigne"]:
1383
- ... print("Code service obligatoire")
1580
+ ... print("Service code required")
1384
1581
  """
1385
- body = {"id_structure_cpp": id_structure_cpp}
1582
+ body = {"id_structure_cpp": structure_cpp_id}
1386
1583
  response = self._make_chorus_request("POST", "/structures/consulter", json_data=body)
1387
1584
  return response.json()
1388
1585
 
1389
- def obtenir_id_chorus_depuis_siret(
1586
+ def get_chorus_id_from_siret(
1390
1587
  self,
1391
1588
  siret: str,
1392
- type_identifiant: str = "SIRET",
1589
+ identifier_type: str = "SIRET",
1393
1590
  ) -> Dict[str, Any]:
1394
- """Obtient l'ID Chorus Pro d'une structure depuis son SIRET.
1591
+ """Get Chorus Pro ID from SIRET.
1395
1592
 
1396
- Raccourci pratique pour obtenir l'id_structure_cpp avant de soumettre une facture.
1593
+ Convenient shortcut to get id_structure_cpp before submitting an invoice.
1397
1594
 
1398
1595
  Args:
1399
- siret: Numéro SIRET ou SIREN
1400
- type_identifiant: Type d'identifiant (SIRET ou SIREN)
1596
+ siret: SIRET or SIREN number
1597
+ identifier_type: Identifier type (SIRET or SIREN)
1401
1598
 
1402
1599
  Returns:
1403
- Dict avec id_structure_cpp, designation_structure, message
1600
+ Dict with id_structure_cpp, designation_structure, message
1404
1601
 
1405
1602
  Example:
1406
- >>> result = client.obtenir_id_chorus_depuis_siret("12345678901234")
1603
+ >>> result = client.get_chorus_id_from_siret("12345678901234")
1407
1604
  >>> id_cpp = result["id_structure_cpp"]
1408
1605
  >>> if id_cpp > 0:
1409
- ... print(f"Structure trouvée: {result['designation_structure']}")
1606
+ ... print(f"Structure found: {result['designation_structure']}")
1410
1607
  """
1411
1608
  body = {
1412
1609
  "siret": siret,
1413
- "type_identifiant": type_identifiant,
1610
+ "type_identifiant": identifier_type,
1414
1611
  }
1415
1612
  response = self._make_chorus_request("POST", "/structures/obtenir-id-depuis-siret", json_data=body)
1416
1613
  return response.json()
1417
1614
 
1418
- def lister_services_structure_chorus(self, id_structure_cpp: int) -> Dict[str, Any]:
1419
- """Liste les services d'une structure Chorus Pro.
1615
+ def list_structure_services_chorus(self, structure_cpp_id: int) -> Dict[str, Any]:
1616
+ """List services of a Chorus Pro structure.
1420
1617
 
1421
1618
  Args:
1422
- id_structure_cpp: ID Chorus Pro de la structure
1619
+ structure_cpp_id: Chorus Pro structure ID
1423
1620
 
1424
1621
  Returns:
1425
- Dict avec liste_services, total, code_retour, libelle
1622
+ Dict with liste_services, total, code_retour, libelle
1426
1623
 
1427
1624
  Example:
1428
- >>> services = client.lister_services_structure_chorus(12345)
1625
+ >>> services = client.list_structure_services_chorus(12345)
1429
1626
  >>> for svc in services["liste_services"]:
1430
1627
  ... if svc["est_actif"]:
1431
1628
  ... print(svc["code_service"], svc["libelle_service"])
1432
1629
  """
1433
- response = self._make_chorus_request("GET", f"/structures/{id_structure_cpp}/services")
1630
+ response = self._make_chorus_request("GET", f"/structures/{structure_cpp_id}/services")
1434
1631
  return response.json()
1435
1632
 
1436
- def soumettre_facture_chorus(
1633
+ def submit_invoice_chorus(
1437
1634
  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,
1635
+ invoice_number: str,
1636
+ invoice_date: str,
1637
+ due_date: str,
1638
+ structure_cpp_id: int,
1639
+ total_excl_tax: str,
1640
+ total_vat: str,
1641
+ total_incl_tax: str,
1642
+ main_attachment_id: Optional[int] = None,
1643
+ main_attachment_name: str = "Invoice",
1644
+ service_code: Optional[str] = None,
1645
+ commitment_number: Optional[str] = None,
1646
+ purchase_order_number: Optional[str] = None,
1647
+ contract_number: Optional[str] = None,
1648
+ comment: Optional[str] = None,
1452
1649
  ) -> Dict[str, Any]:
1453
- """Soumet une facture à Chorus Pro.
1650
+ """Submit an invoice to Chorus Pro.
1454
1651
 
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
1652
+ **Complete workflow**:
1653
+ 1. Get id_structure_cpp via search_structure_chorus()
1654
+ 2. Check mandatory parameters via get_structure_details_chorus()
1655
+ 3. Upload PDF via /transverses/ajouter-fichier API
1656
+ 4. Submit invoice with this method
1460
1657
 
1461
1658
  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
1659
+ invoice_number: Invoice number
1660
+ invoice_date: Invoice date (YYYY-MM-DD)
1661
+ due_date: Due date (YYYY-MM-DD)
1662
+ structure_cpp_id: Chorus Pro recipient ID
1663
+ total_excl_tax: Total excl. tax (e.g., "1000.00")
1664
+ total_vat: VAT amount (e.g., "200.00")
1665
+ total_incl_tax: Total incl. tax (e.g., "1200.00")
1666
+ main_attachment_id: Attachment ID (optional)
1667
+ main_attachment_name: Attachment name (default: "Invoice")
1668
+ service_code: Service code (if required by structure)
1669
+ commitment_number: Commitment number (if required)
1670
+ purchase_order_number: Purchase order number
1671
+ contract_number: Contract number
1672
+ comment: Free comment
1476
1673
 
1477
1674
  Returns:
1478
- Dict avec identifiant_facture_cpp, numero_flux_depot, code_retour, libelle
1675
+ Dict with identifiant_facture_cpp, numero_flux_depot, code_retour, libelle
1479
1676
 
1480
1677
  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",
1678
+ >>> result = client.submit_invoice_chorus(
1679
+ ... invoice_number="INV-2025-001",
1680
+ ... invoice_date="2025-01-15",
1681
+ ... due_date="2025-02-15",
1682
+ ... structure_cpp_id=12345,
1683
+ ... total_excl_tax="1000.00",
1684
+ ... total_vat="200.00",
1685
+ ... total_incl_tax="1200.00",
1489
1686
  ... )
1490
- >>> print(f"Facture soumise: {result['identifiant_facture_cpp']}")
1687
+ >>> print(f"Invoice submitted: {result['identifiant_facture_cpp']}")
1491
1688
  """
1492
1689
  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,
1690
+ "numero_facture": invoice_number,
1691
+ "date_facture": invoice_date,
1692
+ "date_echeance_paiement": due_date,
1693
+ "id_structure_cpp": structure_cpp_id,
1694
+ "montant_ht_total": total_excl_tax,
1695
+ "montant_tva": total_vat,
1696
+ "montant_ttc_total": total_incl_tax,
1500
1697
  }
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
1698
+ if main_attachment_id:
1699
+ body["piece_jointe_principale_id"] = main_attachment_id
1700
+ body["piece_jointe_principale_designation"] = main_attachment_name
1701
+ if service_code:
1702
+ body["code_service"] = service_code
1703
+ if commitment_number:
1704
+ body["numero_engagement"] = commitment_number
1705
+ if purchase_order_number:
1706
+ body["numero_bon_commande"] = purchase_order_number
1707
+ if contract_number:
1708
+ body["numero_marche"] = contract_number
1709
+ if comment:
1710
+ body["commentaire"] = comment
1514
1711
 
1515
1712
  response = self._make_chorus_request("POST", "/factures/soumettre", json_data=body)
1516
1713
  return response.json()
1517
1714
 
1518
- def consulter_facture_chorus(self, identifiant_facture_cpp: int) -> Dict[str, Any]:
1519
- """Consulte le statut d'une facture Chorus Pro.
1715
+ def get_invoice_status_chorus(self, invoice_cpp_id: int) -> Dict[str, Any]:
1716
+ """Get status of a Chorus Pro invoice.
1520
1717
 
1521
1718
  Args:
1522
- identifiant_facture_cpp: ID Chorus Pro de la facture
1719
+ invoice_cpp_id: Chorus Pro invoice ID
1523
1720
 
1524
1721
  Returns:
1525
- Dict avec statut_courant, numero_facture, date_facture, montant_ttc_total, etc.
1722
+ Dict with statut_courant, numero_facture, date_facture, montant_ttc_total, etc.
1526
1723
 
1527
1724
  Example:
1528
- >>> status = client.consulter_facture_chorus(12345)
1529
- >>> print(f"Statut: {status['statut_courant']['code']}")
1725
+ >>> status = client.get_invoice_status_chorus(12345)
1726
+ >>> print(f"Status: {status['statut_courant']['code']}")
1530
1727
  """
1531
- body = {"identifiant_facture_cpp": identifiant_facture_cpp}
1728
+ body = {"identifiant_facture_cpp": invoice_cpp_id}
1532
1729
  response = self._make_chorus_request("POST", "/factures/consulter", json_data=body)
1533
1730
  return response.json()
1534
1731
 
1535
1732
  # ==================== AFNOR Directory ====================
1536
1733
 
1537
- def rechercher_siret_afnor(self, siret: str) -> Dict[str, Any]:
1538
- """Recherche une entreprise par SIRET dans l'annuaire AFNOR.
1734
+ def search_siret_afnor(self, siret: str) -> Dict[str, Any]:
1735
+ """Search a company by SIRET in the AFNOR directory.
1539
1736
 
1540
1737
  Args:
1541
- siret: Numéro SIRET (14 chiffres)
1738
+ siret: SIRET number (14 digits)
1542
1739
 
1543
1740
  Returns:
1544
- Dict avec informations entreprise: raison_sociale, adresse, etc.
1741
+ Dict with company info: company_name, address, etc.
1545
1742
 
1546
1743
  Example:
1547
- >>> result = client.rechercher_siret_afnor("12345678901234")
1548
- >>> print(f"Entreprise: {result['raison_sociale']}")
1744
+ >>> result = client.search_siret_afnor("12345678901234")
1745
+ >>> print(f"Company: {result['raison_sociale']}")
1549
1746
  """
1550
1747
  response = self._make_afnor_request("GET", f"/directory/siret/{siret}")
1551
1748
  return response.json()
1552
1749
 
1553
- def rechercher_siren_afnor(self, siren: str) -> Dict[str, Any]:
1554
- """Recherche une entreprise par SIREN dans l'annuaire AFNOR.
1750
+
1751
+ def search_siren_afnor(self, siren: str) -> Dict[str, Any]:
1752
+ """Search a company by SIREN in the AFNOR directory.
1555
1753
 
1556
1754
  Args:
1557
- siren: Numéro SIREN (9 chiffres)
1755
+ siren: SIREN number (9 digits)
1558
1756
 
1559
1757
  Returns:
1560
- Dict avec informations entreprise et liste des établissements
1758
+ Dict with company info and list of establishments
1561
1759
 
1562
1760
  Example:
1563
- >>> result = client.rechercher_siren_afnor("123456789")
1564
- >>> for etab in result.get('etablissements', []):
1565
- ... print(f"SIRET: {etab['siret']}")
1761
+ >>> result = client.search_siren_afnor("123456789")
1762
+ >>> for estab in result.get('etablissements', []):
1763
+ ... print(f"SIRET: {estab['siret']}")
1566
1764
  """
1567
1765
  response = self._make_afnor_request("GET", f"/directory/siren/{siren}")
1568
1766
  return response.json()
1569
1767
 
1570
- def lister_codes_routage_afnor(self, siren: str) -> List[Dict[str, Any]]:
1571
- """Liste les codes de routage disponibles pour un SIREN.
1768
+
1769
+ def list_routing_codes_afnor(self, siren: str) -> List[Dict[str, Any]]:
1770
+ """List available routing codes for a SIREN.
1572
1771
 
1573
1772
  Args:
1574
- siren: Numéro SIREN (9 chiffres)
1773
+ siren: SIREN number (9 digits)
1575
1774
 
1576
1775
  Returns:
1577
- Liste des codes de routage avec leurs paramètres
1776
+ List of routing codes with their parameters
1578
1777
 
1579
1778
  Example:
1580
- >>> codes = client.lister_codes_routage_afnor("123456789")
1779
+ >>> codes = client.list_routing_codes_afnor("123456789")
1581
1780
  >>> for code in codes:
1582
1781
  ... print(f"Code: {code['code_routage']}")
1583
1782
  """
1584
1783
  response = self._make_afnor_request("GET", f"/directory/siren/{siren}/routing-codes")
1585
1784
  return response.json()
1586
1785
 
1786
+
1587
1787
  # ==================== Validation ====================
1588
1788
 
1589
- def valider_pdf_facturx(
1789
+ def validate_facturx_pdf(
1590
1790
  self,
1591
1791
  pdf_path: Optional[str] = None,
1592
1792
  pdf_bytes: Optional[bytes] = None,
1593
- profil: str = "EN16931"
1793
+ profile: Optional[str] = None,
1794
+ use_verapdf: bool = False,
1594
1795
  ) -> Dict[str, Any]:
1595
- """Valide un PDF Factur-X.
1796
+ """Validate a Factur-X PDF.
1596
1797
 
1597
1798
  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)
1799
+ pdf_path: Path to PDF file (exclusive with pdf_bytes)
1800
+ pdf_bytes: PDF content as bytes (exclusive with pdf_path)
1801
+ profile: Expected Factur-X profile (MINIMUM, BASIC, EN16931, EXTENDED).
1802
+ If None, profile is auto-detected from embedded XML.
1803
+ use_verapdf: Enable strict PDF/A validation with VeraPDF (default: False).
1804
+ - False: Fast metadata validation (~100ms)
1805
+ - True: Strict ISO 19005 validation with 146+ rules (2-10s, recommended in production)
1601
1806
 
1602
1807
  Returns:
1603
- Dict avec: est_conforme (bool), erreurs (list), avertissements (list), profil_detecte
1808
+ Dict with:
1809
+ - is_compliant (bool): True if PDF is compliant
1810
+ - xml_present (bool): True if Factur-X XML is embedded
1811
+ - xml_compliant (bool): True if XML is valid according to Schematron
1812
+ - detected_profile (str): Detected profile (MINIMUM, BASIC, EN16931, EXTENDED)
1813
+ - xml_errors (list): XML validation errors
1814
+ - pdfa_compliant (bool): True if PDF/A compliant
1815
+ - pdfa_version (str): Detected PDF/A version (e.g., "PDF/A-3B")
1816
+ - pdfa_validation_method (str): "metadata" or "verapdf"
1817
+ - pdfa_errors (list): PDF/A compliance errors
1604
1818
 
1605
1819
  Example:
1606
- >>> result = client.valider_pdf_facturx("facture.pdf")
1607
- >>> if result['est_conforme']:
1608
- ... print("PDF Factur-X valide!")
1820
+ >>> # Validation with auto-detected profile
1821
+ >>> result = client.validate_facturx_pdf("invoice.pdf")
1822
+ >>> print(f"Detected profile: {result['detected_profile']}")
1823
+
1824
+ >>> # Strict validation with VeraPDF (recommended in production)
1825
+ >>> result = client.validate_facturx_pdf("invoice.pdf", use_verapdf=True)
1826
+ >>> if result['is_compliant']:
1827
+ ... print("Valid Factur-X PDF!")
1609
1828
  >>> else:
1610
- ... for err in result['erreurs']:
1611
- ... print(f"Erreur: {err}")
1829
+ ... for err in result.get('pdfa_errors', []):
1830
+ ... print(f"PDF/A error: {err}")
1612
1831
  """
1613
1832
  if pdf_path:
1614
1833
  with open(pdf_path, "rb") as f:
1615
1834
  pdf_bytes = f.read()
1616
1835
  if not pdf_bytes:
1617
- raise ValueError("pdf_path ou pdf_bytes requis")
1836
+ raise ValueError("pdf_path or pdf_bytes required")
1618
1837
 
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)
1838
+ files = {"pdf_file": ("invoice.pdf", pdf_bytes, "application/pdf")}
1839
+ data: Dict[str, Any] = {"use_verapdf": str(use_verapdf).lower()}
1840
+ if profile:
1841
+ data["profile"] = profile
1842
+ response = self._request("POST", "/processing/validate-facturx-pdf", files=files, data=data)
1622
1843
  return response.json()
1623
1844
 
1624
- def valider_signature_pdf(
1845
+
1846
+ def validate_pdf_signature(
1625
1847
  self,
1626
1848
  pdf_path: Optional[str] = None,
1627
1849
  pdf_bytes: Optional[bytes] = None
1628
1850
  ) -> Dict[str, Any]:
1629
- """Valide la signature d'un PDF signé.
1851
+ """Validate the signature of a signed PDF.
1630
1852
 
1631
1853
  Args:
1632
- pdf_path: Chemin vers le fichier PDF signé
1633
- pdf_bytes: Contenu PDF en bytes
1854
+ pdf_path: Path to signed PDF file
1855
+ pdf_bytes: PDF content as bytes
1634
1856
 
1635
1857
  Returns:
1636
- Dict avec: is_signed (bool), signatures (list), etc.
1858
+ Dict with: is_signed (bool), signatures (list), etc.
1637
1859
 
1638
1860
  Example:
1639
- >>> result = client.valider_signature_pdf("facture_signee.pdf")
1861
+ >>> result = client.validate_pdf_signature("signed_invoice.pdf")
1640
1862
  >>> if result['is_signed']:
1641
- ... print("PDF signé!")
1863
+ ... print("PDF is signed!")
1642
1864
  ... for sig in result.get('signatures', []):
1643
- ... print(f"Signé par: {sig.get('signer_cn')}")
1865
+ ... print(f"Signed by: {sig.get('signer_cn')}")
1644
1866
  """
1645
1867
  if pdf_path:
1646
1868
  with open(pdf_path, "rb") as f:
1647
1869
  pdf_bytes = f.read()
1648
1870
  if not pdf_bytes:
1649
- raise ValueError("pdf_path ou pdf_bytes requis")
1871
+ raise ValueError("pdf_path or pdf_bytes required")
1650
1872
 
1651
- files = {"fichier_pdf": ("document.pdf", pdf_bytes, "application/pdf")}
1652
- response = self._request("POST", "/traitement/valider-signature-pdf", files=files)
1873
+ files = {"pdf_file": ("document.pdf", pdf_bytes, "application/pdf")}
1874
+ response = self._request("POST", "/processing/validate-pdf-signature", files=files)
1653
1875
  return response.json()
1654
1876
 
1877
+
1655
1878
  # ==================== Signature ====================
1656
1879
 
1657
- def signer_pdf(
1880
+ def sign_pdf(
1658
1881
  self,
1659
1882
  pdf_path: Optional[str] = None,
1660
1883
  pdf_bytes: Optional[bytes] = None,
1661
- raison: Optional[str] = None,
1662
- localisation: Optional[str] = None,
1884
+ reason: Optional[str] = None,
1885
+ location: Optional[str] = None,
1663
1886
  contact: Optional[str] = None,
1664
1887
  use_pades_lt: bool = False,
1665
1888
  use_timestamp: bool = True,
1666
1889
  output_path: Optional[str] = None
1667
1890
  ) -> Union[bytes, str]:
1668
- """Signe un PDF avec le certificat configuré côté serveur.
1891
+ """Sign a PDF with the server-side configured certificate.
1669
1892
 
1670
- Le certificat doit être préalablement configuré dans Django Admin
1671
- pour le client identifié par le client_uid du JWT.
1893
+ The certificate must be pre-configured in Django Admin
1894
+ for the client identified by the JWT client_uid.
1672
1895
 
1673
1896
  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
1897
+ pdf_path: Path to PDF to sign
1898
+ pdf_bytes: PDF content as bytes
1899
+ reason: Signature reason (optional)
1900
+ location: Signature location (optional)
1901
+ contact: Contact email (optional)
1902
+ use_pades_lt: Enable PAdES-B-LT for long-term archiving (default: False)
1903
+ use_timestamp: Enable RFC 3161 timestamping (default: True)
1904
+ output_path: If provided, save signed PDF to this path
1682
1905
 
1683
1906
  Returns:
1684
- bytes du PDF signé, ou chemin si output_path fourni
1907
+ Signed PDF bytes, or path if output_path provided
1685
1908
 
1686
1909
  Example:
1687
- >>> pdf_signe = client.signer_pdf(
1688
- ... pdf_path="facture.pdf",
1689
- ... raison="Conformité Factur-X",
1690
- ... output_path="facture_signee.pdf"
1910
+ >>> signed_pdf = client.sign_pdf(
1911
+ ... pdf_path="invoice.pdf",
1912
+ ... reason="Factur-X Compliance",
1913
+ ... output_path="signed_invoice.pdf"
1691
1914
  ... )
1692
1915
  """
1693
1916
  if pdf_path:
@@ -1695,67 +1918,68 @@ class FactPulseClient:
1695
1918
  pdf_bytes = f.read()
1696
1919
 
1697
1920
  if not pdf_bytes:
1698
- raise ValueError("pdf_path ou pdf_bytes requis")
1921
+ raise ValueError("pdf_path or pdf_bytes required")
1699
1922
 
1700
1923
  files = {
1701
- "fichier_pdf": ("document.pdf", pdf_bytes, "application/pdf"),
1924
+ "pdf_file": ("document.pdf", pdf_bytes, "application/pdf"),
1702
1925
  }
1703
1926
  data: Dict[str, Any] = {
1704
1927
  "use_pades_lt": str(use_pades_lt).lower(),
1705
1928
  "use_timestamp": str(use_timestamp).lower(),
1706
1929
  }
1707
- if raison:
1708
- data["raison"] = raison
1709
- if localisation:
1710
- data["localisation"] = localisation
1930
+ if reason:
1931
+ data["reason"] = reason
1932
+ if location:
1933
+ data["location"] = location
1711
1934
  if contact:
1712
1935
  data["contact"] = contact
1713
1936
 
1714
- response = self._request("POST", "/traitement/signer-pdf", files=files, data=data)
1937
+ response = self._request("POST", "/processing/sign-pdf", files=files, data=data)
1715
1938
  result = response.json()
1716
1939
 
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")
1940
+ # API returns JSON with signed_pdf_base64
1941
+ pdf_signed_b64 = result.get("signed_pdf_base64")
1942
+ if not pdf_signed_b64:
1943
+ raise FactPulseValidationError("Invalid signature response")
1721
1944
 
1722
1945
  import base64
1723
- pdf_signe = base64.b64decode(pdf_signe_b64)
1946
+ pdf_signed = base64.b64decode(pdf_signed_b64)
1724
1947
 
1725
1948
  if output_path:
1726
1949
  with open(output_path, "wb") as f:
1727
- f.write(pdf_signe)
1950
+ f.write(pdf_signed)
1728
1951
  return output_path
1729
1952
 
1730
- return pdf_signe
1953
+ return pdf_signed
1731
1954
 
1732
- def generer_certificat_test(
1955
+
1956
+ def generate_test_certificate(
1733
1957
  self,
1734
1958
  cn: str = "Test Organisation",
1735
1959
  organisation: str = "Test Organisation",
1736
1960
  email: str = "test@example.com",
1737
- duree_jours: int = 365,
1738
- taille_cle: int = 2048,
1961
+ validity_days: int = 365,
1962
+ key_size: int = 2048,
1739
1963
  ) -> Dict[str, Any]:
1740
- """Génère un certificat de test pour la signature (NON PRODUCTION).
1964
+ """Generate a test certificate for signing (NOT FOR PRODUCTION).
1741
1965
 
1742
- Le certificat généré doit ensuite être configuré dans Django Admin.
1966
+ The generated certificate must then be configured in Django Admin.
1743
1967
 
1744
1968
  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)
1969
+ cn: Certificate Common Name
1970
+ organisation: Organisation name
1971
+ email: Email associated with certificate
1972
+ validity_days: Validity duration in days (default: 365)
1973
+ key_size: RSA key size (2048 or 4096)
1750
1974
 
1751
1975
  Returns:
1752
- Dict avec certificat_pem, cle_privee_pem, pkcs12_base64, etc.
1976
+ Dict with certificat_pem, cle_privee_pem, pkcs12_base64, etc.
1753
1977
 
1754
1978
  Example:
1755
- >>> result = client.generer_certificat_test(
1756
- ... cn="Ma Société - Cachet",
1757
- ... organisation="Ma Société SAS",
1758
- ... email="contact@masociete.fr",
1979
+ >>> result = client.generate_test_certificate(
1980
+ ... cn="My Company - Seal",
1981
+ ... organisation="My Company SAS",
1982
+ ... email="contact@mycompany.com",
1759
1983
  ... )
1760
1984
  >>> print(result["certificat_pem"])
1761
1985
  """
@@ -1763,122 +1987,123 @@ class FactPulseClient:
1763
1987
  "cn": cn,
1764
1988
  "organisation": organisation,
1765
1989
  "email": email,
1766
- "duree_jours": duree_jours,
1767
- "taille_cle": taille_cle,
1990
+ "validity_days": validity_days,
1991
+ "key_size": key_size,
1768
1992
  }
1769
- response = self._request("POST", "/traitement/generer-certificat-test", json_data=data)
1993
+ response = self._request("POST", "/processing/generate-test-certificate", json_data=data)
1770
1994
  return response.json()
1771
1995
 
1772
- # ==================== Workflow complet ====================
1773
1996
 
1774
- def generer_facturx_complet(
1997
+ # ==================== Complete workflow ====================
1998
+
1999
+ def generate_complete_facturx(
1775
2000
  self,
1776
- facture: Dict[str, Any],
2001
+ invoice: Dict[str, Any],
1777
2002
  pdf_source_path: Optional[str] = None,
1778
2003
  pdf_source_bytes: Optional[bytes] = None,
1779
- profil: str = "EN16931",
1780
- valider: bool = True,
1781
- signer: bool = False,
1782
- soumettre_afnor: bool = False,
2004
+ profile: str = "EN16931",
2005
+ validate: bool = True,
2006
+ sign: bool = False,
2007
+ submit_afnor: bool = False,
1783
2008
  afnor_flow_name: Optional[str] = None,
1784
2009
  afnor_tracking_id: Optional[str] = None,
1785
2010
  output_path: Optional[str] = None,
1786
2011
  timeout: int = 120000
1787
2012
  ) -> Dict[str, Any]:
1788
- """Génère un PDF Factur-X complet avec validation, signature et soumission optionnelles.
2013
+ """Generate a complete Factur-X PDF with optional validation, signing and submission.
1789
2014
 
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)
2015
+ This method automatically chains:
2016
+ 1. Factur-X PDF generation
2017
+ 2. Validation (optional)
2018
+ 3. Signing (optional, uses server-side certificate)
2019
+ 4. AFNOR PDP submission (optional)
1795
2020
 
1796
- Note: La signature utilise le certificat configuré dans Django Admin
1797
- pour le client identifié par le client_uid du JWT.
2021
+ Note: Signing uses the certificate configured in Django Admin
2022
+ for the client identified by the JWT client_uid.
1798
2023
 
1799
2024
  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
2025
+ invoice: Invoice data (FactureFacturX format)
2026
+ pdf_source_path: Path to source PDF
2027
+ pdf_source_bytes: Source PDF as bytes
2028
+ profile: Factur-X profile (MINIMUM, BASIC, EN16931, EXTENDED)
2029
+ validate: If True, validate generated PDF
2030
+ sign: If True, sign PDF (server-side certificate)
2031
+ submit_afnor: If True, submit PDF to AFNOR PDP
2032
+ afnor_flow_name: AFNOR flow name (default: "Invoice {invoice_number}")
2033
+ afnor_tracking_id: AFNOR tracking ID (default: invoice_number)
2034
+ output_path: Output path for final PDF
2035
+ timeout: Polling timeout in ms
1811
2036
 
1812
2037
  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
2038
+ Dict with:
2039
+ - pdf_bytes: Final PDF bytes
2040
+ - pdf_path: Path if output_path provided
2041
+ - validation: Validation result if validate=True
2042
+ - signature: Signature info if sign=True
2043
+ - afnor: AFNOR submission result if submit_afnor=True
1819
2044
 
1820
2045
  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"
2046
+ >>> result = client.generate_complete_facturx(
2047
+ ... invoice=my_invoice,
2048
+ ... pdf_source_path="quote.pdf",
2049
+ ... profile="EN16931",
2050
+ ... validate=True,
2051
+ ... sign=True,
2052
+ ... submit_afnor=True,
2053
+ ... output_path="final_invoice.pdf"
1829
2054
  ... )
1830
- >>> if result['validation']['valide']:
1831
- ... print(f"Facture soumise! Flow ID: {result['afnor']['flowId']}")
2055
+ >>> if result['validation']['is_compliant']:
2056
+ ... print(f"Invoice submitted! Flow ID: {result['afnor']['flowId']}")
1832
2057
  """
1833
2058
  result: Dict[str, Any] = {}
1834
2059
 
1835
- # 1. Génération
2060
+ # 1. Generation
1836
2061
  if pdf_source_path:
1837
2062
  with open(pdf_source_path, "rb") as f:
1838
2063
  pdf_source_bytes = f.read()
1839
2064
 
1840
- pdf_bytes = self.generer_facturx(
1841
- facture_data=facture,
2065
+ pdf_bytes = self.generate_facturx(
2066
+ invoice_data=invoice,
1842
2067
  pdf_source=pdf_source_bytes,
1843
- profil=profil,
2068
+ profile=profile,
1844
2069
  timeout=timeout
1845
2070
  )
1846
2071
  result["pdf_bytes"] = pdf_bytes
1847
2072
 
1848
2073
  # 2. Validation
1849
- if valider:
1850
- validation = self.valider_pdf_facturx(pdf_bytes=pdf_bytes, profil=profil)
2074
+ if validate:
2075
+ validation = self.validate_facturx_pdf(pdf_bytes=pdf_bytes, profile=profile)
1851
2076
  result["validation"] = validation
1852
- if not validation.get("est_conforme", False):
1853
- # Retourne quand même le résultat mais avec les erreurs
2077
+ if not validation.get("est_conforme", False) and not validation.get("is_compliant", False):
2078
+ # Return result with errors
1854
2079
  if output_path:
1855
2080
  with open(output_path, "wb") as f:
1856
2081
  f.write(pdf_bytes)
1857
2082
  result["pdf_path"] = output_path
1858
2083
  return result
1859
2084
 
1860
- # 3. Signature (utilise le certificat côté serveur)
1861
- if signer:
1862
- pdf_bytes = self.signer_pdf(pdf_bytes=pdf_bytes)
2085
+ # 3. Signing (uses server-side certificate)
2086
+ if sign:
2087
+ pdf_bytes = self.sign_pdf(pdf_bytes=pdf_bytes)
1863
2088
  result["pdf_bytes"] = pdf_bytes
1864
- result["signature"] = {"signe": True}
2089
+ result["signature"] = {"signed": True}
1865
2090
 
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
2091
+ # 4. AFNOR submission
2092
+ if submit_afnor:
2093
+ invoice_number = invoice.get("invoiceNumber", invoice.get("numeroFacture", invoice.get("numero_facture", "INVOICE")))
2094
+ flow_name = afnor_flow_name or f"Invoice {invoice_number}"
2095
+ tracking_id = afnor_tracking_id or invoice_number
1871
2096
 
1872
- # Soumission directe avec bytes (plus de fichier temporaire nécessaire)
1873
- afnor_result = self.soumettre_facture_afnor(
2097
+ # Direct submission with bytes (no temp file needed)
2098
+ afnor_result = self.submit_invoice_afnor(
1874
2099
  flow_name=flow_name,
1875
2100
  pdf_bytes=pdf_bytes,
1876
- pdf_filename=f"{numero_facture}.pdf",
2101
+ pdf_filename=f"{invoice_number}.pdf",
1877
2102
  tracking_id=tracking_id,
1878
2103
  )
1879
2104
  result["afnor"] = afnor_result
1880
2105
 
1881
- # Sauvegarde finale
2106
+ # Final save
1882
2107
  if output_path:
1883
2108
  with open(output_path, "wb") as f:
1884
2109
  f.write(pdf_bytes)