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.
- json2ubl-1.0.1/CHANGELOG.md +108 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/PKG-INFO +13 -10
- {json2ubl-1.0.0 → json2ubl-1.0.1}/README.md +3 -0
- json2ubl-1.0.1/docs/API.md +451 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/pyproject.toml +10 -10
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/__init__.py +27 -21
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/config.py +1 -1
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/converter.py +83 -30
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl.egg-info/PKG-INFO +13 -10
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl.egg-info/SOURCES.txt +2 -0
- json2ubl-1.0.1/src/json2ubl.egg-info/requires.txt +11 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_components.py +68 -5
- {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_error_handling.py +22 -3
- {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_pipeline.py +130 -0
- json2ubl-1.0.0/src/json2ubl.egg-info/requires.txt +0 -11
- {json2ubl-1.0.0 → json2ubl-1.0.1}/.gitattributes +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/.github/workflows/tests.yml +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/.gitignore +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/LICENSE +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/Makefile +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/config/ubl_converter.yaml +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/setup.cfg +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/setup.py +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/__init__.py +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/benchmark.py +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/benchmark_examples.py +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/constants.py +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/core/__init__.py +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/core/mapper.py +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/core/schema_cache_builder.py +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/core/serializer.py +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/core/validator.py +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/exceptions.py +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/cache/CreditNote_schema_cache.json +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/cache/DebitNote_schema_cache.json +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/cache/FreightInvoice_schema_cache.json +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/cache/Invoice_schema_cache.json +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/cache/SelfBilledCreditNote_schema_cache.json +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/cache/SelfBilledInvoice_schema_cache.json +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/CCTS_CCT_SchemaModule-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-CommonAggregateComponents-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-CommonBasicComponents-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-CommonExtensionComponents-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-CommonSignatureComponents-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-CoreComponentParameters-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-ExtensionContentDataType-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-QualifiedDataTypes-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-SignatureAggregateComponents-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-SignatureBasicComponents-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-UnqualifiedDataTypes-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-XAdESv132-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-XAdESv141-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/common/UBL-xmldsig-core-schema-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-ApplicationResponse-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-AttachedDocument-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-AwardedNotification-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-BillOfLading-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-CallForTenders-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-Catalogue-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-CatalogueDeletion-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-CatalogueItemSpecificationUpdate-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-CataloguePricingUpdate-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-CatalogueRequest-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-CertificateOfOrigin-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-ContractAwardNotice-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-ContractNotice-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-CreditNote-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-DebitNote-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-DespatchAdvice-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-DocumentStatus-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-DocumentStatusRequest-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-ExceptionCriteria-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-ExceptionNotification-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-Forecast-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-ForecastRevision-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-ForwardingInstructions-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-FreightInvoice-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-FulfilmentCancellation-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-GoodsItemItinerary-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-GuaranteeCertificate-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-InstructionForReturns-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-InventoryReport-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-Invoice-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-ItemInformationRequest-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-Order-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-OrderCancellation-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-OrderChange-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-OrderResponse-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-OrderResponseSimple-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-PackingList-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-PriorInformationNotice-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-ProductActivity-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-Quotation-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-ReceiptAdvice-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-Reminder-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-RemittanceAdvice-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-RequestForQuotation-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-RetailEvent-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-SelfBilledCreditNote-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-SelfBilledInvoice-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-Statement-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-StockAvailabilityReport-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-Tender-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TenderReceipt-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TendererQualification-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TendererQualificationResponse-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TradeItemLocationProfile-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TransportExecutionPlan-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TransportExecutionPlanRequest-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TransportProgressStatus-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TransportProgressStatusRequest-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TransportServiceDescription-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TransportServiceDescriptionRequest-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TransportationStatus-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-TransportationStatusRequest-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-UnawardedNotification-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-UtilityStatement-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl/schemas/ubl-2.1/maindoc/UBL-Waybill-2.1.xsd +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl.egg-info/dependency_links.txt +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/src/json2ubl.egg-info/top_level.txt +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/__init__.py +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/conftest.py +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_files/batch_invoices.json +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_files/credit_note.json +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_files/debit_note.json +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_files/freight_invoice.json +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_files/invoice.json +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_files/invoice_invalid_doctype.json +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_files/invoice_missing_doctype.json +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_files/self_billed_credit_note.json +0 -0
- {json2ubl-1.0.0 → json2ubl-1.0.1}/tests/test_files/self_billed_invoice.json +0 -0
- {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.
|
|
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
|
|
26
|
-
Requires-Dist: pydantic
|
|
27
|
-
Requires-Dist: pyyaml
|
|
28
|
-
Requires-Dist: loguru
|
|
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
|
|
31
|
-
Requires-Dist: pytest-cov
|
|
32
|
-
Requires-Dist: black
|
|
33
|
-
Requires-Dist: ruff
|
|
34
|
-
Requires-Dist: mypy
|
|
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
|
[](https://badge.fury.io/py/json2ubl)
|
|
42
42
|
[](https://pypi.org/project/json2ubl/)
|
|
43
43
|
[](https://opensource.org/licenses/MIT)
|
|
44
|
+
[](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
|
[](https://badge.fury.io/py/json2ubl)
|
|
6
6
|
[](https://pypi.org/project/json2ubl/)
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
[](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.
|
|
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
|
|
31
|
-
"pydantic
|
|
32
|
-
"pyyaml
|
|
33
|
-
"loguru
|
|
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
|
|
39
|
-
"pytest-cov
|
|
40
|
-
"black
|
|
41
|
-
"ruff
|
|
42
|
-
"mypy
|
|
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]
|