diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 30f6261..2f96071 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -5,14 +5,14 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - name: Set up Go 1.19 - uses: actions/setup-go@v1 + - name: Set up Go 1.24 + uses: actions/setup-go@v5 with: - go-version: 1.19 + go-version: 1.24 id: go - name: Check out code into the Go module directory - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Test run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 2df8640..5853d08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,15 @@ For more details or to discuss releases, please visit the ## [Unreleased] +- add `-pmDir` command line parameter to specify parts database directory +- add support for loading multiple partmaster CSV files from a directory +- **breaking changes** + - changed CSV column heading "qnty" to "qty" + - breaking change: switched to using ',' in CSV files for delimiter instead of + ';'. It turns out that anything besides ',' introduces a lot of friction in + using other tools like LibreOffice. + - breaking change: switch to using space for reference delimiters (was ',') + ## [[0.4.0] - 2024-02-02](https://github.com/git-plm/gitplm/releases/tag/v0.4.0) - output hook stdout/err to gitplm stdout/err diff --git a/bom.go b/bom.go index 90ecdc3..58b1ca4 100644 --- a/bom.go +++ b/bom.go @@ -3,12 +3,15 @@ package main import ( "fmt" "log" + "regexp" + "sort" + "strconv" "strings" ) type bomLine struct { IPN ipn `csv:"IPN" yaml:"ipn"` - Qnty int `csv:"Qnty" yaml:"qnty"` + Qty int `csv:"Qty" yaml:"qty"` MPN string `csv:"MPN" yaml:"mpn"` Manufacturer string `csv:"Manufacturer" yaml:"manufacturer"` Ref string `csv:"Ref" yaml:"ref"` @@ -22,15 +25,15 @@ type bomLine struct { } func (bl *bomLine) String() string { - return fmt.Sprintf("%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v", + return fmt.Sprintf("%v,%v,%v,%v,%v,%v,%v,%v,%v,%v,%v,%v", + bl.IPN, bl.Ref, - bl.Qnty, + bl.Qty, bl.Value, bl.CmpName, bl.Footprint, bl.Description, bl.Vendor, - bl.IPN, bl.Datasheet, bl.Manufacturer, bl.MPN, @@ -38,7 +41,7 @@ func (bl *bomLine) String() string { } func (bl *bomLine) removeRef(ref string) { - refs := strings.Split(bl.Ref, ",") + refs := strings.Split(bl.Ref, " ") refsOut := []string{} for _, r := range refs { r = strings.Trim(r, " ") @@ -46,8 +49,50 @@ func (bl *bomLine) removeRef(ref string) { refsOut = append(refsOut, r) } } - bl.Ref = strings.Join(refsOut, ", ") - bl.Qnty = len(refsOut) + bl.Ref = strings.Join(refsOut, " ") + bl.Qty = len(refsOut) +} + +func sortReferenceDesignators(input string) string { + // Split the input string into individual designators + designators := strings.Fields(input) + + // Sort the designators + sort.Slice(designators, func(i, j int) bool { + // Extract the numeric part from each designator + numI := extractNumber(designators[i]) + numJ := extractNumber(designators[j]) + + // If the numeric parts are the same, compare the whole strings + if numI == numJ { + return designators[i] < designators[j] + } + + // Compare the numeric parts + return numI < numJ + }) + + // Join the sorted designators back into a string + return strings.Join(designators, " ") +} + +func extractNumber(s string) int { + // Use regex to find the numeric part of the string + re := regexp.MustCompile(`\d+`) + match := re.FindString(s) + + // Convert the matched string to an integer + if match != "" { + num, _ := strconv.Atoi(match) + return num + } + + // Return 0 if no number is found + return 0 +} + +func (bl *bomLine) sortRefs() { + bl.Ref = sortReferenceDesignators(bl.Ref) } type bom []*bomLine @@ -106,13 +151,13 @@ func (b *bom) processOurIPN(pn ipn, qty int) error { for _, l := range subBom { isSub, _ := l.IPN.hasBOM() if isSub { - err := b.processOurIPN(l.IPN, l.Qnty*qty) + err := b.processOurIPN(l.IPN, l.Qty*qty) if err != nil { return fmt.Errorf("Error processing sub %v: %v", l.IPN, err) } } n := *l - n.Qnty *= qty + n.Qty *= qty b.addItem(&n) } @@ -122,7 +167,7 @@ func (b *bom) processOurIPN(pn ipn, qty int) error { func (b *bom) addItem(newItem *bomLine) { for i, l := range *b { if newItem.IPN == l.IPN { - (*b)[i].Qnty += newItem.Qnty + (*b)[i].Qty += newItem.Qty return } } @@ -133,6 +178,29 @@ func (b *bom) addItem(newItem *bomLine) { *b = append(*b, &n) } +func (b *bom) addItemMPN(newItem *bomLine, includeRef bool) { + if newItem.Qty <= 0 { + newItem.Qty = 1 + } + + for i, l := range *b { + if newItem.MPN == l.MPN { + (*b)[i].Qty += newItem.Qty + if includeRef { + (*b)[i].Ref += " " + newItem.Ref + (*b)[i].sortRefs() + } + return + } + } + + n := *newItem + if !includeRef { + n.Ref = "" + } + *b = append(*b, &n) +} + // sort methods func (b bom) Len() int { return len(b) } func (b bom) Swap(i, j int) { b[i], b[j] = b[j], b[i] } diff --git a/example/ASY-001-0000/ASY-001-0000-all.csv b/example/ASY-001-0000/ASY-001-0000-all.csv index fad4593..6a6cac1 100644 --- a/example/ASY-001-0000/ASY-001-0000-all.csv +++ b/example/ASY-001-0000/ASY-001-0000-all.csv @@ -1,4 +1,4 @@ -IPN;Qnty;MPN;Manufacturer;Ref;Value;Cmp name;Footprint;Description;Vendor;Datasheet;Checked +IPN;Qty;MPN;Manufacturer;Ref;Value;Cmp name;Footprint;Description;Vendor;Datasheet;Checked ANA-000-0000;16;LT1716IS5#TRPBF;Linear Technology;;LT1716;LT1716;Package_TO_SOT_SMD:SOT-23-5;LT1716;;https://www.analog.com/media/en/technical-documentation/data-sheets/LT1716.pdf; ASY-002-0001;3;;mycompany;;;;;endplate;;;Y ASY-012-0012;6;;mycompany;;;;;endcap assembly;;; diff --git a/example/ASY-001-0000/ASY-001-0000.csv b/example/ASY-001-0000/ASY-001-0000.csv index 81fcb58..cea8b4d 100644 --- a/example/ASY-001-0000/ASY-001-0000.csv +++ b/example/ASY-001-0000/ASY-001-0000.csv @@ -1,3 +1,3 @@ -IPN;Qnty;MPN;Manufacturer;Ref;Value;Cmp name;Footprint;Description;Vendor;Datasheet;Checked +IPN;Qty;MPN;Manufacturer;Ref;Value;Cmp name;Footprint;Description;Vendor;Datasheet;Checked ASY-002-0001;3;;mycompany;;;;;endplate;;;Y PCA-019-0000;2;;mycompany;;;;;PCB assembly;;; diff --git a/example/ASY-001.csv b/example/ASY-001.csv index 230267f..dc02bb3 100644 --- a/example/ASY-001.csv +++ b/example/ASY-001.csv @@ -1,3 +1,3 @@ -IPN;Qnty -PCA-019-0000;2 -ASY-002-0001;3 +IPN,Qty +PCA-019-0000,2 +ASY-002-0001,3 diff --git a/example/electrical/pcb-design/PCA-019-0000/PCA-019-0000.csv b/example/electrical/pcb-design/PCA-019-0000/PCA-019-0000.csv index f6b6917..98f754e 100644 --- a/example/electrical/pcb-design/PCA-019-0000/PCA-019-0000.csv +++ b/example/electrical/pcb-design/PCA-019-0000/PCA-019-0000.csv @@ -1,4 +1,4 @@ -IPN;Qnty;MPN;Manufacturer;Ref;Value;Cmp name;Footprint;Description;Vendor;Datasheet;Checked +IPN;Qty;MPN;Manufacturer;Ref;Value;Cmp name;Footprint;Description;Vendor;Datasheet;Checked ANA-000-0000;8;LT1716IS5#TRPBF;Linear Technology;U1, U2, U4, U5, U7, U8, U10, U11;LT1716;LT1716;Package_TO_SOT_SMD:SOT-23-5;;;https://www.analog.com/media/en/technical-documentation/data-sheets/LT1716.pdf; CAP-000-1001;18;08055C102JAT2A;AVX;C2, C7, C8, C13, C16, C21, C22, C27, C31, C36, C37, C42, C45, C50, C51, C56, C86, C91;10nF_50V;10nF_50V;Capacitor_SMD:C_0603_1608Metric;;;http://datasheets.avx.com/X7RDielectric.pdf;Y DIO-002-0000;7;MMBZ5245B-7-F;Diodes Incorporated;D2, D8, D18, D22, D28, D32, D38;MMBZ5245B-7-F;MMBZ5245B-7-F;Diode_SMD:D_SOT-23_ANK;;;https://www.diodes.com/assets/Datasheets/MMBZ5221B-MMBZ5259B.pdf;Y diff --git a/example/electrical/pcb-design/PCA-019.csv b/example/electrical/pcb-design/PCA-019.csv index 8d91bca..f4e657a 100644 --- a/example/electrical/pcb-design/PCA-019.csv +++ b/example/electrical/pcb-design/PCA-019.csv @@ -1,4 +1,4 @@ -"Ref";"Qnty";"Value";"Cmp name";"Footprint";"Description";"Vendor";"IPN";"Datasheet" +"Ref";"Qty";"Value";"Cmp name";"Footprint";"Description";"Vendor";"IPN";"Datasheet" "U1, U2, U4, U5, U7, U8, U10, U11, ";"8";"LT1716";"LT1716";"Package_TO_SOT_SMD:SOT-23-5";"";"";"ANA-000-0000";"https://www.analog.com/media/en/technical-documentation/data-sheets/LT1716.pdf" "D2, D8, D12, D18, D22, D28, D32, D38, ";"8";"MMBZ5245B-7-F";"MMBZ5245B-7-F";"Diode_SMD:D_SOT-23_ANK";"";"";"DIO-002-0000";"https://www.diodes.com/assets/Datasheets/MMBZ5221B-MMBZ5259B.pdf" "R1, R15, R29, R43, ";"4";"220k_500mW";"220k_500mW";"Resistor_SMD:R_2010_5025Metric";"";"";"RES-008-220K";"https://www.koaspeer.com/pdfs/HV73.pdf" diff --git a/example/mechanical/enclosure/ASY-002-0001/ASY-002-0001-all.csv b/example/mechanical/enclosure/ASY-002-0001/ASY-002-0001-all.csv index 2ee0800..133e782 100644 --- a/example/mechanical/enclosure/ASY-002-0001/ASY-002-0001-all.csv +++ b/example/mechanical/enclosure/ASY-002-0001/ASY-002-0001-all.csv @@ -1,4 +1,4 @@ -IPN;Qnty;MPN;Manufacturer;Ref;Value;Cmp name;Footprint;Description;Vendor;Datasheet;Checked +IPN;Qty;MPN;Manufacturer;Ref;Value;Cmp name;Footprint;Description;Vendor;Datasheet;Checked ASY-012-0012;2;;mycompany;;;;;endcap assembly;;; MCH-001-0001;2;1051023;bracketsRus;;;;;bracket;;https://www.brackets.com/pdfs/21523.pdf;Y SCR-002-0002;18;18a02SDF;screwsRus;;;;;#4 screw;;https://www.screws.com/pdfs/abc.pdf; diff --git a/example/mechanical/enclosure/ASY-002-0001/ASY-002-0001.csv b/example/mechanical/enclosure/ASY-002-0001/ASY-002-0001.csv index b601423..393726f 100644 --- a/example/mechanical/enclosure/ASY-002-0001/ASY-002-0001.csv +++ b/example/mechanical/enclosure/ASY-002-0001/ASY-002-0001.csv @@ -1,3 +1,3 @@ -IPN;Qnty;MPN;Manufacturer;Ref;Value;Cmp name;Footprint;Description;Vendor;Datasheet;Checked +IPN;Qty;MPN;Manufacturer;Ref;Value;Cmp name;Footprint;Description;Vendor;Datasheet;Checked ASY-012-0012;2;;mycompany;;;;;endcap assembly;;; SCR-002-0002;10;18a02SDF;screwsRus;;;;;#4 screw;;https://www.screws.com/pdfs/abc.pdf; diff --git a/example/mechanical/enclosure/ASY-002.csv b/example/mechanical/enclosure/ASY-002.csv index a2310cf..40300c2 100644 --- a/example/mechanical/enclosure/ASY-002.csv +++ b/example/mechanical/enclosure/ASY-002.csv @@ -1,3 +1,3 @@ -IPN;Qnty +IPN;Qty SCR-002-0002;10 ASY-012-0012;2 diff --git a/example/mechanical/enclosure/endcap/ASY-012-0012/ASY-012-0012.csv b/example/mechanical/enclosure/endcap/ASY-012-0012/ASY-012-0012.csv index a9fa90e..7161a6a 100644 --- a/example/mechanical/enclosure/endcap/ASY-012-0012/ASY-012-0012.csv +++ b/example/mechanical/enclosure/endcap/ASY-012-0012/ASY-012-0012.csv @@ -1,3 +1,3 @@ -IPN;Qnty;MPN;Manufacturer;Ref;Value;Cmp name;Footprint;Description;Vendor;Datasheet;Checked +IPN;Qty;MPN;Manufacturer;Ref;Value;Cmp name;Footprint;Description;Vendor;Datasheet;Checked MCH-001-0001;1;1051023;bracketsRus;;;;;;;https://www.brackets.com/pdfs/21523.pdf;Y SCR-002-0002;4;18a02SDF;screwsRus;;;;;;;https://www.screws.com/pdfs/abc.pdf; diff --git a/example/mechanical/enclosure/endcap/ASY-012.csv b/example/mechanical/enclosure/endcap/ASY-012.csv index e15bf2c..81e5158 100644 --- a/example/mechanical/enclosure/endcap/ASY-012.csv +++ b/example/mechanical/enclosure/endcap/ASY-012.csv @@ -1,3 +1,3 @@ -IPN;Qnty +IPN;Qty MCH-001-0001;1 SCR-002-0002;4 diff --git a/example/partmaster.csv b/example/partmaster.csv index 05293c8..060a780 100644 --- a/example/partmaster.csv +++ b/example/partmaster.csv @@ -1,17 +1,17 @@ -IPN;Description;Footprint;Value;Manufacturer;MPN;Datasheet;Priority;Checked -ANA-000-0000;LT1716;Package_TO_SOT_SMD:SOT-23-5;;Linear Technology;LT1716IS5#TRPBF;https://www.analog.com/media/en/technical-documentation/data-sheets/LT1716.pdf;; -ASY-001-0000;productA;;productA;mycompany;;;; -ASY-002-0001;endplate;;endplate;mycompany;;;;Y -ASY-012-0012;endcap assembly;;;mycompany;;;; -CAP-000-1001;1nF, 50V cap;;1nF_50V;Bogus Caps, Inc;1234;;2; -CAP-000-1001;1nF, 50V cap;Capacitor_SMD:C_0805_2012Metric;;AVX;08055C102JAT2A;http://datasheets.avx.com/X7RDielectric.pdf;1;Y -CAP-000-1002;10nF, 50V;Capacitor_SMD:C_0805_2012Metric;10nF_50V;AVX;08055C103JAT2A;http://datasheets.avx.com/X7RDielectric.pdf;; -CAP-000-1005;1.0uF;Capacitor_SMD:C_0805_2012Metric;1.0uF_50V;AVX;08055C105KAT2A;http://datasheets.avx.com/X7RDielectric.pdf;; -CAP-000-470R;470pF, 50V;Capacitor_SMD:C_0603_1608Metric;470pF_50V;AVX;06035C471JAT2A;https://datasheets.avx.com/X7RDielectric.pdf;;Y -CAP-015-1002;10nF/50V;Capacitor_SMD:C_0603_1608Metric;10nF_50V;AVX;06035C103JAT2A;http://datasheets.avx.com/X7RDielectric.pdf;; -DIO-002-0000;SMD diode;Diode_SMD:D_SOT-23_ANK;MMBZ5245B-7-F;Diodes Incorporated;MMBZ5245B-7-F;https://www.diodes.com/assets/Datasheets/MMBZ5221B-MMBZ5259B.pdf;;Y -MCH-001-0001;bracket;;bracket;bracketsRus;1051023;https://www.brackets.com/pdfs/21523.pdf;;Y -PCA-019-0000;PCB assembly;;;mycompany;;;; -PCB-019-0001;PCB;;;mycompany;;;; -RES-008-220K;220k, Resistor;Resistor_SMD:R_2010_5025Metric;220k_500mW;KOA SPEER Electronics;HV732HTTE2203F;https://www.koaspeer.com/pdfs/HV73.pdf;; -SCR-002-0002;#4 screw;;screw #4,2;screwsRus;18a02SDF;https://www.screws.com/pdfs/abc.pdf;; +IPN,Description,Footprint,Value,Manufacturer,MPN,Datasheet,Priority,Checked +ANA-000-0000,LT1716,Package_TO_SOT_SMD:SOT-23-5,Linear Technology,LT1716IS5#TRPBF,https://www.analog.com/media/en/technical-documentation/data-sheets/LT1716.pdf, +ASY-001-0000,productA,productA,mycompany, +ASY-002-0001,endplate,endplate,mycompany,Y +ASY-012-0012,endcap assembly,mycompany, +CAP-000-1001,1nF 50V cap,1nF_50V,Bogus Caps Inc,1234,2, +CAP-000-1001,1nF 50V cap,Capacitor_SMD:C_0805_2012Metric,AVX,08055C102JAT2A,http://datasheets.avx.com/X7RDielectric.pdf,1,Y +CAP-000-1002,10nF 50V,Capacitor_SMD:C_0805_2012Metric,10nF_50V,AVX,08055C103JAT2A,http://datasheets.avx.com/X7RDielectric.pdf, +CAP-000-1005,1.0uF,Capacitor_SMD:C_0805_2012Metric,1.0uF_50V,AVX,08055C105KAT2A,http://datasheets.avx.com/X7RDielectric.pdf, +CAP-000-470R,470pF 50V,Capacitor_SMD:C_0603_1608Metric,470pF_50V,AVX,06035C471JAT2A,https://datasheets.avx.com/X7RDielectric.pdf,Y +CAP-015-1002,10nF/50V,Capacitor_SMD:C_0603_1608Metric,10nF_50V,AVX,06035C103JAT2A,http://datasheets.avx.com/X7RDielectric.pdf, +DIO-002-0000,SMD diode,Diode_SMD:D_SOT-23_ANK,MMBZ5245B-7-F,Diodes Incorporated,MMBZ5245B-7-F,https://www.diodes.com/assets/Datasheets/MMBZ5221B-MMBZ5259B.pdf,Y +MCH-001-0001,bracket,bracket,bracketsRus,1051023,https://www.brackets.com/pdfs/21523.pdf,Y +PCA-019-0000,PCB assembly,mycompany, +PCB-019-0001,PCB,mycompany, +RES-008-220K,220k Resistor,Resistor_SMD:R_2010_5025Metric,220k_500mW,KOA SPEER Electronics,HV732HTTE2203F,https://www.koaspeer.com/pdfs/HV73.pdf, +SCR-002-0002,#4 screw,screw #4 2,screwsRus,18a02SDF,https://www.screws.com/pdfs/abc.pdf, diff --git a/file.go b/file.go index 2e8df47..65a593e 100644 --- a/file.go +++ b/file.go @@ -11,7 +11,7 @@ import ( ) // load CSV into target data structure. target is modified -func loadCSV(fileName string, target interface{}) error { +func loadCSV(fileName string, target any) error { file, err := os.OpenFile(fileName, os.O_RDONLY, 0644) if err != nil { return err @@ -21,7 +21,7 @@ func loadCSV(fileName string, target interface{}) error { return gocsv.UnmarshalFile(file, target) } -func saveCSV(filename string, data interface{}) error { +func saveCSV(filename string, data any) error { file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return err @@ -90,13 +90,13 @@ func findFile(name string) (string, error) { func initCSV() { gocsv.SetCSVReader(func(in io.Reader) gocsv.CSVReader { r := csv.NewReader(in) - r.Comma = ';' + r.Comma = ',' return r }) gocsv.SetCSVWriter(func(out io.Writer) *gocsv.SafeCSVWriter { writer := csv.NewWriter(out) - writer.Comma = ';' + writer.Comma = ',' return gocsv.NewSafeCSVWriter(writer) }) } diff --git a/go.mod b/go.mod index 78e5217..8f45bc6 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,17 @@ module github.com/git-plm/gitplm -go 1.19 +go 1.22 + +toolchain go1.22.1 require ( - github.com/gocarina/gocsv v0.0.0-20211020200912-82fc2684cc48 + github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 + github.com/otiai10/copy v1.9.0 github.com/samber/lo v1.33.0 gopkg.in/yaml.v2 v2.4.0 ) require ( - github.com/otiai10/copy v1.9.0 // indirect golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect ) diff --git a/go.sum b/go.sum index 0f75a2b..8e475f1 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,34 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/gocarina/gocsv v0.0.0-20211020200912-82fc2684cc48 h1:hLeicZW4XBuaISuJPfjkprg0SP0xxsQmb31aJZ6lnIw= -github.com/gocarina/gocsv v0.0.0-20211020200912-82fc2684cc48/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ= +github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/otiai10/copy v1.9.0 h1:7KFNiCgZ91Ru4qW4CWPf/7jqtxLagGRmIxWldPP9VY4= github.com/otiai10/copy v1.9.0/go.mod h1:hsfX19wcn0UWIHUQ3/4fHuehhk2UyArQ9dVFAn3FczI= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.4.0 h1:umwcf7gbpEwf7WFzqmWwSv0CzbeMsae2u9ZvpP8j2q4= github.com/otiai10/mint v1.4.0/go.mod h1:gifjb2MYOoULtKLqUAEILUG/9KONW6f7YsJ6vQLTlFI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/samber/lo v1.33.0 h1:2aKucr+rQV6gHpY3bpeZu69uYoQOzVhGT3J22Op6Cjk= github.com/samber/lo v1.33.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= +github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index ab33812..86be045 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,9 @@ package main import ( + "errors" "flag" "fmt" - "io/ioutil" "log" "os" "path/filepath" @@ -17,6 +17,10 @@ func main() { flagRelease := flag.String("release", "", "Process release for IPN (ex: PCB-056-0005, ASY-002-0023)") flagVersion := flag.Bool("version", false, "display version of this application") + flagSimplify := flag.String("simplify", "", "simplify a BOM file, combine lines with common MPN") + flagOutput := flag.String("out", "", "output file") + flagCombine := flag.String("combine", "", "adds BOM to output bom") + flagPMDir := flag.String("pmDir", "", "specify location of partmaster CSV files") flag.Parse() if *flagVersion { @@ -36,8 +40,79 @@ func main() { log.Println(s) } + if *flagSimplify != "" { + + in := bom{} + out := bom{} + + err := loadCSV(*flagSimplify, &in) + + if err != nil { + log.Printf("Error loading CSV: %v: %v", *flagSimplify, err) + os.Exit(-1) + } + + for _, l := range in { + out.addItemMPN(l, true) + } + + if *flagOutput == "" { + log.Println("Must specify output file") + os.Exit(-1) + } + + err = saveCSV(*flagOutput, out) + + if err != nil { + log.Printf("Error saving CSV: %v: %v", *flagOutput, err) + os.Exit(-1) + } + + return + } + + if *flagCombine != "" { + + in := bom{} + out := bom{} + + err := loadCSV(*flagCombine, &in) + + if err != nil { + log.Printf("Error loading input CSV: %v: %v", *flagSimplify, err) + os.Exit(-1) + } + + if fileExists(*flagOutput) { + err := loadCSV(*flagOutput, &out) + + if err != nil { + log.Printf("Error loading output CSV: %v: %v", *flagOutput, err) + os.Exit(-1) + } + } + + for _, l := range in { + out.addItemMPN(l, false) + } + + if *flagOutput == "" { + log.Println("Must specify output file") + os.Exit(-1) + } + + err = saveCSV(*flagOutput, out) + + if err != nil { + log.Printf("Error saving CSV: %v: %v", *flagOutput, err) + os.Exit(-1) + } + + return + } + if *flagRelease != "" { - relPath, err := processRelease(*flagRelease, &gLog) + relPath, err := processRelease(*flagRelease, &gLog, *flagPMDir) if err != nil { logMsg(fmt.Sprintf("release error: %v\n", err)) } else { @@ -52,7 +127,7 @@ func main() { } fn := fmt.Sprintf("%v-%03v.log", c, n) logFilePath := filepath.Join(relPath, fn) - err = ioutil.WriteFile(logFilePath, []byte(gLog.String()), 0644) + err = os.WriteFile(logFilePath, []byte(gLog.String()), 0644) if err != nil { log.Println("Error writing log file: ", err) } @@ -64,3 +139,8 @@ func main() { fmt.Println("Error, please specify an action") flag.Usage() } + +func fileExists(filePath string) bool { + _, err := os.Stat(filePath) + return !errors.Is(err, os.ErrNotExist) +} diff --git a/partmaster.go b/partmaster.go index 7ee233c..d0b56eb 100644 --- a/partmaster.go +++ b/partmaster.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "path/filepath" "sort" ) @@ -17,8 +18,21 @@ type partmasterLine struct { Checked string `csv:"Checked"` } +func (p *partmasterLine) String() string { + return fmt.Sprintf("%s, %s, %s, %s, %s, %s", + p.IPN, p.Description, p.Footprint, p.Value, p.Manufacturer, p.MPN) +} + type partmaster []*partmasterLine +func (p partmaster) String() string { + result := "" + for _, line := range p { + result += line.String() + "\n" + } + return result +} + // findPart returns part with highest priority func (p *partmaster) findPart(pn ipn) (*partmasterLine, error) { found := []*partmasterLine{} @@ -58,3 +72,25 @@ type byPriority []*partmasterLine func (p byPriority) Len() int { return len(p) } func (p byPriority) Swap(i, j int) { p[i], p[j] = p[j], p[i] } func (p byPriority) Less(i, j int) bool { return p[i].Priority < p[j].Priority } + +// loadPartmasterFromDir loads all CSV files from a directory and combines them into a single partmaster +func loadPartmasterFromDir(dir string) (partmaster, error) { + pm := partmaster{} + + files, err := filepath.Glob(filepath.Join(dir, "*.csv")) + if err != nil { + return pm, fmt.Errorf("error finding CSV files in directory %s: %v", dir, err) + } + + for _, file := range files { + var temp partmaster + err := loadCSV(file, &temp) + if err != nil { + return pm, fmt.Errorf("error loading CSV file %s: %v", file, err) + } + + pm = append(pm, temp...) + } + + return pm, nil +} diff --git a/partmaster_test.go b/partmaster_test.go index 392f59e..c323503 100644 --- a/partmaster_test.go +++ b/partmaster_test.go @@ -7,10 +7,10 @@ import ( ) var pmIn = ` -IPN;Description;Value;Manufacturer;MPN;Priority -CAP-001-1001;superduper cap;;CapsInc;10045;2 -CAP-001-1001;;10k;MaxCaps;abc2322;1 -CAP-001-1002;;;MaxCaps;abc2323; +IPN,Description,Value,Manufacturer,MPN,Priority +CAP-001-1001,superduper cap,,CapsInc,10045,2 +CAP-001-1001,,10k,MaxCaps,abc2322,1 +CAP-001-1002,,,MaxCaps,abc2323, ` func TestPartmaster(t *testing.T) { @@ -38,7 +38,7 @@ func TestPartmaster(t *testing.T) { t.Errorf("Got wrong value for CAP-001-1001: %v", p.Value) } - p, err = pm.findPart("CAP-001-1002") + _, err = pm.findPart("CAP-001-1002") if err != nil { t.Fatalf("Error finding part CAP-001-1002: %v", err) } diff --git a/rel-script.go b/rel-script.go index 0077535..2a9a1ee 100644 --- a/rel-script.go +++ b/rel-script.go @@ -40,7 +40,7 @@ func (rs *relScript) processBom(b bom) (bom, error) { retM := bom{} for _, l := range ret { l.removeRef(r.Ref) - if l.Qnty > 0 { + if l.Qty > 0 { retM = append(retM, l) } } @@ -50,9 +50,9 @@ func (rs *relScript) processBom(b bom) (bom, error) { for _, a := range rs.Add { refs := strings.Split(a.Ref, ",") - a.Qnty = len(refs) - if a.Qnty < 0 { - a.Qnty = 1 + a.Qty = len(refs) + if a.Qty < 0 { + a.Qty = 1 } // for some reason we need to make a copy or it // will alias the last one @@ -142,6 +142,7 @@ func (rs *relScript) hooks(pn string, srcDir, destDir string) error { log.Println("Error running hook: ", err) log.Println("Hook contents: ") fmt.Print(out.String()) + return err } } return nil diff --git a/rel-script_test.go b/rel-script_test.go index 7822312..ce2d6b0 100644 --- a/rel-script_test.go +++ b/rel-script_test.go @@ -17,24 +17,24 @@ remove: - ref: D13 - ref: R11 add: - - cmpName: "screw #4,2" + - cmpName: "screw #4 2" ref: S3 ipn: SCR-002-0002 ` var bomIn = ` -Ref;Qnty;Value;Cmp name;Footprint;Description;Vendor;IPN;Datasheet -TP4, TP5;2;;Test point 2;;;;; -R1, R2, ;2;;100K_100mw;;;;RES-006-0232; -D1, D2, D13, D14;4;;diode;;;;DIO-023-0023; -"R11, ";"1";"2010_500mW_1%_3000V_10M";"2010_500mW_1%_3000V_10M";"Resistor_SMD:R_2010_5025Metric";"";"";"RES-008-1005";"https://www.bourns.com/docs/Product-Datasheets/CHV.pdf" +Ref,Qty,Value,Cmp name,Footprint,Description,Vendor,IPN,Datasheet +TP4 TP5,2,,Test point 2,,,,, +R1 R2,2,,100K_100mw,,,,RES-006-0232, +D1 D2 D13 D14,4,,diode,,,,DIO-023-0023, +"R11","1","2010_500mW_1%_3000V_10M","2010_500mW_1%_3000V_10M","Resistor_SMD:R_2010_5025Metric","","","RES-008-1005","https://www.bourns.com/docs/Product-Datasheets/CHV.pdf" ` var bomExp = ` -Ref;Qnty;Value;Cmp name;Footprint;Description;Vendor;IPN;Datasheet -D1, D2, D14;3;;diode;;;;DIO-023-0023; -R1, R2;2;;100K_100mw;;;;RES-006-0232; -S3;1;;screw #4,2;;;;SCR-002-0002; +Ref,Qty,Value,Cmp name,Footprint,Description,Vendor,IPN,Datasheet +D1 D2 D14,3,,diode,,,,DIO-023-0023, +R1 R2,2,,100K_100mw,,,,RES-006-0232, +S3,1,,screw #4 2,,,,SCR-002-0002, ` func TestRelScript(t *testing.T) { @@ -64,8 +64,8 @@ func TestRelScript(t *testing.T) { } if reflect.DeepEqual(bExp, bModified) != true { - t.Error("bExp not the same as bModified") fmt.Printf("bExp: %v", bExp) fmt.Printf("bModified: %v", bModified) + t.Error("bExp not the same as bModified") } } diff --git a/release.go b/release.go index 2fc107c..2e848d7 100644 --- a/release.go +++ b/release.go @@ -13,7 +13,7 @@ import ( "gopkg.in/yaml.v2" ) -func processRelease(relPn string, relLog *strings.Builder) (string, error) { +func processRelease(relPn string, relLog *strings.Builder, pmDir string) (string, error) { c, n, _, err := ipn(relPn).parse() if err != nil { return "", fmt.Errorf("error parsing bom %v IPN : %v", relPn, err) @@ -22,6 +22,7 @@ func processRelease(relPn string, relLog *strings.Builder) (string, error) { relPnBase := fmt.Sprintf("%v-%03v", c, n) bomFile := relPnBase + ".csv" + bomFileGenerated := relPn + ".csv" ymlFile := relPnBase + ".yml" bomExists := false ymlExists := false @@ -67,7 +68,7 @@ func processRelease(relPn string, relLog *strings.Builder) (string, error) { } } - writeFilePath := filepath.Join(releaseDir, relPn+".csv") + bomFileWritePath := filepath.Join(releaseDir, bomFileGenerated) logErr := func(s string) { _, err := relLog.Write([]byte(s)) @@ -77,15 +78,22 @@ func processRelease(relPn string, relLog *strings.Builder) (string, error) { log.Println(s) } - partmasterPath, err := findFile("partmaster.csv") - if err != nil { - return sourceDir, fmt.Errorf("Error, partmaster.csv not found in any dir") - } - p := partmaster{} - err = loadCSV(partmasterPath, &p) - if err != nil { - return sourceDir, err + if pmDir != "" { + p, err = loadPartmasterFromDir(pmDir) + if err != nil { + return sourceDir, fmt.Errorf("Error loading partmaster from directory %s: %v", pmDir, err) + } + } else { + partmasterPath, err := findFile("partmaster.csv") + if err != nil { + return sourceDir, fmt.Errorf("Error, partmaster.csv not found in any dir") + } + + err = loadCSV(partmasterPath, &p) + if err != nil { + return sourceDir, err + } } b := bom{} @@ -122,6 +130,23 @@ func processRelease(relPn string, relLog *strings.Builder) (string, error) { return sourceDir, fmt.Errorf("Error running hooks specified in YML: %v", err) } + // look if we generated a BOM + if !bomExists { + bomFilePath, err := findFile(bomFileGenerated) + if err == nil { + bomExists = true + err = loadCSV(bomFilePath, &b) + if err != nil { + return sourceDir, err + } + + b, err = rs.processBom(b) + if err != nil { + return sourceDir, fmt.Errorf("Error processing bom with yml file: %v", err) + } + } + } + // copy stuff to release dir specified in YML file err = rs.copy(sourceDir, releaseDir) if err != nil { @@ -146,7 +171,7 @@ func processRelease(relPn string, relLog *strings.Builder) (string, error) { // merge in partmaster info into BOM b.mergePartmaster(p, logErr) - err = saveCSV(writeFilePath, b) + err = saveCSV(bomFileWritePath, b) if err != nil { return sourceDir, fmt.Errorf("Error writing BOM: %v", err) } @@ -204,7 +229,7 @@ func processRelease(relPn string, relLog *strings.Builder) (string, error) { hasBOM, _ := l.IPN.hasBOM() if hasBOM { foundSub = true - err = b.processOurIPN(l.IPN, l.Qnty) + err = b.processOurIPN(l.IPN, l.Qty) if err != nil { return sourceDir, fmt.Errorf("Error proccessing sub %v: %v", l.IPN, err) }