Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions ordering.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,18 @@ func (ui *Invoice) addOrdering(o *bill.Ordering) {
}

for _, contract := range o.Contracts {
ui.ContractDocumentReference = append(ui.ContractDocumentReference, Reference{
ref := Reference{
ID: IDType{Value: string(contract.Code)},
})
}
// BT-12 carries the contract reference (cbc:ID). The French CTC/Chorus
// Pro extension (EXT-FR-FE-01) also expects the contract type in
// cbc:DocumentType. contract.Type is a cbc.Key, so it is always
// lower-case (e.g. "marche"); upper-case values are rejected upstream
// by GOBL's key pattern validation.
if contract.Type != "" {
ref.DocumentType = contract.Type.String()
}
ui.ContractDocumentReference = append(ui.ContractDocumentReference, ref)
}

for _, tender := range o.Tender {
Expand Down
24 changes: 24 additions & 0 deletions ordering_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ package ubl_test
import (
"testing"

ubl "github.com/invopop/gobl.ubl"
"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/org"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNewOrdering(t *testing.T) {
Expand All @@ -15,4 +19,24 @@ func TestNewOrdering(t *testing.T) {
assert.Equal(t, "NA", doc.OrderReference.ID)
})

// BT-12 (contract reference) plus the France CTC/Chorus Pro contract type
// (EXT-FR-FE-01) must both be serialized into ContractDocumentReference.
t.Run("contract type maps to cbc:DocumentType", func(t *testing.T) {
env := loadTestEnvelope(t, "invoice-complete.json")
inv, ok := env.Extract().(*bill.Invoice)
require.True(t, ok)

inv.Ordering.Contracts = []*org.DocumentRef{
{Code: "test nr de marche", Type: "marche"},
}

require.NoError(t, env.Calculate())
doc, err := ubl.ConvertInvoice(env)
require.NoError(t, err)

require.Len(t, doc.ContractDocumentReference, 1)
ref := doc.ContractDocumentReference[0]
assert.Equal(t, "test nr de marche", ref.ID.Value)
assert.Equal(t, "marche", ref.DocumentType)
})
}
29 changes: 27 additions & 2 deletions party.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,31 @@ const SchemeIDEmail = "EM"
// TaxSchemeVAT is the tax scheme code for VAT
const TaxSchemeVAT = "VAT"

// identityKeyPrivateID is the GOBL identity key used for France's "code
// routage" / Chorus Pro "Code Service", and identitySchemeIDRoutage is the
// ISO 6523 scheme (0224) it maps to. The France CTC addon sets the
// iso-scheme-id extension to 0224 during normalization, but the Factur-X
// (France Extended / B2G) addon is a placeholder and does not, so we derive
// the scheme here to ensure the buyer's code routage is serialized as
// PartyIdentification schemeID="0224" (BR-FR-CPRO-11) regardless of profile.
const (
identityKeyPrivateID = "private-id"
identitySchemeIDRoutage = "0224"
)

// identitySchemeID resolves the ISO 6523 scheme ID to use for a party
// identity. It prefers the explicit iso-scheme-id extension and falls back to
// known GOBL identity keys that map to a fixed scheme.
func identitySchemeID(id *org.Identity) string {
if s := id.Ext.Get(iso.ExtKeySchemeID).String(); s != "" {
return s
}
if id.Key == identityKeyPrivateID {
return identitySchemeIDRoutage
}
return ""
}

// SupplierParty represents the supplier party in a transaction
type SupplierParty struct {
Party *Party `xml:"cac:Party"`
Expand Down Expand Up @@ -219,7 +244,7 @@ func newParty(party *org.Party) *Party { //nolint:gocyclo
p.PartyLegalEntity.CompanyID = &IDType{
Value: code,
}
if s := id.Ext.Get(iso.ExtKeySchemeID).String(); s != "" {
if s := identitySchemeID(id); s != "" {
p.PartyLegalEntity.CompanyID.SchemeID = &s
}
firstLegalIdx = i
Expand Down Expand Up @@ -256,7 +281,7 @@ func newParty(party *org.Party) *Party { //nolint:gocyclo
idType := &IDType{
Value: id.Code.String(),
}
if s := id.Ext.Get(iso.ExtKeySchemeID).String(); s != "" {
if s := identitySchemeID(id); s != "" {
idType.SchemeID = &s
}
p.PartyIdentification = append(p.PartyIdentification, Identification{
Expand Down
31 changes: 31 additions & 0 deletions party_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,35 @@ func TestNewParty(t *testing.T) {
require.NotNil(t, doc.PayeeParty.PartyLegalEntity.CompanyID.SchemeID)
assert.Equal(t, "0088", *doc.PayeeParty.PartyLegalEntity.CompanyID.SchemeID)
})

// France "code routage" / Chorus Pro "Code Service" is modelled as an
// identity with key "private-id". The CTC (B2B CIUS) addon sets the
// iso-scheme-id ext to 0224, but the Factur-X (France Extended / B2G) addon
// is a placeholder and does not. The customer identity must still be
// serialized as PartyIdentification schemeID="0224" (BR-FR-CPRO-11) here, so
// the scheme is derived from the key when the ext is absent.
t.Run("private-id key maps to scheme 0224 without ext", func(t *testing.T) {
env := loadTestEnvelope(t, "invoice-complete.json")
inv, ok := env.Extract().(*bill.Invoice)
require.True(t, ok)

// No iso-scheme-id ext set, mirroring the Factur-X / Extended profile.
inv.Customer.Identities = []*org.Identity{
{Key: "private-id", Code: "SERVICE-ACHATS-01"},
}

require.NoError(t, env.Calculate())
doc, err := ubl.ConvertInvoice(env)
require.NoError(t, err)

var found bool
for _, pid := range doc.AccountingCustomerParty.Party.PartyIdentification {
if pid.ID != nil && pid.ID.Value == "SERVICE-ACHATS-01" {
require.NotNil(t, pid.ID.SchemeID)
assert.Equal(t, "0224", *pid.ID.SchemeID)
found = true
}
}
assert.True(t, found, "expected code routage identity in PartyIdentification")
})
}
Loading