json2ubl 1.0.0__tar.gz → 1.0.1__tar.gz

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.
Files changed (132) hide show
  1. json2ubl-1.0.1/CHANGELOG.md +108 -0
  2. {json2ubl-1.0.0 → json2ubl-1.0.1}/PKG-INFO +13 -10
  3. {json2ubl-1.0.0 → json2ubl-1.0.1}/README.md +3 -0
  4. json2ubl-1.0.1/docs/API.md +451 -0
  5. {json2ubl-1.0.0 → json2ubl-1.0.1}/pyproject.toml +10 -10
  6. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/__init__.py +27 -21
  7. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/config.py +1 -1
  8. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/converter.py +83 -30
  9. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl.egg-info/PKG-INFO +13 -10
  10. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl.egg-info/SOURCES.txt +2 -0
  11. json2ubl-1.0.1/src/json2ubl.egg-info/requires.txt +11 -0
  12. {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_components.py +68 -5
  13. {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_error_handling.py +22 -3
  14. {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_pipeline.py +130 -0
  15. json2ubl-1.0.0/src/json2ubl.egg-info/requires.txt +0 -11
  16. {json2ubl-1.0.0 → json2ubl-1.0.1}/.gitattributes +0 -0
  17. {json2ubl-1.0.0 → json2ubl-1.0.1}/.github/workflows/tests.yml +0 -0
  18. {json2ubl-1.0.0 → json2ubl-1.0.1}/.gitignore +0 -0
  19. {json2ubl-1.0.0 → json2ubl-1.0.1}/LICENSE +0 -0
  20. {json2ubl-1.0.0 → json2ubl-1.0.1}/Makefile +0 -0
  21. {json2ubl-1.0.0 → json2ubl-1.0.1}/config/ubl_converter.yaml +0 -0
  22. {json2ubl-1.0.0 → json2ubl-1.0.1}/setup.cfg +0 -0
  23. {json2ubl-1.0.0 → json2ubl-1.0.1}/setup.py +0 -0
  24. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/__init__.py +0 -0
  25. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/benchmark.py +0 -0
  26. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/benchmark_examples.py +0 -0
  27. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/constants.py +0 -0
  28. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/core/__init__.py +0 -0
  29. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/core/mapper.py +0 -0
  30. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/core/schema_cache_builder.py +0 -0
  31. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/core/serializer.py +0 -0
  32. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/core/validator.py +0 -0
  33. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/exceptions.py +0 -0
  34. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/cache/CreditNote_schema_cache.json +0 -0
  35. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/cache/DebitNote_schema_cache.json +0 -0
  36. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/cache/FreightInvoice_schema_cache.json +0 -0
  37. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/cache/Invoice_schema_cache.json +0 -0
  38. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/cache/SelfBilledCreditNote_schema_cache.json +0 -0
  39. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/cache/SelfBilledInvoice_schema_cache.json +0 -0
  40. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/CCTS_CCT_SchemaModule-2.1.xsd +0 -0
  41. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-CommonAggregateComponents-2.1.xsd +0 -0
  42. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-CommonBasicComponents-2.1.xsd +0 -0
  43. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-CommonExtensionComponents-2.1.xsd +0 -0
  44. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-CommonSignatureComponents-2.1.xsd +0 -0
  45. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-CoreComponentParameters-2.1.xsd +0 -0
  46. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-ExtensionContentDataType-2.1.xsd +0 -0
  47. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-QualifiedDataTypes-2.1.xsd +0 -0
  48. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-SignatureAggregateComponents-2.1.xsd +0 -0
  49. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-SignatureBasicComponents-2.1.xsd +0 -0
  50. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-UnqualifiedDataTypes-2.1.xsd +0 -0
  51. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-XAdESv132-2.1.xsd +0 -0
  52. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-XAdESv141-2.1.xsd +0 -0
  53. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-xmldsig-core-schema-2.1.xsd +0 -0
  54. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-ApplicationResponse-2.1.xsd +0 -0
  55. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-AttachedDocument-2.1.xsd +0 -0
  56. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-AwardedNotification-2.1.xsd +0 -0
  57. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-BillOfLading-2.1.xsd +0 -0
  58. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-CallForTenders-2.1.xsd +0 -0
  59. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-Catalogue-2.1.xsd +0 -0
  60. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-CatalogueDeletion-2.1.xsd +0 -0
  61. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-CatalogueItemSpecificationUpdate-2.1.xsd +0 -0
  62. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-CataloguePricingUpdate-2.1.xsd +0 -0
  63. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-CatalogueRequest-2.1.xsd +0 -0
  64. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-CertificateOfOrigin-2.1.xsd +0 -0
  65. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-ContractAwardNotice-2.1.xsd +0 -0
  66. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-ContractNotice-2.1.xsd +0 -0
  67. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-CreditNote-2.1.xsd +0 -0
  68. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-DebitNote-2.1.xsd +0 -0
  69. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-DespatchAdvice-2.1.xsd +0 -0
  70. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-DocumentStatus-2.1.xsd +0 -0
  71. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-DocumentStatusRequest-2.1.xsd +0 -0
  72. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-ExceptionCriteria-2.1.xsd +0 -0
  73. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-ExceptionNotification-2.1.xsd +0 -0
  74. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-Forecast-2.1.xsd +0 -0
  75. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-ForecastRevision-2.1.xsd +0 -0
  76. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-ForwardingInstructions-2.1.xsd +0 -0
  77. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-FreightInvoice-2.1.xsd +0 -0
  78. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-FulfilmentCancellation-2.1.xsd +0 -0
  79. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-GoodsItemItinerary-2.1.xsd +0 -0
  80. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-GuaranteeCertificate-2.1.xsd +0 -0
  81. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-InstructionForReturns-2.1.xsd +0 -0
  82. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-InventoryReport-2.1.xsd +0 -0
  83. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-Invoice-2.1.xsd +0 -0
  84. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-ItemInformationRequest-2.1.xsd +0 -0
  85. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-Order-2.1.xsd +0 -0
  86. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-OrderCancellation-2.1.xsd +0 -0
  87. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-OrderChange-2.1.xsd +0 -0
  88. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-OrderResponse-2.1.xsd +0 -0
  89. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-OrderResponseSimple-2.1.xsd +0 -0
  90. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-PackingList-2.1.xsd +0 -0
  91. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-PriorInformationNotice-2.1.xsd +0 -0
  92. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-ProductActivity-2.1.xsd +0 -0
  93. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-Quotation-2.1.xsd +0 -0
  94. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-ReceiptAdvice-2.1.xsd +0 -0
  95. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-Reminder-2.1.xsd +0 -0
  96. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-RemittanceAdvice-2.1.xsd +0 -0
  97. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-RequestForQuotation-2.1.xsd +0 -0
  98. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-RetailEvent-2.1.xsd +0 -0
  99. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-SelfBilledCreditNote-2.1.xsd +0 -0
  100. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-SelfBilledInvoice-2.1.xsd +0 -0
  101. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-Statement-2.1.xsd +0 -0
  102. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-StockAvailabilityReport-2.1.xsd +0 -0
  103. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-Tender-2.1.xsd +0 -0
  104. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TenderReceipt-2.1.xsd +0 -0
  105. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TendererQualification-2.1.xsd +0 -0
  106. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TendererQualificationResponse-2.1.xsd +0 -0
  107. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TradeItemLocationProfile-2.1.xsd +0 -0
  108. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TransportExecutionPlan-2.1.xsd +0 -0
  109. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TransportExecutionPlanRequest-2.1.xsd +0 -0
  110. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TransportProgressStatus-2.1.xsd +0 -0
  111. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TransportProgressStatusRequest-2.1.xsd +0 -0
  112. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TransportServiceDescription-2.1.xsd +0 -0
  113. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TransportServiceDescriptionRequest-2.1.xsd +0 -0
  114. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TransportationStatus-2.1.xsd +0 -0
  115. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TransportationStatusRequest-2.1.xsd +0 -0
  116. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-UnawardedNotification-2.1.xsd +0 -0
  117. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-UtilityStatement-2.1.xsd +0 -0
  118. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-Waybill-2.1.xsd +0 -0
  119. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl.egg-info/dependency_links.txt +0 -0
  120. {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl.egg-info/top_level.txt +0 -0
  121. {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/__init__.py +0 -0
  122. {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/conftest.py +0 -0
  123. {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_files/batch_invoices.json +0 -0
  124. {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_files/credit_note.json +0 -0
  125. {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_files/debit_note.json +0 -0
  126. {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_files/freight_invoice.json +0 -0
  127. {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_files/invoice.json +0 -0
  128. {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_files/invoice_invalid_doctype.json +0 -0
  129. {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_files/invoice_missing_doctype.json +0 -0
  130. {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_files/self_billed_credit_note.json +0 -0
  131. {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_files/self_billed_invoice.json +0 -0
  132. {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_integration.py +0 -0
@@ -0,0 +1,108 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ---
9
+
10
+ ## [1.0.1] - 2026-02-03
11
+
12
+ ### 🐛 Fixed
13
+
14
+ - **Multi-page document merging**: Fixed critical issue where only last occurrence of array fields (e.g., InvoiceLine, AllowanceCharge) was retained when same invoice ID appeared multiple times
15
+ - **Hardcoded field names**: Replaced hardcoded array field list with schema-driven detection via `maxOccurs="unbounded"`
16
+ - **Case-insensitive field matching**: Fixed merging when input JSON has mixed case field names (e.g., `InvoiceLine` vs `invoiceline`)
17
+
18
+ ### ✨ Added
19
+
20
+ - **Schema-driven array detection**: `_merge_pages()` now dynamically identifies array fields from schema cache instead of hardcoded list
21
+ - **Unified merge behavior across all APIs**: All 3 APIs (dict, file-to-dict, file-to-files) now use `_group_and_merge_documents()` helper for consistent merging
22
+ - **Support for all document types**: Merge logic now works dynamically with all 60+ UBL document types, not just Invoice-specific fields
23
+ - **Nested array merging**: Properly merges nested array fields (e.g., TaxTotal > TaxSubtotal, AllowanceCharge arrays)
24
+ - **Comprehensive test coverage**:
25
+ - Schema-driven array detection tests
26
+ - Mixed case field name merging tests
27
+ - Multi-invoice batch merging tests
28
+ - Nested array field merging tests
29
+ - Cross-API consistency tests
30
+ - Different document type tests (Invoice, CreditNote)
31
+
32
+ ### 🔄 Changed
33
+
34
+ - `Json2UblConverter._merge_pages()`: Now accepts `schema_cache` parameter for dynamic array field detection
35
+ - `Json2UblConverter.convert_json_file_to_xml_dict()`: Refactored to use new `_group_and_merge_documents()` helper
36
+ - `json_dict_to_ubl_xml()` in `__init__.py`: Now calls `_group_and_merge_documents()` to ensure consistent merge behavior
37
+ - Error handling: Improved error messages for merge failures with proper logging
38
+
39
+ ### 🧪 Testing
40
+
41
+ **New test cases added:**
42
+ - `test_merge_pages_multiple_pages` - Schema-driven array detection with Invoice
43
+ - `test_merge_pages_mixed_case_field_names` - Case-insensitive field matching
44
+ - `test_group_and_merge_documents_standalone` - Direct testing of merge helper
45
+ - `test_group_and_merge_documents_skips_no_id` - Filtering documents without ID
46
+ - `test_merge_pages_schema_driven_different_doc_types` - Different document type schema detection (CreditNote)
47
+ - `test_merge_same_invoice_across_all_apis` - Integration test across all 3 APIs
48
+ - `test_merge_multiple_invoices_same_batch` - Multi-invoice batch merging (4 pages, 2 invoices)
49
+ - `test_merge_with_missing_document_type` - Error handling during merge
50
+ - `test_merge_nested_arrays_in_complex_structure` - TaxTotal nested array merging
51
+ - `test_merge_allowancecharge_arrays` - AllowanceCharge array merging
52
+
53
+ ### 🚀 Data Integrity
54
+
55
+ **Important**: This release fixes a critical data loss issue:
56
+ - **Before v1.0.1**: When invoice ID "728621" appeared twice in input JSON, only line items from last occurrence were in output XML (other pages' items lost)
57
+ - **After v1.0.1**: All line items from all pages with same invoice ID are merged into single valid XML file
58
+
59
+ ### 📝 Documentation
60
+
61
+ - Updated README with multi-page document merging feature
62
+ - Added inline code documentation for schema-driven merging
63
+
64
+ ### 🔧 Technical Details
65
+
66
+ **Key files modified:**
67
+ - `src/json2ubl/converter.py`:
68
+ - Added `_group_and_merge_documents()` helper method (lines 609-654)
69
+ - Enhanced `_merge_pages()` with schema_cache parameter (lines 656-680)
70
+ - Refactored `convert_json_file_to_xml_dict()` (lines 354-407)
71
+
72
+ - `src/json2ubl/__init__.py`:
73
+ - Updated `json_dict_to_ubl_xml()` to use merge helper (lines 79-135)
74
+
75
+ - `tests/test_components.py`:
76
+ - Enhanced merge test cases with schema_cache
77
+
78
+ - `tests/test_error_handling.py`:
79
+ - Updated error handling validation
80
+
81
+ - `tests/test_pipeline.py`:
82
+ - Added integration tests for multi-API consistency
83
+ - Added nested array merging tests
84
+
85
+ ---
86
+
87
+ ## [1.0.0] - 2026-01-30
88
+
89
+ ### ✨ Initial Release
90
+
91
+ - Full UBL 2.1 support (60+ document types)
92
+ - Schema-driven JSON-to-XML mapping
93
+ - Three conversion APIs (dict, file-to-dict, file-to-files)
94
+ - XML validation against UBL schemas
95
+ - Comprehensive error handling with rollback
96
+ - Thread-safe batch processing
97
+ - Pre-built schema caches for performance
98
+ - Extensive test coverage
99
+ - Production-ready logging with loguru
100
+
101
+ ---
102
+
103
+ ## Versioning
104
+
105
+ This project follows [Semantic Versioning](https://semver.org/):
106
+ - **MAJOR**: Incompatible API changes
107
+ - **MINOR**: New features (backward compatible)
108
+ - **PATCH**: Bug fixes (backward compatible)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: json2ubl
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: Production-grade JSON to UBL 2.1 XML converter with schema-driven mapping
5
5
  Author-email: SherozShaikh <shaikh.sheroz07@gmail.com>
6
6
  License: MIT
@@ -22,16 +22,16 @@ Classifier: Topic :: Office/Business :: Financial
22
22
  Requires-Python: >=3.10
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
- Requires-Dist: lxml<5.0,>=4.9.4
26
- Requires-Dist: pydantic<3.0,>=2.7.0
27
- Requires-Dist: pyyaml<7.0,>=6.0.1
28
- Requires-Dist: loguru<1.0,>=0.7.2
25
+ Requires-Dist: lxml~=6.0
26
+ Requires-Dist: pydantic~=2.7
27
+ Requires-Dist: pyyaml~=6.0
28
+ Requires-Dist: loguru~=0.7
29
29
  Provides-Extra: dev
30
- Requires-Dist: pytest<9.0,>=8.3.0; extra == "dev"
31
- Requires-Dist: pytest-cov<6.0,>=5.0.0; extra == "dev"
32
- Requires-Dist: black<25.0,>=24.10.0; extra == "dev"
33
- Requires-Dist: ruff<1.0,>=0.8.0; extra == "dev"
34
- Requires-Dist: mypy<2.0,>=1.13.0; extra == "dev"
30
+ Requires-Dist: pytest~=8.3; extra == "dev"
31
+ Requires-Dist: pytest-cov~=5.0; extra == "dev"
32
+ Requires-Dist: black~=24.10; extra == "dev"
33
+ Requires-Dist: ruff~=0.8; extra == "dev"
34
+ Requires-Dist: mypy~=1.13; extra == "dev"
35
35
  Dynamic: license-file
36
36
 
37
37
  # json2ubl
@@ -41,6 +41,7 @@ Dynamic: license-file
41
41
  [![PyPI version](https://badge.fury.io/py/json2ubl.svg)](https://badge.fury.io/py/json2ubl)
42
42
  [![Python Versions](https://img.shields.io/pypi/pyversions/json2ubl.svg)](https://pypi.org/project/json2ubl/)
43
43
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
44
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
44
45
 
45
46
  [json2ubl](https://pypi.org/project/json2ubl/) is a production-ready converter that transforms JSON documents into UBL 2.1-compliant XML. It works with all 60+ UBL document types using automatic schema-driven mapping—no hardcoded field definitions required.
46
47
 
@@ -211,6 +212,8 @@ Convert JSON file and write XML files to disk.
211
212
  - Rolls back on partial failure
212
213
  - Atomic file operations with temp file staging
213
214
 
215
+ **For detailed API documentation with input/output examples and error handling, see [API.md](docs/API.md)**
216
+
214
217
  ---
215
218
 
216
219
  ## 🛡️ Error Handling
@@ -5,6 +5,7 @@
5
5
  [![PyPI version](https://badge.fury.io/py/json2ubl.svg)](https://badge.fury.io/py/json2ubl)
6
6
  [![Python Versions](https://img.shields.io/pypi/pyversions/json2ubl.svg)](https://pypi.org/project/json2ubl/)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
8
9
 
9
10
  [json2ubl](https://pypi.org/project/json2ubl/) is a production-ready converter that transforms JSON documents into UBL 2.1-compliant XML. It works with all 60+ UBL document types using automatic schema-driven mapping—no hardcoded field definitions required.
10
11
 
@@ -175,6 +176,8 @@ Convert JSON file and write XML files to disk.
175
176
  - Rolls back on partial failure
176
177
  - Atomic file operations with temp file staging
177
178
 
179
+ **For detailed API documentation with input/output examples and error handling, see [API.md](docs/API.md)**
180
+
178
181
  ---
179
182
 
180
183
  ## 🛡️ Error Handling
@@ -0,0 +1,451 @@
1
+ # JSON to UBL 2.1 XML Converter - API Documentation
2
+
3
+ **Package:** `json2ubl>=1.0.0`
4
+ **Installation:** `pip install json2ubl>=1.0.0`
5
+
6
+ ---
7
+
8
+ ## Overview
9
+
10
+ The json2ubl package provides three APIs to convert JSON documents to UBL 2.1-compliant XML format. All APIs support all 60+ UBL document types using schema-driven automatic mapping.
11
+
12
+ ---
13
+
14
+ ## API 1: `json_dict_to_ubl_xml()`
15
+
16
+ Convert a list of JSON dictionaries to UBL XML strings in memory.
17
+
18
+ ### Function Signature
19
+
20
+ ```python
21
+ from json2ubl import json_dict_to_ubl_xml
22
+
23
+ response = json_dict_to_ubl_xml(
24
+ list_of_dicts: List[Dict[str, Any]],
25
+ config_path: str | None = None
26
+ ) -> Dict[str, Any]
27
+ ```
28
+
29
+ ### Parameters
30
+
31
+ | Parameter | Type | Required | Description |
32
+ |-----------|------|----------|-------------|
33
+ | `list_of_dicts` | `List[Dict]` | Yes | List of document dictionaries to convert |
34
+ | `config_path` | `str` | No | Path to optional `ubl_converter.yaml` config file |
35
+
36
+ ### Input Format
37
+
38
+ Each dictionary must contain:
39
+ - `id` - Document identifier (required)
40
+ - `document_type` - Numeric code (required). Examples: 380=Invoice, 381=CreditNote, 382=DebitNote
41
+ - Schema fields matching UBL 2.1 structure (case-insensitive)
42
+
43
+ **Example Input:**
44
+ ```python
45
+ invoices = [
46
+ {
47
+ "id": "INV-2026-001",
48
+ "document_type": 380, # Invoice
49
+ "issue_date": "2026-01-30",
50
+ "accounting_supplier_party": {
51
+ "party_name": "Supplier Inc",
52
+ "party_identification": {"id": "12345"}
53
+ },
54
+ "accounting_customer_party": {
55
+ "party_name": "Customer Ltd"
56
+ },
57
+ "invoice_lines": [
58
+ {
59
+ "id": "1",
60
+ "invoiced_quantity": 10,
61
+ "invoiced_quantity_unit_code": "EA",
62
+ "line_extension_amount": 1000.00
63
+ }
64
+ ]
65
+ }
66
+ ]
67
+
68
+ response = json_dict_to_ubl_xml(invoices)
69
+ ```
70
+
71
+ ### Response Format
72
+
73
+ ```python
74
+ {
75
+ "documents": [
76
+ {
77
+ "id": "INV-2026-001",
78
+ "xml": "<ubl:Invoice xmlns:ubl='urn:...'> ... </ubl:Invoice>",
79
+ "unmapped_fields": ["custom_field_1", "custom_field_2"]
80
+ }
81
+ ],
82
+ "summary": {
83
+ "total_inputs": 1,
84
+ "files_created": 0,
85
+ "document_types": {"Invoice": 1}
86
+ },
87
+ "error_response": None
88
+ }
89
+ ```
90
+
91
+ ### Response Fields
92
+
93
+ | Field | Type | Description |
94
+ |-------|------|-------------|
95
+ | `documents` | `List[Dict]` | List of converted documents |
96
+ | `documents[].id` | `str` | Document identifier from input |
97
+ | `documents[].xml` | `str` | Generated UBL 2.1 XML string |
98
+ | `documents[].unmapped_fields` | `List[str]` | JSON fields not found in UBL schema |
99
+ | `summary` | `Dict` | Conversion statistics |
100
+ | `summary.total_inputs` | `int` | Number of documents processed |
101
+ | `summary.files_created` | `int` | Always 0 for this API |
102
+ | `summary.document_types` | `Dict` | Count by document type (e.g., `{"Invoice": 1}`) |
103
+ | `error_response` | `Dict\|None` | Error details if conversion failed |
104
+
105
+ ### Error Response
106
+
107
+ On failure, `error_response` contains:
108
+
109
+ ```python
110
+ {
111
+ "error_code": "MISSING_DOCUMENT_TYPE", # or INVALID_DOCUMENT_TYPE, etc
112
+ "message": "Missing required field: document_type",
113
+ "details": {
114
+ "doc_id": "INV-2026-001",
115
+ "required_field": "document_type"
116
+ }
117
+ }
118
+ ```
119
+
120
+ ### Usage Example
121
+
122
+ ```python
123
+ from json2ubl import json_dict_to_ubl_xml
124
+
125
+ invoices = [
126
+ {
127
+ "id": "INV-001",
128
+ "document_type": 380,
129
+ "issue_date": "2026-01-31",
130
+ "accounting_supplier_party": {"party_name": "ABC Corp"},
131
+ "accounting_customer_party": {"party_name": "XYZ Ltd"},
132
+ "invoice_lines": [{"id": "1", "line_extension_amount": 500.00}]
133
+ }
134
+ ]
135
+
136
+ response = json_dict_to_ubl_xml(invoices)
137
+
138
+ if response.get("error_response"):
139
+ print(f"Error: {response['error_response']['message']}")
140
+ else:
141
+ for doc in response["documents"]:
142
+ print(f"✓ {doc['id']} converted")
143
+ print(f" Unmapped: {doc['unmapped_fields']}")
144
+ # Save XML or process further
145
+ with open(f"{doc['id']}.xml", "w") as f:
146
+ f.write(doc["xml"])
147
+ ```
148
+
149
+ ### Common Errors
150
+
151
+ | Error Code | Cause | Solution |
152
+ |------------|-------|----------|
153
+ | `MISSING_DOCUMENT_TYPE` | Missing `document_type` field | Add numeric document type code |
154
+ | `INVALID_DOCUMENT_TYPE` | Wrong `document_type` value | Use valid code (380=Invoice, 381=CreditNote, etc) |
155
+ | `MappingError` | Invalid JSON structure | Verify field names match UBL schema |
156
+
157
+ ---
158
+
159
+ ## API 2: `json_file_to_ubl_xml_dict()`
160
+
161
+ Convert JSON file to UBL XML strings and return in memory (batch processing).
162
+
163
+ ### Function Signature
164
+
165
+ ```python
166
+ from json2ubl import json_file_to_ubl_xml_dict
167
+
168
+ response = json_file_to_ubl_xml_dict(
169
+ json_file_path: str,
170
+ config_path: str | None = None
171
+ ) -> Dict[str, Any]
172
+ ```
173
+
174
+ ### Parameters
175
+
176
+ | Parameter | Type | Required | Description |
177
+ |-----------|------|----------|-------------|
178
+ | `json_file_path` | `str` | Yes | Path to JSON file containing list of documents |
179
+ | `config_path` | `str` | No | Path to optional config file |
180
+
181
+ ### Input Format
182
+
183
+ JSON file must contain a list array:
184
+
185
+ **example.json:**
186
+ ```json
187
+ [
188
+ {
189
+ "id": "INV-001",
190
+ "document_type": 380,
191
+ "issue_date": "2026-01-31",
192
+ "accounting_supplier_party": {"party_name": "Supplier"},
193
+ "accounting_customer_party": {"party_name": "Customer"},
194
+ "invoice_lines": [{"id": "1", "line_extension_amount": 1000}]
195
+ },
196
+ {
197
+ "id": "INV-002",
198
+ "document_type": 380,
199
+ "issue_date": "2026-02-01",
200
+ ...
201
+ }
202
+ ]
203
+ ```
204
+
205
+ ### Response Format
206
+
207
+ Same as API 1:
208
+
209
+ ```python
210
+ {
211
+ "documents": [
212
+ {"id": "INV-001", "xml": "...", "unmapped_fields": []},
213
+ {"id": "INV-002", "xml": "...", "unmapped_fields": []}
214
+ ],
215
+ "summary": {
216
+ "total_inputs": 2,
217
+ "files_created": 0,
218
+ "document_types": {"Invoice": 2}
219
+ },
220
+ "error_response": None
221
+ }
222
+ ```
223
+
224
+ ### Usage Example
225
+
226
+ ```python
227
+ from json2ubl import json_file_to_ubl_xml_dict
228
+
229
+ response = json_file_to_ubl_xml_dict("invoices.json")
230
+
231
+ if response.get("error_response"):
232
+ print(f"Error: {response['error_response']['message']}")
233
+ print(f"Details: {response['error_response']['details']}")
234
+ else:
235
+ print(f"✓ Converted {len(response['documents'])} documents")
236
+ print(f" Summary: {response['summary']}")
237
+
238
+ for doc in response["documents"]:
239
+ print(f" - {doc['id']}: {len(doc['unmapped_fields'])} unmapped fields")
240
+ ```
241
+
242
+ ### Common Errors
243
+
244
+ | Error | Cause | Solution |
245
+ |-------|-------|----------|
246
+ | `FileNotFoundError` | JSON file not found | Verify file path exists |
247
+ | `JSONDecodeError` | Invalid JSON format | Validate JSON syntax |
248
+ | `MISSING_DOCUMENT_TYPE` | Document missing `document_type` | Add type to all documents |
249
+
250
+ ---
251
+
252
+ ## API 3: `json_file_to_ubl_xml_files()`
253
+
254
+ Convert JSON file and write XML files directly to disk.
255
+
256
+ ### Function Signature
257
+
258
+ ```python
259
+ from json2ubl import json_file_to_ubl_xml_files
260
+
261
+ response = json_file_to_ubl_xml_files(
262
+ json_file_path: str,
263
+ output_dir: str,
264
+ config_path: str | None = None
265
+ ) -> Dict[str, Any]
266
+ ```
267
+
268
+ ### Parameters
269
+
270
+ | Parameter | Type | Required | Description |
271
+ |-----------|------|----------|-------------|
272
+ | `json_file_path` | `str` | Yes | Path to input JSON file |
273
+ | `output_dir` | `str` | Yes | Directory where XML files will be written |
274
+ | `config_path` | `str` | No | Path to optional config file |
275
+
276
+ ### Response Format
277
+
278
+ Same structure as API 1 & 2, but **without** XML content:
279
+
280
+ ```python
281
+ {
282
+ "documents": [
283
+ {
284
+ "id": "INV-001",
285
+ "unmapped_fields": []
286
+ },
287
+ {
288
+ "id": "INV-002",
289
+ "unmapped_fields": []
290
+ }
291
+ ],
292
+ "summary": {
293
+ "total_inputs": 2,
294
+ "files_created": 2,
295
+ "document_types": {"Invoice": 2}
296
+ },
297
+ "error_response": None
298
+ }
299
+ ```
300
+
301
+ ### Response Fields
302
+
303
+ | Field | Type | Description |
304
+ |-------|------|-------------|
305
+ | `documents` | `List[Dict]` | List of processed documents (no XML) |
306
+ | `documents[].id` | `str` | Document identifier |
307
+ | `documents[].unmapped_fields` | `List[str]` | Fields not in UBL schema |
308
+ | `summary.files_created` | `int` | Number of XML files written to disk |
309
+ | `error_response` | `Dict\|None` | Error if conversion failed |
310
+
311
+ **Note:** `xml` field is omitted to reduce response payload when writing to disk.
312
+
313
+ ### Output Files
314
+
315
+ Generated files are named: `{document_id}.xml`
316
+
317
+ ```
318
+ output_dir/
319
+ ├── INV-001.xml
320
+ ├── INV-002.xml
321
+ └── CR-001.xml
322
+ ```
323
+
324
+ ### Usage Example
325
+
326
+ ```python
327
+ from json2ubl import json_file_to_ubl_xml_files
328
+ import os
329
+
330
+ response = json_file_to_ubl_xml_files(
331
+ json_file_path="batch_invoices.json",
332
+ output_dir="./output_xml"
333
+ )
334
+
335
+ if response.get("error_response"):
336
+ print(f"❌ Error: {response['error_response']['message']}")
337
+ else:
338
+ files_created = response["summary"]["files_created"]
339
+ print(f"✓ Successfully created {files_created} XML files")
340
+ print(f" Location: {os.path.abspath('./output_xml')}")
341
+
342
+ for doc in response["documents"]:
343
+ unmapped = len(doc["unmapped_fields"])
344
+ print(f" - {doc['id']}: {unmapped} unmapped fields")
345
+ ```
346
+
347
+ ### Common Errors
348
+
349
+ | Error | Cause | Solution |
350
+ |-------|-------|----------|
351
+ | `FileNotFoundError` | Input file not found | Verify input JSON path |
352
+ | `PermissionError` | Cannot write to output_dir | Check directory permissions |
353
+ | `INVALID_DOCUMENT_TYPE` | Document type invalid | Fix document_type values |
354
+
355
+ ---
356
+
357
+ ## Supported Document Types
358
+
359
+ The converter supports all 60+ UBL 2.1 document types. Common ones:
360
+
361
+ | Code | Document Type |
362
+ |------|----------------|
363
+ | 380 | Invoice |
364
+ | 381 | Credit Note |
365
+ | 382 | Debit Note |
366
+ | 220 | Order |
367
+ | 225 | Order Change |
368
+ | 230 | Order Cancellation |
369
+
370
+ For full list, see [UBL 2.1 Specification](https://docs.oasis-open.org/ubl/os-UBL-2.1/)
371
+
372
+ ---
373
+
374
+ ## Field Mapping
375
+
376
+ - Input JSON keys are **case-insensitive** (automatically normalized to lowercase)
377
+ - Fields not in UBL schema are tracked in `unmapped_fields`
378
+ - Null values are preserved as empty XML elements
379
+ - Nested structures follow UBL hierarchy automatically
380
+
381
+ ---
382
+
383
+ ## Configuration (Optional)
384
+
385
+ Create `ubl_converter.yaml` for custom settings:
386
+
387
+ ```yaml
388
+ schema_root: "./custom_schemas"
389
+ max_recursion_depth: 20
390
+ enable_logging: true
391
+ log_file: "converter.log"
392
+ ```
393
+
394
+ Pass to any API:
395
+ ```python
396
+ response = json_dict_to_ubl_xml(invoices, config_path="ubl_converter.yaml")
397
+ ```
398
+
399
+ ---
400
+
401
+ ## Best Practices
402
+
403
+ 1. **Validate input before conversion** - Ensure `id` and `document_type` are present
404
+ 2. **Check `unmapped_fields`** - Review fields not in UBL schema
405
+ 3. **Handle errors gracefully** - Always check `error_response` before processing
406
+ 4. **Use API 3 for large batches** - Better for 1000+ documents (writes to disk directly)
407
+ 5. **Log conversions** - Track which documents failed for auditing
408
+
409
+ ---
410
+
411
+ ## Error Handling Pattern
412
+
413
+ ```python
414
+ from json2ubl import json_dict_to_ubl_xml
415
+
416
+ def convert_safely(documents):
417
+ response = json_dict_to_ubl_xml(documents)
418
+
419
+ # Check for errors
420
+ if response.get("error_response"):
421
+ error = response["error_response"]
422
+ print(f"Error Code: {error['error_code']}")
423
+ print(f"Message: {error['message']}")
424
+ print(f"Details: {error['details']}")
425
+ return None
426
+
427
+ # Process successful conversions
428
+ for doc in response["documents"]:
429
+ if doc["unmapped_fields"]:
430
+ print(f"Warning: {doc['id']} has unmapped fields: {doc['unmapped_fields']}")
431
+
432
+ # Use XML or save to file
433
+ print(f"✓ {doc['id']} converted successfully")
434
+
435
+ return response
436
+
437
+ # Usage
438
+ result = convert_safely(invoices)
439
+ ```
440
+
441
+ ---
442
+
443
+ ## Summary
444
+
445
+ | API | Use Case | Output | Best For |
446
+ |-----|----------|--------|----------|
447
+ | **API 1** | Single/few documents | XML in memory | Real-time API responses |
448
+ | **API 2** | Batch processing | XML in memory | Reports, archives |
449
+ | **API 3** | Bulk files | XML on disk | High-volume batch jobs |
450
+
451
+ All three APIs return consistent response structure with documents, summary, and error_response fields.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "json2ubl"
7
- version = "1.0.0"
7
+ version = "1.0.1"
8
8
  description = "Production-grade JSON to UBL 2.1 XML converter with schema-driven mapping"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -27,19 +27,19 @@ classifiers = [
27
27
  ]
28
28
 
29
29
  dependencies = [
30
- "lxml>=4.9.4,<5.0",
31
- "pydantic>=2.7.0,<3.0",
32
- "pyyaml>=6.0.1,<7.0",
33
- "loguru>=0.7.2,<1.0",
30
+ "lxml~=6.0",
31
+ "pydantic~=2.7",
32
+ "pyyaml~=6.0",
33
+ "loguru~=0.7",
34
34
  ]
35
35
 
36
36
  [project.optional-dependencies]
37
37
  dev = [
38
- "pytest>=8.3.0,<9.0",
39
- "pytest-cov>=5.0.0,<6.0",
40
- "black>=24.10.0,<25.0",
41
- "ruff>=0.8.0,<1.0",
42
- "mypy>=1.13.0,<2.0",
38
+ "pytest~=8.3",
39
+ "pytest-cov~=5.0",
40
+ "black~=24.10",
41
+ "ruff~=0.8",
42
+ "mypy~=1.13",
43
43
  ]
44
44
 
45
45
  [project.urls]