diff --git a/data/schemas/org/unit.json b/data/schemas/org/unit.json index 879b3957..1ad37132 100644 --- a/data/schemas/org/unit.json +++ b/data/schemas/org/unit.json @@ -4,219 +4,303 @@ "$ref": "#/$defs/Unit", "$defs": { "Unit": { - "anyOf": [ + "oneOf": [ + { + "const": "mg", + "title": "Milligrams" + }, { "const": "g", - "description": "Metric grams" + "title": "Metric grams" }, { "const": "kg", - "description": "Metric kilograms" + "title": "Metric kilograms" }, { "const": "t", - "description": "Metric tons" + "title": "Metric tons" }, { "const": "mm", - "description": "Milimetres" + "title": "Milimetres" }, { "const": "cm", - "description": "Centimetres" + "title": "Centimetres" }, { "const": "m", - "description": "Metres" + "title": "Metres" }, { "const": "km", - "description": "Kilometers" + "title": "Kilometers" }, { "const": "in", - "description": "Inches" + "title": "Inches" }, { "const": "ft", - "description": "Feet" + "title": "Feet" }, { "const": "m2", - "description": "Square metres" + "title": "Square metres" }, { "const": "m3", - "description": "Cubic metres" + "title": "Cubic metres" + }, + { + "const": "ml", + "title": "Millilitres" }, { "const": "cl", - "description": "Centilitres" + "title": "Centilitres" }, { "const": "l", - "description": "Litres" + "title": "Litres" }, { "const": "w", - "description": "Watts" + "title": "Watts" }, { "const": "kw", - "description": "Kilowatts" + "title": "Kilowatts" }, { "const": "kwh", - "description": "Kilowatt Hours" + "title": "Kilowatt Hours" + }, + { + "const": "rate", + "title": "Rate", + "description": "A unit of quantity expressed as a rate for usage of a facility or service." + }, + { + "const": "mon", + "title": "Months", + "description": "Unit of time equal to 1/12 of a year of 365,25 days." }, { "const": "day", - "description": "Days" + "title": "Days" }, { "const": "s", - "description": "Seconds" + "title": "Seconds" }, { "const": "h", - "description": "Hours" + "title": "Hours" }, { "const": "min", - "description": "Minutes" + "title": "Minutes" }, { "const": "piece", - "description": "Pieces" + "title": "Pieces", + "description": "A unit of count defining the number of pieces (piece: a single item, article or exemplar)." }, { "const": "item", - "description": "Items" + "title": "Items", + "description": " A unit of count defining the number of items regarded as separate units." + }, + { + "const": "pair", + "title": "Pairs", + "description": "A unit of count defining the number of pairs (pair: item described by two's)." + }, + { + "const": "dozen", + "title": "Dozens", + "description": "A unit of count defining the number of units in multiples of 12." + }, + { + "const": "assortment", + "title": "Assortments", + "description": "A unit of count defining the number of assortments (assortment: a collection of items or components of a single product packaged together)." }, { "const": "service", - "description": "Service Units" + "title": "Service Units", + "description": "A unit of count defining the number of service units (service unit: defined period / property / facility / utility of supply)." + }, + { + "const": "job", + "title": "Jobs", + "description": "A unit of count defining the number of jobs." }, { "const": "activity", - "description": "Activity" + "title": "Activities", + "description": "A unit of count defining the number of activities (activity: a unit of work or action)." + }, + { + "const": "trip", + "title": "Trips", + "description": "A unit of count defining the number of trips (trip: a journey to a place and back again)." + }, + { + "const": "group", + "title": "Groups", + "description": "A unit of count defining the number of groups (group: set of items classified together)." + }, + { + "const": "outfit", + "title": "Outfits", + "description": "A unit of count defining the number of outfits (outfit: a complete set of equipment / materials / objects used for a specific purpose)." + }, + { + "const": "kit", + "title": "Kits", + "description": "A unit of count defining the number of kits (kit: tub, barrel or pail)." + }, + { + "const": "basebox", + "title": "Base Boxes", + "description": "A unit of area of 112 sheets of tin mil products (tin plate, tin free steel or black plate) 14 by 20 inches, or 31,360 square inches." + }, + { + "const": "pk", + "title": "Bulk Packs", + "description": "A unit of count defining the number of items per bulk pack." }, { "const": "bag", - "description": "Bags" + "title": "Bags" }, { "const": "box", - "description": "Boxes" + "title": "Boxes" }, { "const": "bin", - "description": "Bins" + "title": "Bins" }, { "const": "can", - "description": "Cans" + "title": "Cans" }, { "const": "tub", - "description": "Tubs" + "title": "Tubs" }, { "const": "case", - "description": "Cases" + "title": "Cases" }, { "const": "tray", - "description": "Trays" + "title": "Trays" }, { "const": "portion", - "description": "Portions" + "title": "Portions" }, { - "const": "dozen", - "description": "Dozens" + "const": "set", + "title": "Sets", + "description": "A unit of count defining the number of sets (set: a number of objects grouped together)." }, { "const": "roll", - "description": "Rolls" + "title": "Rolls" }, { "const": "carton", - "description": "Cartons" + "title": "Cartons" }, { "const": "cylinder", - "description": "Cylinders" + "title": "Cylinders" }, { "const": "barrel", - "description": "Barrels" + "title": "Barrels" }, { "const": "jerrican", - "description": "Jerricans" + "title": "Jerricans", + "description": "Jerrican, cylindrical" }, { "const": "carboy", - "description": "Carboys" + "title": "Carboys" }, { "const": "demijohn", - "description": "Demijohn" + "title": "Demijohn" }, { "const": "bottle", - "description": "Bottles" + "title": "Bottles" }, { "const": "6pack", - "description": "Six Packs" + "title": "Six Packs" }, { "const": "canister", - "description": "Canisters" + "title": "Canisters" }, { "const": "pkg", - "description": "Packages" + "title": "Packages", + "description": "Standard packaging unit." }, { "const": "bunch", - "description": "Bunches" + "title": "Bunches" }, { "const": "tetrabrik", - "description": "Tetra-Briks" + "title": "Tetra-Briks" }, { "const": "pallet", - "description": "Pallets" + "title": "Pallets" }, { "const": "reel", - "description": "Reels" + "title": "Reels" }, { "const": "sack", - "description": "Sacks" + "title": "Sacks" }, { "const": "sheet", - "description": "Sheets" + "title": "Sheets" }, { "const": "envelope", - "description": "Envelopes" + "title": "Envelopes" + }, + { + "const": "lot", + "title": "Lot" + }, + { + "const": "unit", + "title": "Unit", + "description": "A type of package composed of a single item or object, not otherwise specified as a unit of transport equipment." }, { - "pattern": "^(?:[a-z]|[a-z0-9][a-z0-9-+]*[a-z0-9])$", - "description": "Custom unit definition" + "pattern": "^[A-Z0-9]{2,3}$", + "description": "UN/ECE Unit Code from Recommendation 20" } ], "type": "string", "title": "Unit", - "description": "Unit describes how the quantity of the product should be interpreted." + "description": "Unit describes how the quantity of the product should be interpreted either using a GOBL key, or UN/ECE code." } }, "$comment": "Generated with GOBL v0.63.0" diff --git a/org/unit.go b/org/unit.go index eae75db0..dde18ddf 100644 --- a/org/unit.go +++ b/org/unit.go @@ -1,22 +1,37 @@ package org import ( + "regexp" + "github.com/invopop/gobl/cbc" "github.com/invopop/jsonschema" "github.com/invopop/validation" ) -// Unit is used to represent standard unit types. -type Unit cbc.Key +// Unit represents either a unit key defined by GOBL *or* a two to three letter code +// defined by the UN/ECE. +type Unit string + +const ( + // UnitPatternUNECE is a regular expression for UN/ECE unit codes when a unit is not covered by GOBL. + UnitPatternUNECE = `^[A-Z0-9]{2,3}$` + // UnitUNECEMutuallyDefined is the UN/ECE code for mutually defined units. + UnitUNECEMutuallyDefined cbc.Code = `ZZ` +) + +var regexpUNECEUnit = regexp.MustCompile(UnitPatternUNECE) -// Set of common units based on UN/ECE recommendation 20 and 21. Some local formats -// may define additional non-standard codes which may be added. There are so -// many different unit codes in the world, that it's impractical to try and define them -// all, this is thus a selection of which we think are the most useful. +// Set of common units based on UN/ECE recommendation 20 and 21 extensions. Some local formats +// may define additional non-standard codes which may be added. +// +// The UN/ECE defines a very large set of units which would be impractical to support +// here in GOBL, so the Unit type will also accept any UN/ECE unit code instead of +// one of the keys defined here. const ( UnitEmpty Unit = `` // No unit defined // Measurement units + UnitMilligram Unit = `mg` UnitGram Unit = `g` UnitKilogram Unit = `kg` UnitMetricTon Unit = `t` @@ -28,19 +43,31 @@ const ( UnitFoot Unit = `ft` UnitSquareMetre Unit = `m2` UnitCubicMetre Unit = `m3` + UnitMillilitre Unit = "ml" UnitCentilitre Unit = `cl` UnitLitre Unit = `l` UnitWatt Unit = `w` UnitKilowatt Unit = `kw` UnitKilowattHour Unit = `kwh` + UnitMonth Unit = `mon` UnitDay Unit = `day` UnitSecond Unit = `s` UnitHour Unit = `h` UnitMinute Unit = `min` + UnitRate Unit = `rate` UnitPiece Unit = `piece` UnitItem Unit = `item` UnitActivity Unit = `activity` UnitService Unit = `service` + UnitGroup Unit = `group` + UnitSet Unit = `set` + UnitTrip Unit = `trip` + UnitJob Unit = `job` + UnitAssortment Unit = `assortment` + UnitOutfit Unit = `outfit` + UnitKit Unit = `kit` + UnitBaseBox Unit = `basebox` + UnitBulkPack Unit = `pk` // Presentation Unit Codes UnitBag Unit = `bag` @@ -51,7 +78,8 @@ const ( UnitCase Unit = `case` UnitTray Unit = `tray` UnitPortion Unit = `portion` // non-standard (src: ES) - UnitDozen Unit = `dozen` // non-standard (src: ES) + UnitDozen Unit = `dozen` + UnitPair Unit = `pair` UnitRoll Unit = `roll` UnitCarton Unit = `carton` UnitCylinder Unit = `cylinder` @@ -70,12 +98,16 @@ const ( UnitSack Unit = `sack` UnitSheet Unit = `sheet` UnitEnvelope Unit = `envelope` + UnitUnit Unit = `unit` + UnitLot Unit = `lot` ) // DefUnit serves to define unit keys. type DefUnit struct { // Key for the Unit Unit Unit `json:"unit" jsonschema:"title=Unit"` + // Name of the Unit + Name string `json:"name" jsonschema:"title=Name"` // Description of the unit Description string `json:"description" jsonschema:"title=Description"` // Standard UN/ECE code @@ -87,76 +119,112 @@ type DefUnit struct { var UnitDefinitions = []DefUnit{ // Recommendations Nº 20 // source: https://unece.org/trade/documents/2021/06/uncefact-rec20-0 - {UnitGram, "Metric grams", "GRM"}, - {UnitKilogram, "Metric kilograms", "KGM"}, - {UnitMetricTon, "Metric tons", "TNE"}, - {UnitMillimetre, "Milimetres", "MMT"}, - {UnitCentimetre, "Centimetres", "CMT"}, - {UnitMetre, "Metres", "MTR"}, - {UnitKilometre, "Kilometers", "KMT"}, - {UnitInch, "Inches", "INH"}, - {UnitFoot, "Feet", "FOT"}, - {UnitSquareMetre, "Square metres", "MTK"}, - {UnitCubicMetre, "Cubic metres", "MTQ"}, - {UnitCentilitre, "Centilitres", "CLT"}, - {UnitLitre, "Litres", "LTR"}, - {UnitWatt, "Watts", "WTT"}, - {UnitKilowatt, "Kilowatts", "KWT"}, - {UnitKilowattHour, "Kilowatt Hours", "KWH"}, - {UnitDay, "Days", "DAY"}, - {UnitSecond, "Seconds", "SEC"}, - {UnitHour, "Hours", "HUR"}, - {UnitMinute, "Minutes", "MIN"}, - {UnitPiece, "Pieces", "H87"}, // A unit of count defining the number of pieces (piece: a single item, article or exemplar). - {UnitItem, "Items", "EA"}, // A unit of count defining the number of items regarded as separate units. - {UnitService, "Service Units", "E48"}, // A unit of count defining the number of service units (service unit: defined period / property / facility / utility of supply). - {UnitActivity, "Activity", "ACT"}, // A unit of count defining the number of activities (activity: a unit of work or action). + {UnitMilligram, "Milligrams", "", "MGM"}, + {UnitGram, "Metric grams", "", "GRM"}, + {UnitKilogram, "Metric kilograms", "", "KGM"}, + {UnitMetricTon, "Metric tons", "", "TNE"}, + {UnitMillimetre, "Milimetres", "", "MMT"}, + {UnitCentimetre, "Centimetres", "", "CMT"}, + {UnitMetre, "Metres", "", "MTR"}, + {UnitKilometre, "Kilometers", "", "KMT"}, + {UnitInch, "Inches", "", "INH"}, + {UnitFoot, "Feet", "", "FOT"}, + {UnitSquareMetre, "Square metres", "", "MTK"}, + {UnitCubicMetre, "Cubic metres", "", "MTQ"}, + {UnitMillilitre, "Millilitres", "", "MLT"}, + {UnitCentilitre, "Centilitres", "", "CLT"}, + {UnitLitre, "Litres", "", "LTR"}, + {UnitWatt, "Watts", "", "WTT"}, + {UnitKilowatt, "Kilowatts", "", "KWT"}, + {UnitKilowattHour, "Kilowatt Hours", "", "KWH"}, + {UnitRate, "Rate", "A unit of quantity expressed as a rate for usage of a facility or service.", "A9"}, + {UnitMonth, "Months", "Unit of time equal to 1/12 of a year of 365,25 days.", "MON"}, + {UnitDay, "Days", "", "DAY"}, + {UnitSecond, "Seconds", "", "SEC"}, + {UnitHour, "Hours", "", "HUR"}, + {UnitMinute, "Minutes", "", "MIN"}, + {UnitPiece, "Pieces", "A unit of count defining the number of pieces (piece: a single item, article or exemplar).", "H87"}, + {UnitItem, "Items", " A unit of count defining the number of items regarded as separate units.", "EA"}, + {UnitPair, "Pairs", "A unit of count defining the number of pairs (pair: item described by two's).", "PR"}, + {UnitDozen, "Dozens", "A unit of count defining the number of units in multiples of 12.", "DZN"}, + {UnitAssortment, "Assortments", "A unit of count defining the number of assortments (assortment: a collection of items or components of a single product packaged together).", "AS"}, + {UnitService, "Service Units", "A unit of count defining the number of service units (service unit: defined period / property / facility / utility of supply).", "E48"}, + {UnitJob, "Jobs", "A unit of count defining the number of jobs.", "E51"}, + {UnitActivity, "Activities", "A unit of count defining the number of activities (activity: a unit of work or action).", "ACT"}, + {UnitTrip, "Trips", "A unit of count defining the number of trips (trip: a journey to a place and back again).", "E54"}, + {UnitGroup, "Groups", "A unit of count defining the number of groups (group: set of items classified together).", "10"}, + {UnitOutfit, "Outfits", "A unit of count defining the number of outfits (outfit: a complete set of equipment / materials / objects used for a specific purpose).", "11"}, + {UnitKit, "Kits", "A unit of count defining the number of kits (kit: tub, barrel or pail).", "KT"}, + {UnitBaseBox, "Base Boxes", "A unit of area of 112 sheets of tin mil products (tin plate, tin free steel or black plate) 14 by 20 inches, or 31,360 square inches.", "BB"}, + {UnitBulkPack, "Bulk Packs", "A unit of count defining the number of items per bulk pack.", "AB"}, // Recommendations Nº 21 // source: https://unece.org/trade/documents/2021/06/uncefact-rec21 - {UnitBag, "Bags", "XBG"}, - {UnitBox, "Boxes", "XBX"}, - {UnitBin, "Bins", "XBI"}, - {UnitCan, "Cans", "XCA"}, - {UnitTub, "Tubs", "XTB"}, - {UnitCase, "Cases", "XCS"}, - {UnitTray, "Trays", "XDS"}, // plastic - {UnitPortion, "Portions", ""}, // non-standard (src: ES) - {UnitDozen, "Dozens", ""}, // non-standard (src: ES) - {UnitRoll, "Rolls", "XRO"}, - {UnitCarton, "Cartons", "XCT"}, - {UnitCylinder, "Cylinders", "XCY"}, - {UnitBarrel, "Barrels", "XBA"}, - {UnitJerrican, "Jerricans", "XJY"}, // cylindrical - {UnitCarboy, "Carboys", "XCO"}, // non-protected - {UnitDemijohn, "Demijohn", "XDJ"}, // non-protected - {UnitBottle, "Bottles", "XBO"}, // non-protected, cylindrical - {UnitSixPack, "Six Packs", ""}, // non-standard (src: ES) - {UnitCanister, "Canisters", "XCI"}, - {UnitPackage, "Packages", "XPK"}, - {UnitBunch, "Bunches", "XBH"}, - {UnitTetraBrik, "Tetra-Briks", ""}, // non-standard (src: ES) - {UnitPallet, "Pallets", "XPX"}, - {UnitReel, "Reels", "XRL"}, - {UnitSack, "Sacks", "XSA"}, - {UnitSheet, "Sheets", "XST"}, - {UnitEnvelope, "Envelopes", "XEN"}, + {UnitBag, "Bags", "", "XBG"}, + {UnitBox, "Boxes", "", "XBX"}, + {UnitBin, "Bins", "", "XBI"}, + {UnitCan, "Cans", "", "XCA"}, + {UnitTub, "Tubs", "", "XTB"}, + {UnitCase, "Cases", "", "XCS"}, + {UnitTray, "Trays", "", "XDS"}, // plastic + {UnitPortion, "Portions", "", ""}, // non-standard (src: ES) + {UnitSet, "Sets", "A unit of count defining the number of sets (set: a number of objects grouped together).", "SET"}, + {UnitRoll, "Rolls", "", "XRO"}, + {UnitCarton, "Cartons", "", "XCT"}, + {UnitCylinder, "Cylinders", "", "XCY"}, + {UnitBarrel, "Barrels", "", "XBA"}, + {UnitJerrican, "Jerricans", "Jerrican, cylindrical", "XJY"}, + {UnitCarboy, "Carboys", "", "XCO"}, // non-protected + {UnitDemijohn, "Demijohn", "", "XDJ"}, // non-protected + {UnitBottle, "Bottles", "", "XBO"}, // non-protected, cylindrical + {UnitSixPack, "Six Packs", "", ""}, // non-standard (src: ES) + {UnitCanister, "Canisters", "", "XCI"}, + {UnitPackage, "Packages", "Standard packaging unit.", "XPK"}, + {UnitBunch, "Bunches", "", "XBH"}, + {UnitTetraBrik, "Tetra-Briks", "", ""}, // non-standard (src: ES) + {UnitPallet, "Pallets", "", "XPX"}, + {UnitReel, "Reels", "", "XRL"}, + {UnitSack, "Sacks", "", "XSA"}, + {UnitSheet, "Sheets", "", "XST"}, + {UnitEnvelope, "Envelopes", "", "XEN"}, + {UnitLot, "Lot", "", "XLT"}, + {UnitUnit, "Unit", "A type of package composed of a single item or object, not otherwise specified as a unit of transport equipment.", "XUN"}, +} + +var isValidUnit = validation.In(validUnits()...) + +func validUnits() []interface{} { + list := make([]interface{}, len(UnitDefinitions)) + for i, d := range UnitDefinitions { + list[i] = string(d.Unit) + } + return list } // Validate ensures the unit looks correct func (u Unit) Validate() error { - return validation.Validate(string(u), validation.Match(cbc.KeyValidationRegexp)) + if regexpUNECEUnit.MatchString(string(u)) { + return nil + } + return validation.Validate(string(u), isValidUnit.Error("must be a valid value or UN/ECE code")) } // UNECE provides the unit's UN/ECE equivalent -// value. If not available, returns CodeEmpty. +// value. func (u Unit) UNECE() cbc.Code { + if u == UnitEmpty { + return cbc.CodeEmpty + } + // If already a UNECE code, return it. + if regexpUNECEUnit.MatchString(string(u)) { + return cbc.Code(string(u)) + } for _, def := range UnitDefinitions { if def.Unit == u { return def.UNECE } } - return cbc.CodeEmpty + return UnitUNECEMutuallyDefined // Assume something else. } // JSONSchema provides a representation of the struct for usage in Schema. @@ -164,19 +232,20 @@ func (u Unit) JSONSchema() *jsonschema.Schema { s := &jsonschema.Schema{ Title: "Unit", Type: "string", - AnyOf: make([]*jsonschema.Schema, len(UnitDefinitions)), - Description: "Unit describes how the quantity of the product should be interpreted.", + OneOf: make([]*jsonschema.Schema, len(UnitDefinitions)), + Description: "Unit describes how the quantity of the product should be interpreted either using a GOBL key, or UN/ECE code.", } for i, v := range UnitDefinitions { - s.AnyOf[i] = &jsonschema.Schema{ + s.OneOf[i] = &jsonschema.Schema{ Const: v.Unit, + Title: v.Name, Description: v.Description, } } - // Add the custom unit to the end - s.AnyOf = append(s.AnyOf, &jsonschema.Schema{ - Pattern: cbc.KeyPattern, - Description: "Custom unit definition", + // Add the UN/ECE unit code pattern as an alternative to the pre-defined units. + s.OneOf = append(s.OneOf, &jsonschema.Schema{ + Pattern: UnitPatternUNECE, + Description: "UN/ECE Unit Code from Recommendation 20", }) return s } diff --git a/org/unit_test.go b/org/unit_test.go index 168a6f79..977fb4b2 100644 --- a/org/unit_test.go +++ b/org/unit_test.go @@ -12,10 +12,19 @@ func TestUnitValidation(t *testing.T) { u := org.Unit("h") assert.NoError(t, u.Validate()) - u = org.Unit("FOO") + u = org.Unit("XUN") + assert.NoError(t, u.Validate()) + + u = org.Unit("X") err := u.Validate() if assert.Error(t, err) { - assert.Contains(t, err.Error(), "valid format") + assert.Contains(t, err.Error(), "must be a valid value or UN/ECE code") + } + + u = org.Unit("XUNX") + err = u.Validate() + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "must be a valid value or UN/ECE code") } } @@ -26,6 +35,9 @@ func TestUnitUNECE(t *testing.T) { u = org.UnitTetraBrik assert.Equal(t, u.UNECE(), cbc.CodeEmpty, "valid but no code") - u = org.Unit("FOO") - assert.Equal(t, u.UNECE(), cbc.CodeEmpty) + u = org.Unit("XUN") + assert.Equal(t, u.UNECE(), cbc.Code("XUN")) + + u = org.Unit("random-something") + assert.Equal(t, u.UNECE(), cbc.Code("ZZ")) } diff --git a/regimes/mx/invoice_validator_test.go b/regimes/mx/invoice_validator_test.go index fe955875..b4cde8d8 100644 --- a/regimes/mx/invoice_validator_test.go +++ b/regimes/mx/invoice_validator_test.go @@ -52,7 +52,7 @@ func validInvoice() *bill.Invoice { Item: &org.Item{ Name: "bogus", Price: num.MakeAmount(10000, 2), - Unit: "mutual", + Unit: org.UnitPackage, Ext: cbc.CodeMap{ mx.ExtKeyCFDIProdServ: "01010101", },