Skip to content

Commit

Permalink
Merge branch 'main' into en16931-tags
Browse files Browse the repository at this point in the history
  • Loading branch information
apardods committed Oct 15, 2024
2 parents 00edc75 + 9d13ffb commit 6dd886b
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 15 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ All notable changes to GOBL will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). See also the [GOBL versions](https://docs.gobl.org/overview/versions) documentation site for more details.

## [Unreleased]

### Change

- `org.DocumentRef`: renamed `line` to `lines` that accepts an array of integers making it possible to define a selection of reference lines in another document as opposed to just one.

### Added

- `cbc.Code`: new `Join` and `JoinWith` methods to help concatenate codes.
- `it-sdi-v1`: added CIG and CUP identity type codes.

### Fixed

- `mx`: fixed panic when normalizing an invoice with `tax` but no `ext` inside.

## [v0.201.0]

### Fixed
Expand Down
10 changes: 10 additions & 0 deletions addons/it/sdi/identities.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package sdi

import "github.com/invopop/gobl/cbc"

const (
// IdentityTypeCUP defines code managed by the CIPE (Interministerial Committee for Economic Planning) which characterises every public investment project (Individual Project Code)
IdentityTypeCUP cbc.Code = "CUP"
// IdentityTypeCIG defines tender procedure identification code.
IdentityTypeCIG cbc.Code = "CIG"
)
40 changes: 34 additions & 6 deletions cbc/code.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ import (
"regexp"
"strings"

"github.com/invopop/gobl/pkg/here"
"github.com/invopop/jsonschema"
"github.com/invopop/validation"
)

const (
// DefaultCodeSeparator is the default separator used to join codes.
DefaultCodeSeparator Code = "-"
)

// Code represents a string used to uniquely identify the data we're looking
// at. We use "code" instead of "id", to reenforce the fact that codes should
// be more easily set and used by humans within definitions than IDs or UUIDs.
Expand Down Expand Up @@ -81,15 +87,37 @@ func (c Code) In(ary ...Code) bool {
return false
}

// Join returns a new code that is the result of joining the provided
// code with the current one using a default separator.
func (c Code) Join(c2 Code) Code {
return c.JoinWith(DefaultCodeSeparator, c2)
}

// JoinWith returns a new code that is the result of joining the provided
// code with the current one using the provided separator. If any of the codes
// are empty, no separator will be added.
func (c Code) JoinWith(separator Code, c2 Code) Code {
if c == CodeEmpty {
return c2
}
if c2 == CodeEmpty {
return c
}
return c + separator + c2
}

// JSONSchema provides a representation of the struct for usage in Schema.
func (Code) JSONSchema() *jsonschema.Schema {
return &jsonschema.Schema{
Type: "string",
Pattern: CodePattern,
Title: "Code",
MinLength: &CodeMinLength,
MaxLength: &CodeMaxLength,
Description: "Alphanumerical text identifier with upper-case letters, no whitespace, nor symbols.",
Type: "string",
Pattern: CodePattern,
Title: "Code",
MinLength: &CodeMinLength,
MaxLength: &CodeMaxLength,
Description: here.Doc(`
Alphanumerical text identifier with upper-case letters and limits on using
special characters or whitespace to separate blocks.
`),
}
}

Expand Down
120 changes: 120 additions & 0 deletions cbc/code_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package cbc_test

import (
"encoding/json"
"testing"

"github.com/invopop/gobl/cbc"
"github.com/invopop/jsonschema"
"github.com/invopop/validation"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCodeIn(t *testing.T) {
Expand All @@ -14,6 +18,30 @@ func TestCodeIn(t *testing.T) {
assert.False(t, c.In("BAR", "DOM"))
}

func TestCodeEmpty(t *testing.T) {
assert.Equal(t, cbc.Code(""), cbc.CodeEmpty)
assert.True(t, cbc.Code("").IsEmpty())
}

func TestCodeJoin(t *testing.T) {
t.Run("basic join", func(t *testing.T) {
c := cbc.Code("BAR")
assert.Equal(t, "BAR-FOO", c.Join("FOO").String())
})
t.Run("empty base join", func(t *testing.T) {
c := cbc.Code("")
assert.Equal(t, "FOO", c.Join("FOO").String())
})
t.Run("empty postfix join", func(t *testing.T) {
c := cbc.Code("BAR")
assert.Equal(t, "BAR", c.Join("").String())
})
t.Run("custom separator", func(t *testing.T) {
c := cbc.Code("BAR")
assert.Equal(t, "BAR|FOO", c.JoinWith("|", "FOO").String())
})
}

func TestNormalizeCode(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -193,3 +221,95 @@ func TestCode_Validate(t *testing.T) {
})
}
}

func TestCodeMap(t *testing.T) {
cm := cbc.CodeMap{
"foo": cbc.Code("01"),
"bar": cbc.Code("02"),
}
t.Run("Has", func(t *testing.T) {
assert.True(t, cm.Has("foo"))
assert.True(t, cm.Has("foo", "bar"))
assert.False(t, cm.Has("dom"))
assert.False(t, cm.Has("foo", "dom"))
})

t.Run("validation", func(t *testing.T) {
assert.NoError(t, cm.Validate())
cm2 := cbc.CodeMap{
"Invalid": cbc.Code("01"),
}
assert.ErrorContains(t, cm2.Validate(), "Invalid: must be in a valid format")
})
}

func TestCodeMap_Equals(t *testing.T) {
cm := cbc.CodeMap{
"foo": cbc.Code("01"),
"bar": cbc.Code("02"),
}
cm2 := cbc.CodeMap{
"foo": cbc.Code("01"),
"bar": cbc.Code("02"),
}
cm3 := cbc.CodeMap{
"foo": cbc.Code("01"),
"bar": cbc.Code("03"),
}
cm4 := cbc.CodeMap{
"foo": cbc.Code("01"),
"bar": cbc.Code("02"),
"dom": cbc.Code("03"),
}
cm5 := cbc.CodeMap{
"foo": cbc.Code("01"),
}
cm6 := cbc.CodeMap{
"foo": cbc.Code("01"),
"dom": cbc.Code("02"),
}
assert.True(t, cm.Equals(cm2))
assert.False(t, cm.Equals(cm3))
assert.False(t, cm.Equals(cm4))
assert.False(t, cm.Equals(cm5))
assert.False(t, cm.Equals(cm6))
}

func TestCodeMapHas(t *testing.T) {
cm := cbc.CodeMap{
"foo": cbc.Code("01"),
"bar": cbc.Code("02"),
}
err := validation.Validate(cm, cbc.CodeMapHas("foo", "bar"))
assert.NoError(t, err)
assert.ErrorContains(t, validation.Validate(cm, cbc.CodeMapHas("foo", "dom")), "dom: required.")
err = validation.Validate(nil, cbc.CodeMapHas("foo"))
assert.NoError(t, err)
}

func TestCodeJSONSchema(t *testing.T) {
s := cbc.Code("").JSONSchema()
assert.Equal(t, "string", s.Type)
assert.Equal(t, "Code", s.Title)
assert.Equal(t, uint64(1), *s.MinLength)
assert.Equal(t, uint64(32), *s.MaxLength)
}

func TestCodeMapJSONSchemaExtend(t *testing.T) {
eg := `{
"type": "object",
"additionalProperties": {
"$ref": "https://gobl.org/draft-0/cbc/code"
},
"description": "CodeMap is a map of keys to specific codes, useful to determine regime specific codes from their key counterparts."
}`
js := new(jsonschema.Schema)
require.NoError(t, json.Unmarshal([]byte(eg), js))

cm := cbc.CodeMap{}
cm.JSONSchemaExtend(js)

assert.Nil(t, js.AdditionalProperties)
assert.Equal(t, 1, len(js.PatternProperties))
assert.Equal(t, "https://gobl.org/draft-0/cbc/code", js.PatternProperties[cbc.KeyPattern].Ref)
}
4 changes: 2 additions & 2 deletions org/document_ref.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ type DocumentRef struct {
Series cbc.Code `json:"series,omitempty" jsonschema:"title=Series"`
// Source document's code or other identifier.
Code cbc.Code `json:"code" jsonschema:"title=Code"`
// Line index number inside the document, if relevant.
Line int `json:"line,omitempty" jsonschema:"title=Line"`
// Line index numbers inside the document, if relevant.
Lines []int `json:"lines,omitempty" jsonschema:"title=Lines"`
// List of additional codes, IDs, or SKUs which can be used to identify the document or its contents, agreed upon by the supplier and customer.
Identities []*Identity `json:"identities,omitempty" jsonschema:"title=Identities"`
// Tax period in which the referred document had an effect required by some tax regimes and formats.
Expand Down
20 changes: 14 additions & 6 deletions org/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,21 @@ import (
"github.com/invopop/validation"
)

// Default or common identity keys that may be used to identify a person or company.
// Common identity keys that may be used to identify something, like an item, document,
// person, organisation, or company. Ideally, these will only be used when no other
// more structured properties are available inside GOBL. The keys suggested here are
// non-binding and can be used as a reference for other implementations.
const (
IdentityKeyPassport cbc.Key = "passport"
IdentifyKeyNational cbc.Key = "national"
IdentityKeyForeign cbc.Key = "foreign"
IdentityKeyResident cbc.Key = "resident"
IdentityKeyOther cbc.Key = "other"
IdentityKeySKU cbc.Key = "sku" // stock code unit ID
IdentityKeyItem cbc.Key = "item" // item number
IdentityKeyOrder cbc.Key = "order" // order number or code
IdentityKeyAgreement cbc.Key = "agreement" // agreement number
IdentityKeyContract cbc.Key = "contract" // contract number
IdentityKeyPassport cbc.Key = "passport" // Passport number
IdentityKeyNational cbc.Key = "national" // National ID card number
IdentityKeyForeign cbc.Key = "foreign" // Foreigner ID card number
IdentityKeyResident cbc.Key = "resident" // Resident ID card number
IdentityKeyOther cbc.Key = "other" // Other ID card number
)

// Identity is used to define a code for a specific context.
Expand Down
2 changes: 1 addition & 1 deletion regimes/mx/invoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func normalizeInvoice(inv *bill.Invoice) {
// set.
normalizeParty(inv.Supplier) // first do party
ext := make(tax.Extensions)
if inv.Tax != nil {
if inv.Tax != nil && inv.Tax.Ext != nil {
ext = inv.Tax.Ext
}
if ext.Has(extKeyIssuePlace) {
Expand Down
10 changes: 10 additions & 0 deletions regimes/mx/invoice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ func TestNormalizeInvoice(t *testing.T) {
require.NotNil(t, inv.Tax)
assert.Equal(t, tax.ExtValue("21000"), inv.Tax.Ext[cfdi.ExtKeyIssuePlace])
})

t.Run("no ext", func(t *testing.T) {
inv := baseInvoice()
inv.Tax = &bill.Tax{}
require.NoError(t, inv.Calculate())
require.NoError(t, inv.Validate())
require.NotNil(t, inv.Tax)
assert.Equal(t, tax.ExtValue("21000"), inv.Tax.Ext[cfdi.ExtKeyIssuePlace])
})

t.Run("with supplier address code", func(t *testing.T) {
inv := baseInvoice()
delete(inv.Supplier.Ext, cfdi.ExtKeyPostCode)
Expand Down

0 comments on commit 6dd886b

Please sign in to comment.