-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathubl.go
More file actions
219 lines (198 loc) · 5.87 KB
/
Copy pathubl.go
File metadata and controls
219 lines (198 loc) · 5.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
// Package ubl helps convert GOBL into UBL documents and vice versa.
package ubl
import (
"bytes"
"encoding/xml"
"fmt"
"io"
"github.com/invopop/gobl"
"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/cbc"
"github.com/invopop/xmlctx"
)
// setEnvelopeRouting records the transport addresses the document was received
// with (opts' WithRouting) on the envelope's Head.From / Head.To, BEFORE the
// envelope is calculated. The addresses are the fully-qualified participant
// URIs the routing layer supplies and are recorded verbatim. GOBL's
// normalizeRouting only fills empty routing fields, so setting them first makes
// it respect them rather than derive them from the document assuming an
// outgoing (supplier→customer) direction — wrong for a received document.
// Applies to any document type.
func setEnvelopeRouting(env *gobl.Envelope, o *options) {
env.Head.From = o.from
env.Head.To = o.to
}
var (
// ErrUnknownDocumentType is returned when the document type
// is not recognized during parsing.
ErrUnknownDocumentType = fmt.Errorf("unknown document type")
// ErrUnsupportedDocumentType is returned when the document type
// is not supported for conversion.
ErrUnsupportedDocumentType = fmt.Errorf("unsupported document type")
)
// Version is the version of UBL documents that will be generated
// by this package.
const Version = "2.1"
// Parse parses a raw UBL document and returns the underlying Go struct.
// The returned value should be type asserted to the appropriate type.
//
// Supported types:
// - *Invoice (for both Invoice and CreditNote documents)
//
// Example usage:
//
// doc, err := ubl.Parse(xmlData)
// if err != nil {
// // handle error
// }
// if inv, ok := doc.(*ubl.Invoice); ok {
// env, err := inv.Convert()
// attachments := inv.ExtractBinaryAttachments()
// // ...
// }
func Parse(data []byte) (any, error) {
ns, err := extractRootNamespace(data)
if err != nil {
return nil, err
}
switch ns {
case NamespaceUBLInvoice, NamespaceUBLCreditNote:
in := new(Invoice)
if err := xmlctx.Unmarshal(data, in, xmlctx.WithNamespaces(map[string]string{
"": ns,
"cbc": NamespaceCBC,
"cac": NamespaceCAC,
"qdt": NamespaceQDT,
"udt": NamespaceUDT,
"ccts": NamespaceCCTS,
"xsi": NamespaceXSI,
"ext": NamespaceEXT,
})); err != nil {
return nil, err
}
return in, nil
case NamespaceUBLApplicationResponse:
ar := new(ApplicationResponse)
if err := xmlctx.Unmarshal(data, ar, xmlctx.WithNamespaces(map[string]string{
"": ns,
"cbc": NamespaceCBC,
"cac": NamespaceCAC,
})); err != nil {
return nil, err
}
return ar, nil
// Future document types can be added here
// case NamespaceUBLOrder:
// order := new(Order)
// if err := xmlctx.Parse(data, order, xmlctx.WithNamespaces(map[string]string{
// "cbc": NamespaceCBC,
// "cac": NamespaceCAC,
// "qdt": NamespaceQDT,
// "udt": NamespaceUDT,
// "ccts": NamespaceCCTS,
// "xsi": NamespaceXSI,
// "ext": "urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2",
// })); err != nil {
// return nil, err
// }
// return order, nil
default:
return nil, ErrUnknownDocumentType
}
}
// Convert takes a GOBL envelope and converts to a UBL document of one
// of the supported types.
//
// Add a WithContext option to specify the desired UBL Guideline and Profile ID.
// If none is provided, EN16931 will be used by default.
func Convert(env *gobl.Envelope, opts ...Option) (any, error) {
o := &options{
context: ContextEN16931,
}
for _, opt := range opts {
opt(o)
}
switch doc := env.Extract().(type) {
case *bill.Invoice:
// Check and add missing addons
if err := ensureAddons(env, o.context.Addons); err != nil {
return nil, err
}
// Removes included taxes as they are not supported in UBL
if err := doc.RemoveIncludedTaxes(); err != nil {
return nil, fmt.Errorf("cannot convert invoice with included taxes: %w", err)
}
return ublInvoice(doc, o)
case *bill.Status:
return ublApplicationResponse(doc, o), nil
default:
return nil, ErrUnsupportedDocumentType
}
}
// ensureAddons checks if the invoice has all required addons and adds missing ones
func ensureAddons(env *gobl.Envelope, required []cbc.Key) error {
if len(required) == 0 {
return nil
}
inv, ok := env.Extract().(*bill.Invoice)
if !ok {
return fmt.Errorf("expected bill.Invoice, got %T", env.Extract())
}
var missing []cbc.Key
existing := inv.GetAddons()
for _, addon := range required {
if !addon.In(existing...) {
missing = append(missing, addon)
}
}
if len(missing) == 0 {
return nil
}
inv.SetAddons(append(existing, missing...)...)
if err := env.Calculate(); err != nil {
return err
}
if err := env.Validate(); err != nil {
return err
}
return nil
}
func extractRootNamespace(data []byte) (string, error) {
dc := xml.NewDecoder(bytes.NewReader(data))
for {
tk, err := dc.Token()
if err == io.EOF {
break
}
if err != nil {
return "", fmt.Errorf("error parsing XML: %w", err)
}
switch t := tk.(type) {
case xml.StartElement:
return t.Name.Space, nil // Extract and return the namespace
}
}
return "", ErrUnknownDocumentType
}
// Bytes returns the raw XML of the UBL document including
// the XML Header.
func Bytes(in any) ([]byte, error) {
b, err := xml.MarshalIndent(in, "", " ")
if err != nil {
return nil, err
}
// Go's xml.Marshal encodes single quotes as ',
// this is a quick fix
b = bytes.ReplaceAll(b, []byte("'"), []byte("'"))
return append([]byte(xml.Header), b...), nil
}
// BytesCompact returns the raw XML of the UBL document without
// indentation, including the XML Header.
func BytesCompact(in any) ([]byte, error) {
b, err := xml.Marshal(in)
if err != nil {
return nil, err
}
b = bytes.ReplaceAll(b, []byte("'"), []byte("'"))
return append([]byte(xml.Header), b...), nil
}