Skip to content

Commit 383ea72

Browse files
Magnus Hartvig Grønbechclaude
andcommitted
Add AL documentation for PEPPOL
Bootstrap docs for the PEPPOL app using al-docs init. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e98ea20 commit 383ea72

5 files changed

Lines changed: 529 additions & 0 deletions

File tree

src/Apps/W1/PEPPOL/App/CLAUDE.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# PEPPOL BIS 3.0 Electronic Invoicing
2+
3+
Export engine that transforms posted Sales and Service documents into
4+
PEPPOL BIS 3.0 UBL XML for European e-invoicing compliance.
5+
6+
## What it does
7+
8+
Reads posted invoices/credit memos (sales **and** service), validates them
9+
against PEPPOL business rules, then serializes to UBL 2.1 XML via
10+
dedicated XmlPorts. No import path exists -- this is export-only.
11+
12+
## Architecture at a glance
13+
14+
The entire app is built on a **strategy/interface pattern** with a single
15+
extensible enum (`PEPPOL 3.0 Format`) as the dispatch mechanism:
16+
17+
```
18+
Setup table --> Enum "PEPPOL 3.0 Format" --> 10 interfaces --> Codeunit implementations
19+
(Sales | Service) (one per UBL domain)
20+
```
21+
22+
The enum implements all 10 interfaces simultaneously. Each enum value
23+
selects different implementations for **validation** and **document
24+
iteration**, while sharing the same data-extraction implementations
25+
(the `PEPPOL30` codeunit).
26+
27+
### Key design decisions
28+
29+
- **Sales Header as universal buffer.** All document types (Sales Invoice,
30+
Sales Cr.Memo, Service Invoice, Service Cr.Memo) are converted to
31+
`Sales Header`/`Sales Line` temporary records before processing. This
32+
lets the XmlPorts and data-extraction code work against a single record
33+
type regardless of source. The conversion happens in `PEPPOL30 Common`.
34+
35+
- **Facade + Impl split.** `PEPPOL30` (public facade) delegates every call
36+
to `PEPPOL30 Impl.` (Access = Internal). Extend via enum + interface,
37+
not by modifying these codeunits.
38+
39+
- **No custom tables beyond setup.** The only table is `PEPPOL 3.0 Setup`
40+
(singleton). All data comes from standard BC tables: Company Information,
41+
Customer, Sales Header/Line, VAT Posting Setup, etc.
42+
43+
## Object inventory
44+
45+
| Type | Count | ID range |
46+
|------|-------|----------|
47+
| Table | 1 | 37202 |
48+
| Page | 1 | 37202 |
49+
| Enum | 1 | 37200 |
50+
| Interface | 10 | -- |
51+
| Codeunit | 15 | 37200-37220 |
52+
| XmlPort | 2 | 37200-37201 |
53+
| **Total** | **30** | **37200-37300** |
54+
55+
## Folder map
56+
57+
| Folder | Purpose | Score |
58+
|--------|---------|-------|
59+
| `src/Common/` | Core enum, facade, impl, converter, subscribers | **5/5** -- start here |
60+
| `src/Interfaces/` | All 10 interface definitions | **4/5** -- the contract layer |
61+
| `src/Sales/` | Sales export codeunits, validation, XmlPorts | **4/5** -- primary export path |
62+
| `src/Services/` | Service doc export (thin layer over Sales) | **3/5** -- delegates to Sales validation |
63+
| `src/Setup/` | Singleton setup table + page | **2/5** -- simple config |
64+
| `src/Install/` | Install + upgrade codeunits | **2/5** -- boilerplate |
65+
66+
## Extending this app
67+
68+
The enum is `Extensible = true`. To add a new PEPPOL format variant:
69+
70+
1. Add a new enum value to `PEPPOL 3.0 Format`
71+
2. Provide implementations for `PEPPOL30 Validation` and
72+
`PEPPOL Posted Document Iterator` (the two format-specific interfaces)
73+
3. The remaining 8 interfaces use `DefaultImplementation = "PEPPOL30"`
74+
-- override only what differs
75+
76+
Country localizations typically override validation rules and possibly
77+
party info providers (to handle local VAT registration formats).
78+
79+
## Gotchas
80+
81+
- **Denmark special case.** `UseVATSchemeID()` returns true only for
82+
`DK` (ISO code). This affects VAT registration number formatting in
83+
BIS billing mode. Other countries get a simpler format.
84+
85+
- **Invoice rounding lines are excluded** from the XML line items and
86+
instead appear as `PayableRoundingAmount` in the monetary totals. The
87+
detection logic uses `Customer Posting Group."Invoice Rounding Account"`.
88+
89+
- **Credit memos require Applies-to Doc.** Validation enforces that
90+
credit memos reference the original invoice via `Applies-to Doc. No.`
91+
when `Applies-to Doc. Type` is Invoice.
92+
93+
- **Outside-scope VAT (category O)** is limited to a single VAT breakdown
94+
per document. Multiple O-category posting setups will fail validation.
95+
96+
- **Service documents reuse Sales validation.** The service validation
97+
impl converts to Sales Header/Line and delegates entirely to the sales
98+
validation codeunit. There is no separate service-specific validation.
99+
100+
## See also
101+
102+
- [docs/data-model.md](docs/data-model.md) -- Setup table, enum, and data sources
103+
- [docs/business-logic.md](docs/business-logic.md) -- Export pipeline and validation
104+
- [docs/extensibility.md](docs/extensibility.md) -- Interfaces and extension points
105+
- [docs/patterns.md](docs/patterns.md) -- Architectural patterns used
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Business Logic
2+
3+
## Export pipeline
4+
5+
Every export follows the same sequence regardless of document type:
6+
7+
```mermaid
8+
flowchart TD
9+
A[Record Export Buffer triggers codeunit] --> B[Load Setup + resolve format enum]
10+
B --> C{Validate posted document}
11+
C -->|Fail| ERR[Error raised -- export aborted]
12+
C -->|Pass| D[Initialize XmlPort with RecordRef + Format]
13+
D --> E[XmlPort iterates headers via PEPPOL Posted Document Iterator]
14+
E --> F[Convert posted record to Sales Header buffer]
15+
F --> G[Extract all UBL data sections via 8 provider interfaces]
16+
G --> H[XmlPort iterates lines via same iterator]
17+
H --> I[Convert each posted line to Sales Line buffer]
18+
I --> J[Extract line-level UBL data]
19+
J --> K[Serialize to UBL XML on OutStream]
20+
K --> L[Write XML to Record Export Buffer."File Content"]
21+
```
22+
23+
### Entry points
24+
25+
There are 4 export codeunits, each bound to `Record Export Buffer`:
26+
27+
| Codeunit | Source table | Format field |
28+
|----------|-------------|--------------|
29+
| Exp. Sales Inv. PEPPOL30 (37206) | Sales Invoice Header | Sales Format |
30+
| Exp. Sales CrM. PEPPOL30 (37205) | Sales Cr.Memo Header | Sales Format |
31+
| Exp. Serv.Inv. PEPPOL30 (37212) | Service Invoice Header | Service Format |
32+
| Exp. Serv.CrM. PEPPOL30 (37211) | Service Cr.Memo Header | Service Format |
33+
34+
All four follow the identical pattern: get record from buffer, validate,
35+
generate XML. The service codeunits reuse the same `Sales Invoice - PEPPOL30`
36+
XmlPort (not a typo -- service documents are converted to Sales Header
37+
buffers before the XmlPort sees them).
38+
39+
### The "Sales Header as universal buffer" trick
40+
41+
`PEPPOL30 Common.ConvertPostedHeaderToSalesHeader()` is the conversion
42+
hub. It uses `TransferFields` for sales documents and
43+
`PEPPOL30.TransferHeaderToSalesHeader()` (RecordRef field-by-field copy)
44+
for service documents. The document type field is explicitly set after
45+
transfer:
46+
47+
- Invoice headers -> `Document Type::Invoice`
48+
- Credit memo headers -> `Document Type::"Credit Memo"`
49+
50+
For service lines, `MapServiceLineTypeToSalesLineType()` maps service
51+
line types to their sales equivalents. Service types that have no
52+
direct sales match (Cost, G/L Account) map to `Sales Line Type::"G/L Account"`.
53+
54+
## Validation
55+
56+
Validation runs **before** XML generation and will abort the export with
57+
an error if any check fails.
58+
59+
```mermaid
60+
flowchart TD
61+
V1[ValidatePostedDocument] --> V2{Document type?}
62+
V2 -->|Sales Invoice| V3[TransferFields to Sales Header]
63+
V2 -->|Sales Cr.Memo| V3
64+
V2 -->|Service Invoice| V4[RecRefTransferFields to Sales Header]
65+
V2 -->|Service Cr.Memo| V4
66+
V4 --> V3
67+
V3 --> V5[CheckSalesDocument - header-level]
68+
V5 --> V6[CheckSalesDocumentLine - per line]
69+
70+
V5 --> H1[Currency code = 3 chars]
71+
V5 --> H2[Company Info: name, address, country, GLN or VAT]
72+
V5 --> H3[Bill-to: name, address, city, post code, country]
73+
V5 --> H4[Customer: GLN or VAT Reg No.]
74+
V5 --> H5[Ship-to address complete]
75+
V5 --> H6[Due date, Your Reference filled]
76+
V5 --> H7[Bank: IBAN or account + branch + SWIFT]
77+
V5 --> H8[Credit memo: Applies-to Doc. No. if type is Invoice]
78+
79+
V6 --> L1[UoM has international standard code]
80+
V6 --> L2[Non-blank lines have description]
81+
V6 --> L3[VAT Prod. Posting Group filled]
82+
V6 --> L4[Tax Category checks]
83+
V6 --> L5[Negative unit price confirmation]
84+
85+
L4 --> TC1{Tax Category}
86+
TC1 -->|S| TC2[VAT % must be > 0]
87+
TC1 -->|Z,E,AE,K,G| TC3[VAT % must be 0]
88+
TC1 -->|O| TC4[VAT % = 0 + single breakdown only]
89+
```
90+
91+
### Validation gotchas
92+
93+
- **Service validation delegates to Sales.** `PEPPOL30 Serv. Validation Impl`
94+
converts service records to sales records and calls the sales validation
95+
impl. Shipment Date is set to Posting Date (services don't have a
96+
shipment date).
97+
98+
- **Country codes must have 2-char ISO codes.** The check reads
99+
`Country/Region."ISO Code"` and errors if not exactly 2 characters.
100+
101+
- **GLN-or-VAT is enforced** for both Company Information (supplier) and
102+
Customer (buyer). Missing both is a hard error.
103+
104+
## Tax category mapping
105+
106+
The PEPPOL tax categories come from `VAT Posting Setup."Tax Category"`:
107+
108+
| Code | Meaning | VAT % rule |
109+
|------|---------|------------|
110+
| S | Standard rate | Must be > 0% |
111+
| Z | Zero rated | Must be 0% |
112+
| E | Exempt from tax | Must be 0% |
113+
| AE | VAT reverse charge | Must be 0% |
114+
| K | EEA intra-community | Must be 0% |
115+
| G | Free export, not charged | Must be 0% |
116+
| O | Outside scope of VAT | Must be 0%, max 1 breakdown |
117+
118+
## Monetary totals calculation
119+
120+
`GetLegalMonetaryInfo` in `PEPPOL30 Impl.` computes:
121+
122+
- **LineExtensionAmount** = VAT Base + Invoice Discount Amount (pre-discount line total)
123+
- **TaxExclusiveAmount** = VAT Base - Payment Discount Amount
124+
- **TaxInclusiveAmount** = Amount Including VAT - Payment Discount Amount
125+
- **AllowanceTotalAmount** = Invoice Discount Amount + Payment Discount Amount
126+
- **PayableRoundingAmount** = difference from rounding line (or fractional cents)
127+
- **PayableAmount** = Amount Including VAT + rounding - Payment Discount (rounded to 0.01)
128+
129+
## Installation and upgrade
130+
131+
`PEPPOL30 Initialize` (Install subtype) registers 6 electronic document
132+
format entries in the `Electronic Document Format` table:
133+
134+
- Sales Invoice export, Sales Credit Memo export, Sales Validation
135+
- Service Invoice export, Service Credit Memo export, Service Validation
136+
137+
The upgrade codeunit re-runs the same registration with an upgrade tag
138+
(`MS-121225-PEPPOL1P-APP-INSTALL`). A `Company-Initialize` event
139+
subscriber also ensures formats exist in new companies.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Data Model
2+
3+
## Own tables
4+
5+
The app owns exactly **one** table:
6+
7+
### PEPPOL 3.0 Setup (37202)
8+
9+
Singleton (Code[10] primary key, never set). Stores which PEPPOL format
10+
enum value to use for Sales and Service document export.
11+
12+
| Field | Type | Purpose |
13+
|-------|------|---------|
14+
| Primary Key | Code[10] | Always blank -- singleton pattern |
15+
| PEPPOL 3.0 Sales Format | Enum "PEPPOL 3.0 Format" | Format for sales invoice/credit memo export |
16+
| PEPPOL 3.0 Service Format | Enum "PEPPOL 3.0 Format" | Format for service invoice/credit memo export |
17+
18+
The `OnInsert` trigger defaults both fields to their respective enum
19+
values (`PEPPOL 3.0 - Sales` and `PEPPOL 3.0 - Service`). The `GetSetup`
20+
method auto-creates the record on first access with feature telemetry
21+
logging.
22+
23+
## Enum
24+
25+
### PEPPOL 3.0 Format (37200)
26+
27+
The central dispatch enum. Implements all 10 interfaces simultaneously.
28+
`Extensible = true` -- localizations add new values.
29+
30+
| Value | Caption | Overrides |
31+
|-------|---------|-----------|
32+
| 0 | PEPPOL 3.0 - Sales | Validation -> Sales Validation, Iterator -> Sales Iterator |
33+
| 1 | PEPPOL 3.0 - Service | Validation -> Service Validation, Iterator -> Services Iterator |
34+
35+
All 8 data-extraction interfaces use `DefaultImplementation = "PEPPOL30"`,
36+
so both enum values share the same data-extraction logic. Only validation
37+
and document iteration differ.
38+
39+
## External data sources
40+
41+
The app reads from (but never writes to) these BC standard tables:
42+
43+
```mermaid
44+
erDiagram
45+
PEPPOL_SETUP ||--|| PEPPOL_FORMAT : selects
46+
PEPPOL_FORMAT ||--|{ EXPORT_CODEUNIT : dispatches
47+
48+
COMPANY_INFORMATION ||--o| EXPORT_CODEUNIT : "supplier party"
49+
CUSTOMER ||--o| EXPORT_CODEUNIT : "customer party"
50+
SALES_INVOICE_HEADER ||--o| EXPORT_CODEUNIT : "source doc"
51+
SALES_CR_MEMO_HEADER ||--o| EXPORT_CODEUNIT : "source doc"
52+
SERVICE_INVOICE_HEADER ||--o| EXPORT_CODEUNIT : "source doc"
53+
SERVICE_CR_MEMO_HEADER ||--o| EXPORT_CODEUNIT : "source doc"
54+
55+
SALES_INVOICE_HEADER ||--|{ SALES_INVOICE_LINE : lines
56+
SALES_CR_MEMO_HEADER ||--|{ SALES_CR_MEMO_LINE : lines
57+
SERVICE_INVOICE_HEADER ||--|{ SERVICE_INVOICE_LINE : lines
58+
SERVICE_CR_MEMO_HEADER ||--|{ SERVICE_CR_MEMO_LINE : lines
59+
60+
VAT_POSTING_SETUP ||--o| EXPORT_CODEUNIT : "tax categories"
61+
VAT_AMOUNT_LINE ||--o| EXPORT_CODEUNIT : "tax totals"
62+
COUNTRY_REGION ||--o| EXPORT_CODEUNIT : "ISO codes"
63+
UNIT_OF_MEASURE ||--o| EXPORT_CODEUNIT : "UNECERec20 codes"
64+
DOCUMENT_ATTACHMENT ||--o| EXPORT_CODEUNIT : "embedded attachments"
65+
ITEM ||--o| EXPORT_CODEUNIT : "GTIN, item info"
66+
ITEM_VARIANT ||--o| EXPORT_CODEUNIT : "additional properties"
67+
PAYMENT_TERMS ||--o| EXPORT_CODEUNIT : "payment note"
68+
RESPONSIBILITY_CENTER ||--o| EXPORT_CODEUNIT : "supplier address override"
69+
SALESPERSON ||--o| EXPORT_CODEUNIT : "contact info"
70+
SHIP_TO_ADDRESS ||--o| EXPORT_CODEUNIT : "delivery GLN"
71+
```
72+
73+
### Key data flows
74+
75+
**Supplier party** -- sourced from `Company Information`. If a
76+
`Responsibility Center` is set on the document, address fields are
77+
overridden from that center. GLN takes priority over VAT Registration No.
78+
for endpoint identification when `Use GLN in Electronic Document` is set.
79+
80+
**Customer party** -- sourced from `Customer` (via `Bill-to Customer No.`)
81+
and `Sales Header` bill-to fields. Same GLN-vs-VAT priority logic.
82+
83+
**Tax information** -- derived from `VAT Posting Setup."Tax Category"`.
84+
The tax category code (S, Z, E, AE, K, G, O) drives both validation
85+
rules and XML output. `VAT Amount Line` is built up line-by-line during
86+
export, not read from a persisted table.
87+
88+
**Attachments** -- `Document Attachment` records linked to the posted
89+
document are base64-encoded and embedded in the XML as
90+
`AdditionalDocumentReference` elements. MIME type is inferred from
91+
file extension; unsupported types are silently skipped.
92+
93+
**Currency** -- when `Sales Header."Currency Code"` is blank, the
94+
`General Ledger Setup."LCY Code"` is used. Must be a 3-character ISO
95+
4217 code.

0 commit comments

Comments
 (0)