#usage "Export a Bill Of Material\n" "

" "Generates a project's Bill Of Material." "

" "A database with additional information like order codes, manufacturers " "or prices can be created and managed." "

" "Author: support@cadsoft.de" // THIS PROGRAM IS PROVIDED AS IS AND WITHOUT WARRANTY OF ANY KIND, EXPRESSED OR IMPLIED string HelpText = "How to generate the Bill Of Material\n" "

\n" "List type\n" "

\n" "The Bill Of Material can be generated either as a list\n" "of parts (where every part is listed on a line of its own),\n" "or as a list of values, where all parts with the same value are grouped\n" "together in one line. Use the Parts and Values\n" "radio buttons to select the list type.\n" "

\n" "Output format\n" "

\n" "Choose between pure ASCII Text format or HTML.\n" "

\n" "Database\n" "

\n" "You can pull in additional information about the used parts by loading\n" "a database file with the Load button.\n" "

\n" "A database file must consist of lines of text, each of which contains\n" "one record consisting of CSV (Comma Separated Values)\n" "or TSV (Tab Separated Values) data.\n" "The very first line must contain a \"header\", which defines a unique name for\n" "each column, and the first column of every following line must contain\n" "a unique (non-empty) key for this record.\n" "

\n" "An example for a valid database file would be:\n" "

\n"
  "Key              Manufacturer       Order Code Price\n"
  "74LS00N          Texas Instruments  123-456    0.20\n"
  "R-EU_0204/5:4k7  Somebody           RES4k7     0.10\n"
  "
\n" "Note that the columns are separated by a tab character (you may also\n" "use a semicolon (';') to separate the columns, but then you will have to make sure\n" "none of the data items contains a semicolon).\n" "The keys for looking up records in the database are built from the\n" "parts' values. If a part's device has defined \"value on\" it means that\n" "the user needs to specify a particular value for this part, as for\n" "example with a resistor. In such a case the key consists of the device\n" "name and the user defined value, separated by a colon (':'). If the\n" "device has \"value off\", only the device name is used as key (if the\n" "user has edited the value of such a part and insisted on changing\n" "it, the edited value will be used).\n" "

\n" "Creating a new database\n" "

\n" "Click on the New button to create a new database.\n" "You will get a dialog in which you can define the names of the column headers\n" "for your new database. The first column always contains the key for database\n" "lookups and can't be deleted (you can edit it, though, to give it a different\n" "name than the default \"Key\"). This first column will not be visible in the\n" "generated list, so you don't really need to worry about it.\n" "

\n" "Editing the database\n" "

\n" "If you have loaded a database you can either double click on a line\n" "in the list, or select a line and press Enter (or click on the Edit\n" "button) to bring up a dialog in which you can edit the database entry\n" "for this part. If the database has been modified you will be asked if\n" "you want to save it before leaving the program or loading a new database.\n" "

\n" "Viewing the output\n" "

\n" "Click on the View button to get a preview of the list output.\n" "

\n" "Saving the list\n" "

\n" "Click on the Save button to save the list to disk.\n" ; if (!schematic) { dlgMessageBox(usage + "


ERROR: No schematic!

\nThis program can only work in the schematic editor."); exit(1); } //XXX /* TODO: - Query user for missing database entries ("Check" button) - Allow user to define which database columns to actually use - dto. for the internal data? - store and retrieve the setup? - what if this is run in a board? */ int NumParts; numeric string Lines[]; numeric string PartName[], PartValue[], PartDevice[], PartPackage[], PartHeadline[], PartDescription[]; int PartValueOn[]; int Selected; enum { ltParts, ltValues }; // List Types enum { ofText, ofHTML }; // Output Formats int ListType = 0; int OutputFormat = 0; string DatabaseFile; string Database[]; char DatabaseSeparator = '\t'; string DatabaseFields[]; int DatabaseModified = 0; char ValueSeparator = ':'; string StripWhiteSpace(string s) { while (s && isspace(s[0])) s = strsub(s, 1); while (s && isspace(s[strlen(s) - 1])) s = strsub(s, 0, strlen(s) - 1); return s; } void CollectPartData(void) { NumParts = 0; schematic(SCH) { SCH.parts(P) { if (P.device.package) { PartName[NumParts] = P.name; PartValue[NumParts] = P.value; PartDevice[NumParts] = P.device.name; PartPackage[NumParts] = P.device.package.name; PartHeadline[NumParts] = P.device.headline; PartDescription[NumParts] = P.device.description; PartValueOn[NumParts] = P.device.value == "On"; NumParts++; } } } } string DatabaseHeader(void) { string s; if (Database[0]) { string a[]; int n = strsplit(a, Database[0], DatabaseSeparator); int i; for (i = 1; i < n; i++) { s += "\t" + a[i]; DatabaseFields[i - 1] = a[i]; } DatabaseFields[i - 1] = ""; } return s; } string DatabaseKey(int i) { string key = PartValue[i]; if (PartValueOn[i]) key = PartDevice[i] + ValueSeparator + key; return key; } string DatabaseLookup(string key, int f) { return lookup(Database, key, DatabaseFields[f], DatabaseSeparator); } void GeneratePartList(void) { int NumLines = 0; //XXX column sequence? Lines[NumLines++] = "Part\tValue\tDevice\tPackage\tDescription" + DatabaseHeader(); for (int i = 0; i < NumParts; i++) { Lines[NumLines] = PartName[i] + "\t" + PartValue[i] + "\t" + PartDevice[i] + "\t" + PartPackage[i] + "\t" + PartHeadline[i]; if (Database[0]) { string key = DatabaseKey(i); for (int f = 0; DatabaseFields[f]; f++) Lines[NumLines] += "\t" + DatabaseLookup(key, f); Lines[NumLines] += "\t" + key; // hidden field! } NumLines++; } Lines[NumLines] = ""; } void GenerateValueList(void) { int NumLines = 0; int Index[]; //XXX column sequence? Lines[NumLines++] = "Qty\tValue\tDevice\tParts" + DatabaseHeader(); sort(NumParts, Index, PartValue, PartDevice, PartName); for (int n1 = 0, n2 = 0; ++n2 <= NumParts; ) { int i1 = Index[n1]; if (n2 < NumParts) { int i2 = Index[n2]; //XXX value on/off? if (PartValue[i1] == PartValue[i2] && PartDevice[i1] == PartDevice[i2])//XXX && lname[i1] == lname[i2]) continue; } string Quantity; sprintf(Quantity, "%d", n2 - n1); Lines[NumLines] = Quantity + "\t" + PartValue[i1] + "\t" + PartDevice[i1] + "\t"; for (;;) { Lines[NumLines] += PartName[i1]; if (++n1 < n2) { i1 = Index[n1]; Lines[NumLines] += ", "; } else break; } if (Database[0]) { string key = DatabaseKey(i1); for (int f = 0; DatabaseFields[f]; f++) Lines[NumLines] += "\t" + DatabaseLookup(key, f); Lines[NumLines] += "\t" + key; // hidden field! } NumLines++; } Lines[NumLines] = ""; } void GenerateList(void) { switch (ListType) { case ltParts: GeneratePartList(); break; case ltValues: GenerateValueList(); break; } } string MakeListHeader(void) { string s; schematic(SCH) sprintf(s, "Partlist exported from %s at %s", SCH.name, t2string(time())); return s; } string MakeListText(void) { int l, Width[]; for (l = 0; Lines[l]; l++) { string a[]; for (int n = strsplit(a, Lines[l], '\t'); n--; ) Width[n] = max(Width[n], strlen(a[n])); } string List; List = MakeListHeader() + "\n\n"; int numHeaders; for (l = 0; Lines[l]; l++) { string line, a[]; int n = strsplit(a, Lines[l], '\t'); if (l == 0) numHeaders = n; else n = numHeaders; // for the hidden key! for (int i = 0; i < n; i++) { string s; sprintf(s, "%s%-*s", line ? " " : "", Width[i], a[i]); line += s; } List += line + "\n"; } return List; } string MakeListHTML(void) { string List; List = "" + MakeListHeader() + "\n

\n"; List += "\n"; int numHeaders; for (int l = 0; Lines[l]; l++) { List += ""; string a[]; int n = strsplit(a, Lines[l], '\t'); if (l == 0) numHeaders = n; else n = numHeaders; // for the hidden key! for (int i = 0; i < n; i++) { if (l == 0) a[i] = "" + a[i] + ""; List += ""; } List += "\n"; } List += "
" + a[i] + "
\n"; return List; } string MakeList(void) { switch (OutputFormat) { case ofText: return MakeListText(); break; case ofHTML: return MakeListHTML(); break; } return ""; } void ViewList(void) { dlgDialog("Bill Of Material - Preview") { string s = MakeList(); if (OutputFormat == ofText) s = "

" + s + "
"; dlgHBoxLayout dlgSpacing(400); dlgHBoxLayout { dlgVBoxLayout dlgSpacing(300); dlgTextView(s); } dlgHBoxLayout { dlgStretch(1); dlgPushButton("-Close") dlgReject(); } }; } void SaveList(void) { string FileName; schematic(SCH) FileName = filesetext(SCH.name, OutputFormat == ofHTML ? ".htm" : ".bom"); FileName = dlgFileSave("Save Bill Of Material", FileName); if (FileName) { string a[]; if (!fileglob(a, FileName) || dlgMessageBox("File '" + FileName + "' exists\n\nOverwrite?", "+&Yes", "-&No") == 0) { output(FileName, "wt") { printf("%s", MakeList()); // using "%s" to avoid problems if list contains any '%' } } } } int ReadDatabase(string FileName) { string data; if (fileread(data, FileName) > 0) { strsplit(Database, data, '\n'); DatabaseSeparator = (strchr(Database[0], '\t') > -1) ? '\t' : ';'; DatabaseFile = FileName; return 1; } return 0; } // --- Create a new database ------------------------------------------------- string Headers[]; int NumHeaders; int SelectedHeader; int NewDatabaseHeaderOk(string Name) { for (int i = 0; i < NumHeaders; i++) { if (Name == Headers[i]) { dlgMessageBox("Name already defined!"); return 0; } } return 1; } void NewDatabaseEdit(string Title, string Name) { int NewName = !Name; dlgDialog(Title + " Header") { dlgLabel("&Name:"); dlgStringEdit(Name); dlgHBoxLayout { dlgStretch(1); dlgPushButton("+Ok") { Name = StripWhiteSpace(Name); if (!NewName) { if (Name == Headers[SelectedHeader] || NewDatabaseHeaderOk(Name)) { Headers[SelectedHeader] = Name; dlgAccept(); } } else if (Name) { if (NewDatabaseHeaderOk(Name)) { SelectedHeader = NumHeaders; Headers[NumHeaders] = Name; Headers[++NumHeaders] = ""; dlgAccept(); } } else dlgMessageBox("Name can't be empty!"); } dlgPushButton("-Cancel") dlgReject(); } }; } void NewDatabase(void) { DatabaseFile = ""; Database[0] = ""; GenerateList(); dlgRedisplay(); Headers[0] = "Key"; Headers[1] = ""; NumHeaders = 1; SelectedHeader = -1; int result = dlgDialog("New Database") { dlgHBoxLayout { dlgVBoxLayout { dlgLabel("&Headers"); dlgListBox(Headers, SelectedHeader) NewDatabaseEdit("Edit", Headers[SelectedHeader]); } dlgVBoxLayout { dlgPushButton("&Add") NewDatabaseEdit("New", ""); dlgPushButton("&Del") { if (SelectedHeader > 0) { for (int i = SelectedHeader; i < NumHeaders - 1; i++) Headers[i] = Headers[i + 1]; Headers[--NumHeaders] = ""; if (SelectedHeader >= NumHeaders) SelectedHeader = NumHeaders - 1; } else dlgMessageBox("Can't delete the \"Key\" header!\n\nUse \"Edit\" to change it."); } dlgPushButton("&Edit") NewDatabaseEdit("Edit", Headers[SelectedHeader]); } } dlgHBoxLayout { dlgStretch(1); dlgPushButton("+Ok") { if (NumHeaders > 1) dlgAccept(); else dlgMessageBox("Please add at least one header!"); } dlgPushButton("-Cancel") dlgReject(); } }; if (result) { string sep; for (int i = 0; Headers[i]; i++) { Database[0] += sep + Headers[i]; sep = "\t"; } DatabaseSeparator = '\t'; DatabaseModified = 1; GenerateList(); } } // --- void LoadDatabase(void) { string FileName = dlgFileOpen("Choose database file", DatabaseFile, "Database files (*.tsv *.csv);;All files (*)"); if (FileName) { if (ReadDatabase(FileName)) { GenerateList(); DatabaseModified = 0; } } } int SaveDatabase(void) { if (!DatabaseFile) { string ext = (DatabaseSeparator == '\t') ? ".tsv" : ".csv"; DatabaseFile = dlgFileSave("Save database file", "", "Database files (*" + ext + ");;All files (*)"); if (!DatabaseFile) return 0; if (fileext(DatabaseFile) != ext) DatabaseFile += ext; } fileerror(); output(DatabaseFile, "wt") { for (int i = 0; Database[i]; i++) printf("%s\n", Database[i]); }; return !fileerror(); } void EditDatabaseEntry(string Key, int Entry) { string Header[]; string Data[]; int Fields = strsplit(Header, Database[0], DatabaseSeparator); strsplit(Data, Database[Entry], DatabaseSeparator); if (!Data[0]) Data[0] = Key; int result = dlgDialog("Edit Database") { dlgGridLayout { for (int f = 0; f < Fields; f++) { dlgCell(f, 0) dlgLabel(Header[f]); dlgCell(f, 1) if (f) { dlgStringEdit(Data[f]); } else { dlgLabel(Data[f]); } } } dlgHBoxLayout { dlgStretch(1); dlgPushButton("+Ok") dlgAccept(); dlgPushButton("-Cancel") dlgReject(); } }; if (result) { for (int f = 0; f < Fields; f++) Data[f] = StripWhiteSpace(Data[f]); Database[Entry] = strjoin(Data, DatabaseSeparator); DatabaseModified = 1; GenerateList(); } } void EditDatabase(void) { if (Database[0]) { if (Selected) { string a[]; int KeyField = strsplit(a, Lines[0], '\t'); strsplit(a, Lines[Selected], '\t'); string key = a[KeyField]; string data; int entry; for (entry = 0; Database[entry]; entry++) { strsplit(a, Database[entry], DatabaseSeparator); if (a[0] == key) { data = Database[entry]; break; } } EditDatabaseEntry(key, entry); } else dlgMessageBox("Please select a list entry first!"); } else dlgMessageBox("Please load a database file first!"); } int OkToClose(void) { if (DatabaseModified) { switch (dlgMessageBox("Database has been modified\n\nSave?", "+&Yes", "&No", "-Cancel")) { case 0: return SaveDatabase(); case 1: break; case 2: return 0; } } return 1; } void DisplayHelp(void) { dlgDialog("Bill Of Material - Help") { dlgHBoxLayout dlgSpacing(400); dlgHBoxLayout { dlgVBoxLayout dlgSpacing(300); dlgTextView(HelpText); } dlgHBoxLayout { dlgStretch(1); dlgPushButton("-Close") dlgReject(); } }; } CollectPartData(); GenerateList(); dlgDialog("Bill Of Material") { dlgListView("", Lines, Selected) EditDatabase(); dlgHBoxLayout { dlgLabel("Database:"); dlgLabel(DatabaseFile, 1); dlgStretch(1); dlgPushButton("&Load") if (OkToClose()) LoadDatabase(); dlgPushButton("&New") if (OkToClose()) NewDatabase(); } dlgHBoxLayout { dlgGroup("List type") { dlgRadioButton("&Parts", ListType) GeneratePartList(); dlgRadioButton("&Values", ListType) GenerateValueList(); } dlgGroup("Output format") { dlgRadioButton("&Text", OutputFormat); dlgRadioButton("&HTML", OutputFormat); } } dlgHBoxLayout { dlgStretch(1); dlgPushButton("+Edit") EditDatabase(); dlgPushButton("Vie&w") ViewList(); dlgPushButton("&Save...") SaveList(); dlgPushButton("&Help") DisplayHelp(); dlgPushButton("-Close") if (OkToClose()) dlgAccept(); } };