Guest User

org-bom

a guest
Oct 20th, 2011
82
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. ;    Copyright 2011 Free Software Foundation, Inc.
  2. ;
  3. ;    Filename: org-bom.el
  4. ;    Version: 0.3
  5. ;    Author: Christian Fortin <frozenlock AT gmail DOT com>
  6. ;    Keywords: org, bill-of-materials, collection, tables
  7. ;    Description: Create a bill-of-materials (bom) of the entire org buffer
  8. ;
  9. ;    This program is free software: you can redistribute it and/or modify
  10. ;    it under the terms of the GNU General Public License as published by
  11. ;    the Free Software Foundation, either version 3 of the License, or
  12. ;    (at your option) any later version.
  13. ;
  14. ;    This program is distributed in the hope that it will be useful,
  15. ;    but WITHOUT ANY WARRANTY without even the implied warranty of
  16. ;    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  17. ;    GNU General Public License for more details.
  18. ;
  19. ;    You should have received a copy of the GNU General Public License
  20. ;    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  21. ;
  22. ;----------------------- TUTORIAL --------------------
  23. ;-------- This tutorial should read as an org-file----
  24. ;-----------------------------------------------------
  25.  
  26. ;; * BOM introduction
  27.  
  28. ;;   This add-on collects components across the entire org buffer (even
  29. ;;   in drawers), making it easy to retrieve and sort data. It uses the
  30. ;;   column special name as a landmark. We will refer to them as
  31. ;;   'Keywords'. The keywords are searched using a string-match function,
  32. ;;   which gives the ability to have multiple column with the same
  33. ;;   functionality, but also to use the column name as we would usually
  34. ;;   with org-mode. For example, we can have 'tag' and 'tag2', both are
  35. ;;   recognized by the BOM add-on and can be used in a spreadsheet-like
  36. ;;   formula without any confusion. The keywords are also
  37. ;;   case-insensitive. 'Component' and 'component' will work in the same
  38. ;;   way.
  39.  
  40. ;;   The BOM is usually used with a dynamic block. (You can use the
  41. ;;   different functions in emacs-lisp code, but this is beyond the
  42. ;;   purpose of this tutorial.) Here is the basic dynamic block:
  43.  
  44. ;; :  #+BEGIN: bom
  45. ;; :  #+END:
  46.  
  47. ;;   And here is what we obtain at this point:
  48. ;; :  #+BEGIN: bom
  49. ;; : | Section | Tag | Component | Quantity |
  50. ;; : |---------+-----+-----------+----------|
  51. ;; :  #+END:
  52.  
  53. ;;   The table is empty, because we have to either:
  54. ;;   1. Add keywords in a table;
  55. ;;   2. Add a line-component.
  56.  
  57. ;; * BOM keywords
  58. ;; ** Component
  59.  
  60. ;;      This is the most important keyword and act as the trigger. For
  61. ;;   this example, let's say we write down things we want to buy. In
  62. ;;   this case, a new keyboard for our computer.  This is how the
  63. ;;   table should be:
  64.  
  65. ;; :  |   | Material  |
  66. ;; :  | ! | Component |
  67. ;; :  |---+-----------|
  68. ;; :  |   | Keyboard  |
  69.  
  70. ;;   The '!' character is used in org table to specify column name, such
  71. ;;   as our keyword, 'component'.
  72. ;;   And here is what the bill-of-materials for this table is:
  73.      
  74. ;; :  #+BEGIN:  bom
  75. ;; : | Section   | Tag | Component | Quantity |
  76. ;; : |-----------+-----+-----------+----------|
  77. ;; : | Component |     | Keyboard  |        1 |
  78. ;; :  #+END:
  79.  
  80. ;;   As you can see, the heading (Component) was automatically
  81. ;;   used as the 'section', which doesn't require attention for
  82. ;;   now. The quantity is, unsurprisingly, 1. There is nothing in the tag
  83. ;;   column for now, so let's dismiss it by adding the parameter *:no-tag
  84. ;;   t*.
  85. ;;   This will results in the following:
  86. ;; :  #+BEGIN: bom  :no-tag t
  87. ;; : | Section   | Component | Quantity |
  88. ;; : |-----------+-----------+----------|
  89. ;; : | Component | Keyboard  |        1 |
  90. ;; :  #+END:
  91.  
  92. ;;   Now suppose that our friend too wants a new keyboard.
  93.  
  94. ;; :  |   | For    | Material  |
  95. ;; :  | ! |        | Component |
  96. ;; :  |---+--------+-----------|
  97. ;; :  |   | Me     | Keyboard  |
  98. ;; :  |   | Friend | Keyboard  |
  99.      
  100. ;; :   #+BEGIN: bom :no-tag t
  101. ;; :  | Section   | Component | Quantity |
  102. ;; :  |-----------+-----------+----------|
  103. ;; :  | Component | Keyboard  |        2 |
  104. ;; :   #+END:
  105.  
  106. ;;   As expected, we get 2 keyboards.
  107.      
  108. ;; ** Section
  109.    
  110. ;;      The section is used to separate what would otherwise be an
  111. ;;   identical component. Suppose we don't want our friend's wishes to be
  112. ;;   in the same BOM as our, but still have them in the same table.
  113.  
  114. ;; :  |   | For     | Material  |
  115. ;; :  | ! | Section | Component |
  116. ;; :  |---+---------+-----------|
  117. ;; :  |   | Me      | Keyboard  |
  118. ;; :  |   | Friend  | Keyboard  |
  119.  
  120. ;;   This will results in the following BOM:
  121.  
  122. ;; :  #+BEGIN: bom :no-tag t
  123. ;; : | Section | Component | Quantity |
  124. ;; : |---------+-----------+----------|
  125. ;; : | Friend  | Keyboard  |        1 |
  126. ;; : | Me      | Keyboard  |        1 |
  127. ;; :  #+END:
  128.  
  129. ;;   Please note that when a component is given a section, it isn't
  130. ;;   associated with the heading anymore. As an alternative, you can set
  131. ;;   a ':SECTION:' property in the heading, which will be inherited by
  132. ;;   all the components _without_ a specified section.
  133. ;;   Section's priorities are as follow:
  134.  
  135. ;;   1. Given section with the 'section' keyword;
  136. ;;   2. The SECTION property;
  137. ;;   3. The heading.
  138.  
  139. ;; ** Qty
  140.  
  141. ;;      With this keyword, it is possible to specify a quantity for the
  142. ;;   associated component. In our always improving scenario, we now want to
  143. ;;   give a keyboard to another of our friend (as a gift). This is going to
  144. ;;   be bought at the same time as our keyboard, so they belong together.
  145.  
  146. ;; :  |   | For     | Material  |     |
  147. ;; :  | ! | Section | Component | Qty |
  148. ;; :  |---+---------+-----------+-----|
  149. ;; :  |   | Me      | Keyboard  |   2 |
  150. ;; :  |   | Friend  | Keyboard  |   1 |
  151.  
  152. ;; :   #+BEGIN: bom :no-tag t
  153. ;; :  | Section | Component | Quantity |
  154. ;; :  |---------+-----------+----------|
  155. ;; :  | Friend  | Keyboard  |        1 |
  156. ;; :  | Me      | Keyboard  |        2 |
  157. ;; :   #+END:
  158.      
  159. ;;   *Important*: If Qty keyword is present, then any empty field will
  160. ;;   be considered as _zero_. This way, multiple column quantity are
  161. ;;   made quite easily:
  162.      
  163. ;; :  |   | For     | Material  | Personal | Gift |
  164. ;; :  | ! | Section | Component |      Qty | Qty2 |
  165. ;; :  |---+---------+-----------+----------+------|
  166. ;; :  |   | Me      | Keyboard  |        1 | 1    |
  167. ;; :  |   | Friend  | Keyboard  |        1 |      |
  168.  
  169. ;; :   #+BEGIN: bom :no-tag t
  170. ;; :  | Section | Component | Quantity |
  171. ;; :  |---------+-----------+----------|
  172. ;; :  | Friend  | Keyboard  |        1 |
  173. ;; :  | Me      | Keyboard  |        2 |
  174. ;; :   #+END:  
  175.  
  176. ;; ** Tag
  177.  
  178. ;;      When a BOM starts to get big, we often need a quick reminder of
  179. ;;   why we need certain component. Another use is also to identify the
  180. ;;   component. As the Qty keyword, multiple Tag columns can be associated
  181. ;;   with a single component. Here we will simply use the tag as a reminder
  182. ;;   of what we want to look for in the store.
  183.  
  184. ;; :  |   | For     | Material  | Personal | Gift | Need               |
  185. ;; :  | ! | Section | Component |      Qty | Qty2 | Tag                |
  186. ;; :  |---+---------+-----------+----------+------+--------------------|
  187. ;; :  |   | Me      | Keyboard  |        1 | 1    | Matching colors    |
  188. ;; :  |   | Friend  | Keyboard  |        1 |      | Dinosaurs pictures |
  189.  
  190. ;;   To show the tag column in the BOM, we simply remove the no-tag
  191. ;;   parameter.
  192. ;; :  #+BEGIN: bom
  193. ;; : | Section | Tag                | Component | Quantity |
  194. ;; : |---------+--------------------+-----------+----------|
  195. ;; : | Friend  | Dinosaurs pictures | Keyboard  |        1 |
  196. ;; : | Me      | Matching colors    | Keyboard  |        2 |
  197. ;; :  #+END:  
  198.  
  199.  
  200. ;;   If two Tag columns are present for a single Component column, the
  201. ;;   tags will be associated with this component, separated by a comma.
  202.  
  203. ;; * Renaming BOM columns
  204.      
  205. ;;      It is possible to rename the BOM columns with the following
  206. ;;   parameters:
  207. ;;   - col-name-component
  208. ;;   - col-name-section
  209. ;;   - col-name-quantity
  210. ;;   - col-name-tag
  211. ;;   - col-name-description
  212. ;;   - col-name-price
  213.  
  214. ;;   This is how our renamed BOM would look like:
  215.      
  216. ;; :  #+BEGIN: bom :col-name-section For :col-name-tag Need :col-name-component Stuff :col-name-quantity Qty
  217. ;; : | For    | Need               | Stuff    | Qty |
  218. ;; : |--------+--------------------+----------+-----|
  219. ;; : | Friend | Dinosaurs pictures | Keyboard |   1 |
  220. ;; : | Me     | Matching colors    | Keyboard |   2 |
  221. ;; :  #+END:  
  222.  
  223. ;; * Multiple component's column
  224.  
  225. ;;      There is two way to add components in a section. Either by adding
  226. ;;   other rows with the same section's name, or by adding other
  227. ;;   columns. Both have their uses and they should come to you quite
  228. ;;   naturally. In our example, we want more stuff.
  229.  
  230. ;; :  |   | For     | Material  | Personal | Gift | Need               | Stuff     | More stuff | Much more stuff | How many |
  231. ;; :  | ! | Section | Component |      Qty | Qty2 | Tag                | Component | Component  | Component       | Qty      |
  232. ;; :  |---+---------+-----------+----------+------+--------------------+-----------+------------+-----------------+----------|
  233. ;; :  |   | Me      | Keyboard  |        1 | 1    | Matching colors    | Mouse     | Headset    | USB flash drive | 23       |
  234. ;; :  |   | Friend  | Keyboard  |        1 |      | Dinosaurs pictures |           |            |                 |          |
  235. ;; :  |   | Friend  |           |          |      |                    |           |            | CDs             | 50       |
  236. ;; :  |   | Friend  | Mouse     |        1 |      |                    |           |            |                 |          |
  237.      
  238. ;;   This is beginning to get interesting. Note that even if we can
  239. ;;   name the additional columns 'Component2' or 'ComponentAAA',
  240. ;;   there's no use to do it if no table-formula uses the column
  241. ;;   names.
  242.  
  243. ;; * Precise section selection
  244. ;;   Now suppose we want to get OUR to-buy list. Simply specify
  245. ;;   the section's parameter *:section Me*:
  246.  
  247. ;; :   #+BEGIN: bom :section Me
  248. ;; :  | Tag             | Component       | Quantity |
  249. ;; :  |-----------------+-----------------+----------|
  250. ;; :  |                 | Headset         |        1 |
  251. ;; :  | Matching colors | Keyboard        |        2 |
  252. ;; :  |                 | Mouse           |        1 |
  253. ;; :  |                 | USB flash drive |       23 |
  254. ;; :   #+END:  
  255.      
  256. ;;   Wait, where's the section column?  Well we don't need it anymore,
  257. ;;   as we specified one.
  258.  
  259. ;;   A '+' sign will specify we want more than a single section. *:section
  260. ;;   Me+Friend* will select both section, and add the quantity and tags
  261. ;;   for each component.
  262.  
  263. ;; :  #+BEGIN: bom :section Me+Friend
  264. ;; : | Tag                                 | Component       | Quantity |
  265. ;; : |-------------------------------------+-----------------+----------|
  266. ;; : |                                     | CDs             |       50 |
  267. ;; : |                                     | Headset         |        1 |
  268. ;; : | Dinosaurs pictures, Matching colors | Keyboard        |        3 |
  269. ;; : |                                     | Mouse           |        2 |
  270. ;; : |                                     | USB flash drive |       23 |
  271. ;; :  #+END:
  272.  
  273. ;;   *Do not* put a whitespace between the section name and the '+' sign.
  274. ;;   Speaking of whitespace, if you need one in a section name, simply
  275. ;;   put it in a string:
  276. ;; : #+BEGIN: bom :section "Section with whitespace"
  277.  
  278. ;;   We can also return every section that matches at least what we
  279. ;;   provide. To activate this, use *:part-match t*. With this, if we
  280. ;;   write "fr", the Friend section is returned. If we had another
  281. ;;   section named "Frosting", than Friend and Frosting would have been
  282. ;;   merged and we would have a total for both section.
  283.  
  284. ;; :  #+BEGIN: bom :section fr :part-match t
  285. ;; : | Tag                | Component | Quantity |
  286. ;; : |--------------------+-----------+----------|
  287. ;; : |                    | CDs       |       50 |
  288. ;; : | Dinosaurs pictures | Keyboard  |        1 |
  289. ;; : |                    | Mouse     |        1 |
  290. ;; :  #+END:
  291.  
  292. ;;   It is also possible to specify that we don't want any section
  293. ;;   containing "fr". For this, use the parameter *:remove t*.
  294.  
  295. ;; :  #+BEGIN: bom :section fr :part-match t :remove t
  296. ;; : | Tag             | Component       | Quantity |
  297. ;; : |-----------------+-----------------+----------|
  298. ;; : |                 | Headset         |        1 |
  299. ;; : | Matching colors | Keyboard        |        2 |
  300. ;; : |                 | Mouse           |        1 |
  301. ;; : |                 | USB flash drive |       23 |
  302. ;; :  #+END:
  303.  
  304. ;;   In this case, getting all sections not containing "fr" is the
  305. ;;   equivalent of choosing the section "Me".
  306.  
  307. ;;   If you simply want the components from the current heading, use the
  308. ;;   parameter *:local-only t*. This will return components with the
  309. ;;   current heading as their section, which is the default of every
  310. ;;   component if no section is provided. If a section has been provided to
  311. ;;   a component (and is not exactly equal to the heading), the component
  312. ;;   will not be returned.
  313.  
  314. ;;   Here, we don't have any component under this heading:
  315. ;; :  #+BEGIN: bom :local-only t
  316. ;; : | Tag | Component | Quantity |
  317. ;; : |-----+-----------+----------|
  318. ;; :  #+END:
  319.  
  320. ;; * BOM total
  321. ;;   This is all really interesting, but when we're in a shop, we want
  322. ;;   to know how many of each item we have to buy, we need a *total*.
  323. ;;   For this, simply add the *:total t* parameter. We will also remove
  324. ;;   the tags once again by using *:no-tag t*.
  325.  
  326. ;; :  #+BEGIN: bom :total t :no-tag t
  327. ;; : | Component       | Quantity |
  328. ;; : |-----------------+----------|
  329. ;; : | CDs             |       50 |
  330. ;; : | Headset         |        1 |
  331. ;; : | Keyboard        |        3 |
  332. ;; : | Mouse           |        2 |
  333. ;; : | USB flash drive |       23 |
  334. ;; :  #+END:
  335.  
  336. ;;   This is the equivalent of merging every sections together.
  337. ;; * Adding a component without a table
  338.  
  339. ;;   There is another option you might need. If you ever want to
  340. ;;   add a component without a table, use the #+BOM commentary. As any
  341. ;;   other org-mode commentary, this one won't appear when exported to
  342. ;;   another document (pdf, html, docbook..). It will, however, enable
  343. ;;   you to add a single component in the bill-of-materials. Here is an
  344. ;;   example:
  345. ;; :  #+BOM: Keyboard :section Need :tag "Matching colors"
  346.  
  347. ;;   As with the table components, you can simply give a component name if
  348. ;;   you desire. If no section is given, it will be inherited as an
  349. ;;   ordinary component in a table: a section property or the current
  350. ;;   heading.
  351.  
  352. ;; * Adding details
  353. ;;   There is two way to add details to a BOM. The first one is to setq
  354. ;;   `org-bom-details' with a plist containing, depending on your
  355. ;;   needs, :description, :datasheet-pdf and :price. You must, however, at
  356. ;;   least have the component name in the :name property. Here is an
  357. ;;   example on how to set this variable:
  358.  
  359. ;; #+BEGIN_SRC emacs-lisp
  360. ;; (setq org-bom-details '((:name "Keyboard" :description
  361. ;;                           "Something" :price "40")
  362. ;;                           (:name "CDs" :description "Not
  363. ;;                           DVDs" :datasheet-pdf "CD.pdf")))
  364. ;; #+END_SRC
  365. ;;   Please note that the price is a *string*.
  366.  
  367. ;;   The other method, valid for the current buffer only, is to give one
  368. ;;   or more bom-details table. It is recognized when a table is named as
  369. ;;   such:
  370. ;; :  #+TBLNAME: bom-details
  371.  
  372. ;;   Once again, the column names are used. Contrary to the normal BOM
  373. ;;   keywords however, these are case-sensitive and must be written
  374. ;;   exactly as their property name. For example, the column of the
  375. ;;   property ':name' must be 'name'.
  376. ;; :  #+TBLNAME: bom-details
  377. ;; : | ! | name     | description  | price |
  378. ;; : |---+----------+--------------+-------|
  379. ;; : |   | Keyboard | Used to type |    40 |
  380. ;; : |   | CDs      |              |       |
  381.  
  382. ;;   Any bom-details table will temporarily overshadow the
  383. ;;   `org-bom-details' variable, but won't erase or modify it. This means
  384. ;;   you can safely use a bom-details table if you need to change some
  385. ;;   local buffer description, while using `org-bom-details' in multiple
  386. ;;   buffer.
  387.  
  388. ;;   Look at the CDs description. When a field is empty, it is *not* used
  389. ;;   and BOM falls back to the property in the `org-bom-details'
  390. ;;   variable.
  391.  
  392. ;; ** Description
  393.    
  394. ;;    You can add a description column in a BOM by adding the
  395. ;;    *:description t* parameter.
  396.  
  397. ;; :   #+BEGIN: bom :total t :no-tag t :description t
  398. ;; :  | Component       | Quantity | Description  |
  399. ;; :  |-----------------+----------+--------------|
  400. ;; :  | CDs             |       50 | Not DVDs     |
  401. ;; :  | Headset         |        1 | N/A          |
  402. ;; :  | Keyboard        |        3 | Used to type |
  403. ;; :  | Mouse           |        2 | N/A          |
  404. ;; :  | USB flash drive |       23 | N/A          |
  405. ;; :   #+END:
  406.  
  407. ;;    See how the CDs' description wasn't the empty field from the
  408. ;;    bom-details table.
  409.  
  410. ;; ** Price
  411.    
  412. ;;    You can add a price column in a BOM by adding the *:price t*
  413. ;;    parameter.
  414.  
  415. ;; :   #+BEGIN: bom :total t :no-tag t :description t :price t
  416. ;; :  | Component       | Quantity | Price | Description  |
  417. ;; :  |-----------------+----------+-------+--------------|
  418. ;; :  | CDs             |       50 |       | Not DVDs     |
  419. ;; :  | Headset         |        1 |       | N/A          |
  420. ;; :  | Keyboard        |        3 |   120 | Used to type |
  421. ;; :  | Mouse           |        2 |       | N/A          |
  422. ;; :  | USB flash drive |       23 |       | N/A          |
  423. ;; :  |-----------------+----------+-------+--------------|
  424. ;; :  | TOTAL:          |          |   120 |              |
  425. ;; :      #+TBLFM: @>$3=vsum(@I..@>>)
  426. ;; :   #+END:
  427. ;;    The price is automatically multiplied by the quantity of each
  428. ;;    component. In addition, a total row is added at the table's bottom
  429. ;;    with a vertical sum formula.
  430.    
  431. ;; ** Datasheet
  432.    
  433. ;;    This is a special property and must be used only if you intend to
  434. ;;    export in a pdf document. See [[LaTeX mode and bom-datasheet]] for more details.
  435.    
  436. ;; * List of BOM parameters
  437. ;;   Here is a list of all the parameters usable in a BOM dynamic block,
  438. ;;   as seen throughout this tutorial:
  439.  
  440. ;;   - no-tag :: Remove the tags column
  441. ;;   - section :: Select this section (or more if there's a + sign)
  442. ;;   - part-match :: Select every section with at least the string
  443. ;;                   provided for the section parameter
  444. ;;   - remove :: Select every sections except the one(s) provided
  445. ;;   - descripton :: Add the description column
  446. ;;   - price :: Add the price column and a total row at the bottom of the
  447. ;;              table
  448. ;;   - col-name-*** :: Rename the associated column
  449. ;; * Advanced and elisp functions
  450. ;; ** Speed up updates
  451. ;;    Each BOM dynamic block scans the entire buffer individually. While
  452. ;;    it is necessary that each block be able to update itself, it
  453. ;;    becomes a waste when the command `org-update-all-dblocks' is
  454. ;;    used. (The components usually aren't changing from a dblock evaluation to
  455. ;;    another.)
  456.    
  457. ;;    In order to speed up updates, there's a variable that can be used
  458. ;;    to stop each BOM dblock from doing a buffer-wide scan. To disable the
  459. ;;    scans, set `org-bom-update-enable' to nil.
  460.  
  461. ;;    The author uses a function similar to this one to speed up updates:
  462. ;; #+BEGIN_SRC emacs-lisp :exports code
  463. ;; (defun reg-update-project (&optional latex-mode)
  464. ;;   "Update every table and dynamic block in the buffer. If latex-mode
  465. ;; is non-nil, various latex commands will be inserted."
  466. ;;   (interactive)
  467. ;;   (org-table-iterate-buffer-tables)
  468. ;;   (org-bom-total); manually update the BOM database
  469. ;;   (let ((org-bom-update-enable nil)
  470. ;;  (org-bom-latex-mode latex-mode)
  471. ;;  (org-bom-details (copy-tree org-bom-details)));so we don't overwrite
  472. ;;     (org-bom-check-for-details-table); manually update `org-bom-details'
  473. ;;     (org-update-all-dblocks))
  474. ;;   (message "Project updated"))
  475. ;; #+END_SRC
  476.      
  477. ;; ** LaTeX mode and bom-datasheet
  478. ;;   This mode isn't fully integrated to org-mode and should be seen as a
  479. ;;   hack. It is however useful to the author, which is why it is
  480. ;;   explained here.
  481.  
  482. ;;   Set the `org-bom-latex-mode' variable to non-nil in order to
  483. ;;   activate the latex-mode. If set, all BOM dynamic block will insert
  484. ;;   some latex commands.
  485.  
  486. ;;   These commands targets:
  487. ;;   - Tags :: When there is more tags than `org-bom-latex-max-tags' per
  488. ;;             component, the remaining tags are put in a pdf comment.
  489. ;;   - Component name :: If a datasheet exists for the component, its
  490. ;;                       name will become a link to its datasheet.
  491.  
  492.              
  493. ;;   If you ever activate the LaTeX mode, use the bom-datasheet dynamic
  494. ;;   block at the end of your document. The optional parameter
  495. ;;   *:description t* will add a summary of all the components used in
  496. ;;   this buffer with their description, just before the datasheets.
  497.  
  498. ;; :  #+BEGIN: bom-datasheet
  499. ;; :  
  500. ;; :  #+LaTeX: \includepdf[pages=-,landscape=true,addtotoc={1, subsection, 1, CDs,CD.pdf}]{\DATASHEETPATH/CD.pdf}
  501. ;; :  
  502. ;; :  #+END:
  503.  
  504. ;;   As you may have noticed, there's a LaTeX variable in this command:
  505. ;;         \DATASHEETPATH. In order to work, you must set this variable
  506. ;;         using:
  507.  
  508. ;; :    #+LATEX_HEADER: \newcommand{\DATASHEETPATH}{Name-of-the-folder/}'
  509.  
  510. ;;  Name-of-the-folder is the folder where the datasheets' files
  511. ;;         are located.
  512. ;-----------------------------------------------------
  513. ;------------------- End of tutorial -----------------
  514. ;-----------------------------------------------------
  515. ;      
  516. ;=====================================================
  517. ; The program begins here
  518. ;=====================================================
  519.  
  520. (require 'org)
  521. (require 'org-table)
  522. (require 'gnus-util)
  523.  
  524. ;========== Global variable section ==========
  525.  
  526. (defvar org-bom-database nil
  527.   "Global variable used to build a database of the components used, as
  528. well as their section, tags and quantity.")
  529.  
  530. (defvar org-bom-details nil
  531.   "Need to be given by the user. A suggested use is to bind it to
  532. a local user's database. Should be a plist with at least \":name\" and
  533. \":description\". It should also contain \":datasheet-pdf\" in order
  534. to use the bom-datasheet dynamic block.")
  535.  
  536. (defvar org-bom-update-enable t
  537.   "Scan the buffer and update the BOM when a dynamic block is
  538. refreshed. Should be set to nil for a buffer-wide dynamic block,
  539. such as with `org-update-all-dblocks'. However, be sure to update
  540. manually with `org-bom-total' in this case.")
  541.  
  542. (defvar org-bom-latex-mode nil
  543.   "If activated, every component's name will be replaced by a reference
  544. to the datasheet and comments might be activated if necessary (large
  545. number of tags). See `org-bom-latex-max-tags'.")
  546.  
  547. (defvar org-bom-latex-max-tags 10
  548.   "Define the maximum number before the tags start being hidden in a
  549. PDF comment. Set to nil to disable.")
  550.  
  551.  
  552. ;========== Database section ==========
  553.  
  554. (defun org-bom-add-component (comp)
  555.   (push comp org-bom-database))
  556.  
  557. (defstruct component name section quantity tag)
  558.  
  559. (defun org-bom-select-in-db (database selector-fn value &optional remove part-match)
  560.  "Return every entry in the database which has the corresponding
  561. value for a given selector. Can be the DATABASE's argument of
  562. itself in case of multiple SELECTOR-FN. The SELECTOR-FN must be
  563. the quoted function, such as 'component-name. If REMOVE is
  564. non-nil, every entry with a match will be discarded rather than
  565. keeped. If PART-MATCH is non-nil, `string-match' function is used
  566. instead of `gnus-string-equal'."
  567.  (when (atom value)
  568.    (setf value (list value)))
  569.  (let ((temp-results database)
  570.        (results nil))
  571.    (dolist (current-value value)
  572.      (setf temp-results
  573.        (funcall (if remove 'remove-if 'remove-if-not)
  574.             #'(lambda (component)
  575.             (let ((current-component (funcall selector-fn component)))
  576.               (if (numberp current-component);if it's a component quantity
  577.                   (equal current-component current-value)
  578.                 (if part-match (string-match current-value current-component)
  579.                   (gnus-string-equal current-component current-value)))))
  580.             (if remove temp-results database)))
  581.      (unless remove (setf results (append results temp-results)))); cumulate the results
  582.    (when remove (setf results temp-results))
  583.    (org-bom-sort results)))
  584.  
  585. (defun org-bom-check-and-push-to-db (name section quantity tag)
  586.   "Check if the combo name-section is already in the database. If it
  587. is, add the quantity and the tag, otherwise create a new entry."
  588.   (let ((exists-flag nil))
  589.     (dolist (temp-car-db org-bom-database) ;For every item in the database...
  590.       (when (and (gnus-string-equal (component-name temp-car-db)
  591.                     name)
  592.          (gnus-string-equal (component-section temp-car-db)
  593.                     section))
  594.     (setf (component-quantity temp-car-db)
  595.           (+ (component-quantity temp-car-db) quantity)) ; if the combo name-section exists, simply add the quantity
  596.     (setf (component-tag temp-car-db)
  597.           (append tag (component-tag temp-car-db)))
  598.     (setf exists-flag t))) ; set the exist flag t
  599.     (if (not exists-flag) (org-bom-add-component (make-component :name name ; if it's a new component (in the section), then add it in the database
  600.                               :section section
  601.                               :quantity quantity
  602.                               :tag tag)))))
  603.  
  604. ;========== End of database section ==========
  605.  
  606. (defun org-bom-total (&optional section-override)
  607.   "Go to every tables in the buffer and get info from them."
  608.   (interactive)
  609.   (save-excursion
  610.     (save-restriction
  611.       (setq org-bom-database nil) ; Reset the database before each new buffer-wide scan
  612.       (widen)
  613.       (org-bom-prepare-linedata-for-database section-override) ;scan for line items
  614.       (org-table-map-tables (lambda () (org-bom-prepare-tabledata-for-database
  615.                     section-override)) t)
  616.       (setq org-bom-database
  617.         (org-bom-sort org-bom-database))))
  618.   (message "org-bom-total"))
  619.  
  620. (defun org-bom-sort (database)
  621.   "Returns the DATABASE sorted alphabetically by component name"
  622.   (sort database ;sort in alphabetical order
  623.     (lambda (arg1 arg2)
  624.       (gnus-string< (component-name arg1)
  625.             (component-name arg2)))))
  626.  
  627. (defun org-bom-get-keyword-column-numbers ()
  628.   "Return a list of plists composed of \"components\", \"qty\",
  629. \"tag\" and \"section\" column numbers."
  630.   (org-table-get-specials)
  631.   (let ((column-names org-table-column-names)
  632.     results
  633.     component-col
  634.     qty-col
  635.     tag-col
  636.     section-col
  637.     (push-the-list '(push (list :name component-col
  638.                     :qty qty-col
  639.                     :tag tag-col
  640.                     :section section-col) results)))
  641.     (while column-names
  642.       (let* ((temp-name (pop column-names))
  643.          (name (car temp-name))
  644.          (ncolumn (string-to-number (cdr (last temp-name)))))
  645.     (when (string-match "section" name)
  646.       (setq section-col ncolumn))
  647.     (when (string-match "component" name)
  648.       (when component-col ;test if it's the first component column
  649.         (eval push-the-list))
  650.       (setq qty-col nil tag-col nil);set them all to nil
  651.       (setq component-col ncolumn))
  652.     (when (string-match "qty" name)
  653.       (push ncolumn qty-col))
  654.     (when (string-match "tag" name)
  655.       (push ncolumn tag-col))))
  656.     (eval push-the-list)
  657.     results))
  658.  
  659. (defun org-bom-after-header-line ()
  660.  "Go to and return the position of the first non-header line."
  661.  (let ((beg (org-table-begin))
  662.        (end (org-table-end)))
  663.    (goto-char beg)
  664.    (if (and (re-search-forward org-table-dataline-regexp end t)
  665.         (re-search-forward org-table-hline-regexp end t)
  666.         (re-search-forward org-table-dataline-regexp end t))
  667.        (match-beginning 0))))
  668.  
  669. (defun org-bom-prepare-linedata-for-database (&optional section-override)
  670.   "Scan the buffer and add line-components to database. Search for an
  671. org-mode comment \"#+BOM:\". Everything before the keys (:section, :qty, :tag)
  672. is considered to be the component's name, except the last whitespaces.
  673. The same \"section\" priority is in this order: Given with the :section key,
  674. in a :SECTION: property, or the org heading."
  675.   (goto-char (point-min))
  676.   (while (re-search-forward "^[ \t]*#\\+BOM:[ \t]+\\([^:\n]+\\)\\(.*\\)?" nil t)
  677.     (let* ((name (org-no-properties (match-string 1)))
  678.        (params (read (concat "(" (match-string 2) ")")))
  679.        (quantity (or (plist-get params :qty) 1))
  680.        (section-given (plist-get params :section))
  681.        (tag (plist-get params :tag)))
  682.       (setq name (org-trim name))
  683.       (when section-given
  684.     (unless (stringp section-given)
  685.       (setq section-given (symbol-name section-given))))
  686.       (when tag
  687.     (unless (stringp tag)
  688.       (if (numberp tag)
  689.           (setq tag (number-to-string tag))
  690.         (setq tag (symbol-name tag)))))
  691.       (org-bom-check-and-push-to-db
  692.        name
  693.        (or section-override
  694.        section-given
  695.        (org-bom-check-possible-section))
  696.       quantity
  697.       (list (list tag))))));double `list' because there's a list per tag and a list per item
  698.  
  699.      
  700. (defun org-bom-check-possible-section ()
  701.   "Return a possible section from properties or heading"
  702.   (let ((section-property (org-entry-get nil "SECTION" 'selective)))
  703.     (when section-property
  704.       (if (string= "" section-property)
  705.       (setq section-property nil))) ;; set to nil if empty string
  706.     (or section-property
  707.     (if (org-before-first-heading-p)
  708.         "" ; If we are before the first heading, default to "".
  709.       (substring-no-properties (org-get-heading t t))))))
  710.      
  711. (defun org-bom-prepare-tabledata-for-database (&optional section-override)
  712.   "Scan in the current table for any column named as \"Component\". If
  713. a name in the \"Component\" column starts with the '-' character, it
  714. will be escaped. Optional info \"section\" must be somewhere before
  715. the components' column. If no section is given, then will check for
  716. a :SECTION: property. If none is found, the heading will be taken
  717. as a section. A section-override will asign every single component
  718. to this section. Optional info \"Qty\" and \"Tag\" should be a
  719. column somewhere after the components column, as many times as
  720. needed. To add another components column, simply add another
  721. \"Component\". Note that if a \"Qty\" column is present, it will
  722. default to 0 if the field is empty. This gives the possibility to
  723. have many quantity columns without the need to enter 0 multiple
  724. times."
  725.  
  726.   (unless (org-at-table-p) (error "Not at a table"))
  727.   (org-bom-after-header-line)
  728.   (forward-line -1) ;Don't go on the first dataline yet
  729.   (let ((end (org-table-end))
  730.     (beg (org-table-begin))
  731.     (dline org-table-dataline-regexp)
  732.     (possible-section (org-bom-check-possible-section)))
  733.     (while (re-search-forward dline end t)
  734.       (dolist (current-comp (org-bom-get-keyword-column-numbers))
  735.     (when (plist-get current-comp :name) ;test if there's a component column
  736.     (org-bom-check-and-push-to-db  
  737.      (org-bom-comp-get-name (plist-get current-comp :name))
  738.      (or section-override
  739.          (org-bom-comp-get-section (plist-get current-comp :section)
  740.                        possible-section))
  741.      (org-bom-comp-get-qty (plist-get current-comp :qty))
  742.      (org-bom-comp-get-tag (plist-get current-comp :tag))))))))
  743.  
  744.  
  745.  
  746. (defun org-bom-check-for-details-table ()
  747.   "Scans the buffer to find \"#+TBLNAME: bom-details and add the data
  748. in `org-bom-details'. Please use the form
  749. (let ((org-bom-details (copy-tree org-bom-details))) before calling this
  750. command, otherwise `org-bom-details' will be overwritten."
  751.   (save-excursion
  752.     (save-restriction
  753.       (widen)
  754.       (goto-char (point-min))
  755.       (while (re-search-forward "#\\+TBLNAME: bom-details" nil t)
  756.     (forward-line)
  757.     (when (org-at-table-p)
  758.       (org-bom-add-details-from-table))))))
  759.  
  760.  
  761. (defun org-bom-add-details-from-table ()
  762.   "Scans the table for a special row (\"!\"), looking for \"name\"
  763. \"description\", \"price\", and \"datasheet-pdf\". Adds the data to
  764. `org-bom-details' Warning, case-sensitive!."
  765.   (org-table-get-specials) ;needed to refresh org-table-column-names
  766.   (let ((column-names (nreverse org-table-column-names))
  767.     (end (org-table-end))
  768.     (dline org-table-dataline-regexp)
  769.     (propertize '(lambda (arg) (read (concat ":" (car arg))))))
  770.     (when (assoc "name" column-names);if there isn't a component name, do nothing
  771.       (org-bom-after-header-line) (forward-line -1)
  772.       (while (re-search-forward dline end t)
  773.     (let ((temp-plist '()))
  774.       (dolist (current-column column-names)
  775.         (let ((property-value (org-bom-get-table-field
  776.                    (string-to-number (cdr current-column)))))
  777.           (when (> (length property-value) 0)
  778.         (push property-value temp-plist)
  779.         (push (funcall propertize current-column)
  780.               temp-plist))))
  781.       (org-bom-add-or-replace-in-details temp-plist))))))
  782.                  
  783.  
  784. (defun org-bom-add-or-replace-in-details (plist)
  785.   "Add or replace the plist in `org-bom-details', depending on
  786. whether it already exists."
  787.   (let ((component-details
  788.      (org-bom-get-current-component (plist-get plist :name))))
  789.     (if component-details ;if the component already exists
  790.     (let ((name-position (position ':name plist)))
  791.       (delete ;remove the name property
  792.        (nth name-position plist)
  793.        (delete ;remove the :name keyword
  794.         (nth name-position plist) plist))
  795.       (while plist
  796.         (plist-put component-details (pop plist) (pop plist))))
  797.       (push plist org-bom-details))))
  798.    
  799.  
  800. (defun org-bom-comp-get-tag (&optional column-number)
  801.   "Retrieve the component-tag in the same row and apply some filter
  802. functions."
  803.   (let (temp-tag
  804.     tag)
  805.     (dolist (col column-number)
  806.       (setq temp-tag (org-bom-get-table-field (org-table-goto-column col)))
  807.       (when (> (length temp-tag) 0) (pushnew temp-tag tag)))
  808.     (setq tag (org-bom-split-mix-tag tag)))) ; tags written as "foo-1, foo, bar," will be separated
  809.      
  810.  
  811.  
  812. (defun org-bom-comp-get-qty (&optional column-number)
  813.   "Retrieve the component-qty in the same row and apply some filter
  814. functions. If column-number is nil, default to 1."
  815.   (let ((qty 0))
  816.     (dolist (col column-number)
  817.       (setq qty (+ qty (max 0 (string-to-number
  818.                    (org-bom-get-table-field
  819.                 (org-table-goto-column col)))))))
  820.     (if column-number qty 1)))
  821.  
  822.  
  823. (defun org-bom-comp-get-name (column-number)
  824.   "Retrieve the component-name in the same row and apply some filter
  825. functions. (Remove footnotes, make \"-\" an escape character)"
  826.   (let ((comp-name
  827.      (replace-regexp-in-string "\\[fn.*\\]" ""  ;Remove any footnotes [fn*]
  828.                    (org-bom-get-table-field
  829.                     (org-table-goto-column column-number)))))
  830.     (if (string= "-" (if (> (length comp-name) 0)
  831.              (substring comp-name 0 1) ""))
  832.     (setf comp-name "")) ;if the special character '-' is present, replace by an empty string
  833.     comp-name))
  834.  
  835.  
  836. (defun org-bom-comp-get-section (&optional column-number possible-section)
  837.   "Retrieve the component-section at COLUMN-NUMBER in the same row,
  838. or the POSSIBLE-SECTION."
  839.   (or (when column-number
  840.     (let ((field (org-bom-get-table-field
  841.               (org-table-goto-column column-number))))
  842.       (if (string= "" field) nil field)))
  843.       possible-section))
  844.  
  845.        
  846. (defun org-bom-get-table-field (&optional N)
  847.   "Same as `org-table-get-field', but with some string cleaning."  
  848.   (org-trim (substring-no-properties (org-table-get-field N))))
  849.  
  850. (defun org-bom-split-mix-tag (tag &optional separator)
  851.   "Separate the tags and mix them. For example: '(\"foo, bar, foo\" \"do, ré, mi\")
  852. would give '(\"foo\" \"do\") '(\"bar\" \"\") '(\"foo\" \"mi\") with the '\", \" separator."
  853.   (let ((temp-tags tag) (new-tags nil))
  854.     (dolist (single-string-tags temp-tags) ;separate the tags into single string
  855.       (push (org-split-string single-string-tags (or separator ", ")) new-tags))
  856.     (org-bom-mix-alternate new-tags)))
  857.          
  858.  
  859. (defun org-bom-mix-alternate (list)
  860.   "Create new lists composed alternatively of an element of each list"
  861.   (let ((temp-list nil))
  862.     (push (remove nil (mapcar 'car list)) temp-list) ;the first item is composed of the first element of each list
  863.     (when (remove nil (mapcar 'cdr list)) ;while everything is not nil
  864.       (setf temp-list (append temp-list (org-bom-mix-alternate (mapcar 'cdr list)))))
  865.     temp-list))
  866.  
  867.          
  868. (defun org-bom-list-to-tsv-file (list &optional filename column)
  869.   "Export a list in a tsv file"
  870.   (save-excursion
  871.     (set-buffer (find-file-noselect (or filename "list-export.txt")))
  872.     (erase-buffer)
  873.     (let ((n-col (or column 1)))
  874.       (dolist (single-item list)
  875.     (if (> n-col 0)
  876.         (progn (setf n-col (1- n-col))
  877.            (insert single-item)
  878.            (if (> n-col 1)
  879.                (insert-tab)))
  880.       (setf n-col (1- (or column 1)))
  881.       (newline)
  882.       (insert single-item)
  883.       (if (> n-col 0)
  884.         (insert-tab)))))
  885.     (save-buffer)
  886.     (kill-buffer)))
  887.  
  888. (defun org-bom-get-all (database selector-fn)
  889.   "Return every different instance of a certain type in a single list. For
  890. example, (org-bom-get-all org-bom-database 'component-tag) will return every
  891. tag in the database. Empty strings are removed."
  892.   (let ((collector nil))
  893.     (dolist (current-component database)
  894.       (let ((current-item (funcall selector-fn current-component)))
  895.     (push current-item collector)))
  896.     (delete-dups (remove "" collector))))
  897.      
  898.  
  899. (defun org-bom-listify (list-with-lists)
  900.   "Return everything contained in the argument (lists within lists) as
  901. a plain list"
  902.   (let ((new-list nil))
  903.     (if (listp list-with-lists)
  904.     (progn (dolist (temp-item list-with-lists) ;for each item
  905.          (if (atom temp-item)
  906.              (pushnew temp-item new-list) ;if it's an atom, add it to the list
  907.            (setf new-list (append new-list (org-bom-listify temp-item))))) ;otherwise listify it
  908.            (remove nil new-list)) ; remove any remaining 'nil' from the list
  909.       (list list-with-lists))))
  910.        
  911. (defun org-bom-tag-to-list (&optional section-name remove part-match)
  912.   "Return a list of all the tags in the section, those from the same
  913. component in the same string. See `org-bom-select-in-db' for more details."
  914.   (let ((results nil)
  915.     (items (org-bom-get-all (if section-name
  916.                    (org-bom-select-in-db org-bom-database
  917.                              'component-section
  918.                              section-name
  919.                              remove
  920.                              part-match)
  921.                  org-bom-database) 'component-tag)))
  922.     (dolist (current-item items)
  923.       (dolist (current-tags current-item)
  924.     (push (funcall '(lambda (x) (org-bom-concat-list (nreverse x) " ")) current-tags) results)))
  925.     (sort (remove "" results) 'string<)))
  926.  
  927. (defun org-bom-tag-remove-to-list (section-name)
  928.   "Return a list of all the tags NOT in the section. In case of
  929.  multiple sections, add a \"+\" between."
  930.   (setf section-name (org-split-string section-name "+")) ; convert the section-name in a list of string, so the user don't have to enter it as one
  931.   (let ((list nil)
  932.     (temp-tag nil)
  933.     (component-db org-bom-database))
  934.     (dolist (current-section-name section-name); For every section-name
  935.       (setf component-db (org-bom-select-in-db
  936.               component-db
  937.               'component-section
  938.               current-section-name
  939.               'remove
  940.               'part-match))) ;remove any partly matching section-name
  941.     (dolist (current-component component-db) ;; Put every tag in a list
  942.       (dolist (single-component-tags (component-tag current-component))
  943.     (push (org-bom-concat-list (org-bom-listify single-component-tags) " ") list)))
  944.     (setf list (remove "" (sort list 'string<)))))
  945.  
  946. (defun org-bom-concat-list (list &optional separator)
  947.   "Concatenate in a single string every string in the list with an
  948.  optional separator, such as \" \"."
  949.   (concat (car list) (unless (atom (cdr list))
  950.                (concat separator (org-bom-concat-list (cdr list) separator)))))
  951.  
  952. (defun org-dblock-write:bom (params)
  953.   "Insert a table with every component gathered in the buffer.
  954. See `org-bom-insert-table' for more details."
  955.   (let ((org-bom-details (copy-tree org-bom-details)))
  956.     (when org-bom-update-enable
  957.       (org-bom-check-for-details-table)
  958.       (org-bom-total)); Scan the buffer and refresh the bill of materials
  959.     (org-bom-insert-table params)
  960.   (message "Bill of materials created")))
  961.  
  962. (defun org-bom-stringify (&optional argument)
  963.   "If ARGUMENT is string, returns unchanged. If it's a symbol,
  964. returns the symbol-name. If nil, return nil"
  965.   (if (null argument)
  966.       nil
  967.     (cond ((not (stringp argument)) (symbol-name argument))
  968.       ((not (null argument)) argument))))
  969.  
  970.  
  971. (defun org-bom-insert-table (params)
  972.   "Insert a table with every component gathered in the buffer.
  973.  
  974. Set \":local-only\" to get components marked with the current heading
  975. as their \"section\". Components with given section (either in a table
  976. or a property) will NOT appear.
  977.  
  978. Set \":section\" to get a specified section only. Note that if a
  979. section is given to a component, it won't appear in a local-only
  980. table. In addition, set \"part-match\" to get partly matching
  981. sections. In addition, a + sign will add an additionnal section. For
  982. example: \":section A+B\" will retrieve section A and section B.
  983.  
  984. Set \":remove\" to remove the specified section and keep everything
  985. else.
  986.  
  987. Set \":total\" to merge every section together and obtain a grand
  988. total.
  989.  
  990. Set \":no-tag\" to remove the tags column.
  991.  
  992. Set \":description\" to insert a description column. You must have a
  993. PLIST with \":name\" and \":description\" in it. The function will
  994. search for a matching component's name and get its description. Copy
  995. your property list to the variable `org-bom-details'.
  996.  
  997. Set \":org-bom-latex-max-tags\" to hide every remaining tags in a
  998. pdf comment (need org-bom-latex-mode activated)
  999.  
  1000. Set \":price\" to insert a price column. You must have a
  1001. PLIST with \":name\" and \":price\" in it. The function will
  1002. search for a matching component's name and get its price. Copy
  1003. your property list to the variable `org-bom-details'.
  1004.  
  1005. The columns' name can be set with :col-name-tag, :col-name-component,
  1006. :col-name-section, :col-name-quantity, col-name-price and
  1007. col-name-description.
  1008.  
  1009. See `org-bom-prepare-tabledata-for-database' for more information."
  1010.   (unless (if (and (plist-get params :local-only) (plist-get params :section))
  1011.           (error "Specify a section OR local-only, not both"))
  1012.  
  1013. ;; Check options given by the user
  1014.     (let ((heading-list '())
  1015.       (table-list '())
  1016.       (local-only (plist-get params :local-only))
  1017.       (section-name
  1018.        (org-bom-stringify (plist-get params :section)))
  1019.       (grand-total (plist-get params :total))
  1020.       (col-name-section
  1021.        (or (org-bom-stringify (plist-get params :col-name-section))
  1022.            "Section"))
  1023.       (col-name-price
  1024.        (or (org-bom-stringify (plist-get params :col-name-price))
  1025.            "Price"))
  1026.       (col-name-quantity
  1027.        (or (org-bom-stringify (plist-get params :col-name-quantity))
  1028.            "Quantity"))
  1029.       (col-name-tag
  1030.        (or (org-bom-stringify (plist-get params :col-name-tag))
  1031.            "Tag"))
  1032.       (col-name-component
  1033.        (or (org-bom-stringify (plist-get params :col-name-component))
  1034.            "Component"))
  1035.       (col-name-description
  1036.        (or (org-bom-stringify (plist-get params :col-name-description))
  1037.            "Description"))
  1038.       (insert-col-section
  1039.        (not (or (plist-get params :total)
  1040.             (plist-get params :local-only)
  1041.             (plist-get params :section)))) ; No use to put a section column if it's given local or given by the user
  1042.       (insert-col-description
  1043.        (if (plist-get params :description) t nil)) ; Activate if the user want to use it
  1044.       (insert-col-price
  1045.        (if (plist-get params :price) t nil)) ; Activate if the user want to use it
  1046.       (insert-col-tag
  1047.        (if (plist-get params :no-tag) nil t)) ; Default ON, must be turned off by the user
  1048.       (insert-col-component t) ; Always true, for now
  1049.       (insert-col-quantity t) ; Always true, for now
  1050.       (remove-mark
  1051.        (if (plist-get params :remove) t nil)) ; indicate if we should remove rather than keep
  1052.       (part-match
  1053.        (if (plist-get params :part-match) t nil)); If 't', a string-match will be used to select the section
  1054.       (current-heading (if (org-before-first-heading-p)
  1055.                    (format "") ; If we are before the first heading, then simply default to "".
  1056.                  (org-get-heading t t))))
  1057. ;; End of user options
  1058.  
  1059. ;; select what is needed in the database
  1060.         (when section-name
  1061.     (setf section-name (org-split-string section-name "+"))) ; convert the section-name in a list of string, so the user don't have to enter it as one
  1062.       (let ((temp-section-name)
  1063.         (temp-db (org-bom-select-in-db
  1064.               org-bom-database
  1065.               'component-name
  1066.               ""
  1067.               'remove!))) ;Remove any blank names
  1068.     (when (setf section-name
  1069.             (or section-name
  1070.             (if local-only (list current-heading))))
  1071.       (setf temp-db (org-bom-totalize ;if a section is defined, then keep only the database's relevant part
  1072.              (org-bom-select-in-db temp-db
  1073.                            'component-section
  1074.                            section-name
  1075.                            remove-mark
  1076.                            part-match))))
  1077.     (if grand-total (setf temp-db (org-bom-totalize temp-db))) ; fuse all sections and get the total
  1078.    
  1079. ;; Now construct the orgtbl-lisp
  1080.       ;;heading
  1081.     (when insert-col-description
  1082.       (push col-name-description heading-list))
  1083.     (when insert-col-price
  1084.       (push col-name-price heading-list))
  1085.     (when insert-col-quantity
  1086.       (push col-name-quantity heading-list))
  1087.     (when insert-col-component
  1088.       (push col-name-component heading-list))
  1089.     (when insert-col-tag
  1090.       (push col-name-tag heading-list))
  1091.     (when insert-col-section
  1092.       (push col-name-section heading-list))
  1093.    
  1094.     ;;add a separator line to the table
  1095.     (push 'hline table-list)
  1096.    
  1097.     ;;now add the heading to the table
  1098.     (push heading-list table-list)
  1099.    
  1100.     ;;The body of the table
  1101.     (setq table-list
  1102.           (append table-list
  1103.               (nreverse (org-bom-to-lisp-table
  1104.                  temp-db
  1105.                  insert-col-section
  1106.                  insert-col-tag
  1107.                  insert-col-price
  1108.                  insert-col-description))))
  1109.    
  1110.     ;;if there's a price, add a total line
  1111.     (when insert-col-price
  1112.       (setq table-list (nreverse table-list))
  1113.       (push 'hline table-list)
  1114.       (push (append '("TOTAL:")
  1115.             (make-list (1- (length heading-list)) "")) table-list)
  1116.       (setq table-list (nreverse table-list)))
  1117.    
  1118.    
  1119.     (insert (orgtbl-to-orgtbl table-list
  1120.                   (list
  1121.                    :remove-newlines t
  1122.                    :tstart nil :tend nil
  1123.                    :hline "|---"
  1124.                    :sep " | "
  1125.                    :lstart "| "
  1126.                    :lend " |")))
  1127.     (org-table-align)
  1128.     (when insert-col-price
  1129.       (org-table-store-formulas
  1130.        (list (cons (concat "@>$"
  1131.                    (number-to-string (1+ (position col-name-price heading-list))))
  1132.                "vsum(@I..@>>)")))
  1133.       (org-table-iterate))))))
  1134.  
  1135.  
  1136.  
  1137.  
  1138. (defun org-bom-to-lisp-table (database &optional section tag price description)
  1139.   "Returns an orgtbl compliant table from an org-bom DATABASE.
  1140. See `org-bom-to-lisp-table-row' for more details."
  1141.   (let ((table '())) ;an empty list
  1142.   (dolist (current-component database)
  1143.     (push (org-bom-to-lisp-table-row current-component
  1144.                      section
  1145.                      tag
  1146.                      price
  1147.                      description)
  1148.       table))
  1149.   table))
  1150.  
  1151.  
  1152.  
  1153. (defun org-bom-to-lisp-table-row (component &optional section tag price description)
  1154.   "Returns an orgtbl compliant row for a given COMPONENT
  1155. from the org-bom-database."
  1156.   (let ((list '())
  1157.     (tags (component-tag component))
  1158.     (name (component-name component))
  1159.     (quantity (component-quantity component)))
  1160.     (when section
  1161.       (push (component-section component) list))
  1162.     (when tag
  1163.       (push (org-bom-to-lisp-table-tags (component-tag component)) list))
  1164.     (when component
  1165.       (push (org-bom-to-lisp-table-name (component-name component)) list)
  1166.       (push (number-to-string (component-quantity component)) list))
  1167.     (when price
  1168.       (let ((current-price
  1169.           (plist-get (org-bom-get-current-component name) :price)))
  1170.     (push (if current-price
  1171.           (number-to-string (* quantity  
  1172.                        (string-to-number current-price)))
  1173.         "" )
  1174.           list)))
  1175.     (when description
  1176.       (push (or (plist-get
  1177.              (org-bom-get-current-component name) :description)
  1178.             "N/A" )
  1179.         list))
  1180.     (setq list (nreverse list))
  1181.     list))
  1182.      
  1183.  
  1184.  
  1185. (defun org-bom-to-lisp-table-name (name)
  1186.   "Check if `org-bom-latex-mode' is non-nil, if the datasheet exists
  1187. and add the necessary LaTeX command."
  1188.   (let ((temp-datasheet
  1189.      (plist-get (org-bom-get-current-component name) :datasheet-pdf)))
  1190.     (if (and (> (length temp-datasheet) 1) org-bom-latex-mode)
  1191.     (concat "\\hyperref["temp-datasheet"]{"name"}")
  1192.       name)))
  1193.  
  1194.  
  1195.  
  1196. (defun org-bom-to-lisp-table-tags (tags)
  1197.   "Takes the initial tags list form org-bom-database and
  1198. convert it in a single string. If `org-bom-latex-mode' is
  1199. non-nil, and if the number of tags is greater than
  1200. `org-bom-latex-max-tags', a latex command to add a pdf comment
  1201. is inserted."
  1202.   (let ((temp-tag (sort (delete-dups (org-bom-listify tags)) 'string<))
  1203.               (single-tag nil)
  1204.               (max-tags-activated? nil))
  1205.     (with-temp-buffer ;easier than trying to concat everything
  1206.       (when (and org-bom-latex-mode
  1207.          (numberp org-bom-latex-max-tags)
  1208.          (> (length temp-tag) org-bom-latex-max-tags))
  1209.     (insert "\\pdfcomment[color=Ivory,subject={Tags},icon=Note,open=true,hoffset=-1cm]{")
  1210.     (setq max-tags-activated? t))
  1211.       (while (> (length temp-tag) 0)
  1212.     (when (stringp (setf single-tag (pop temp-tag)))
  1213.       (insert single-tag)            
  1214.       (if (> (length temp-tag) 0)
  1215.           (insert ", "))); Insert a white space between the tags
  1216.     (when (and org-bom-latex-mode (numberp org-bom-latex-max-tags)
  1217.            (= (- (length temp-tag) org-bom-latex-max-tags) 0))
  1218.       (delete-char -2) ;delete the last comma in the PDF comment
  1219.       (insert "}{")))
  1220.       (if max-tags-activated? (insert "}..."))
  1221.       (replace-regexp-in-string "[\\]" "\\\\" (buffer-string)))))
  1222.  
  1223.  
  1224.  
  1225. (defun org-bom-totalize (database)
  1226.   "Will ignore the sections and return a new database with a true
  1227. total for each component."
  1228.   (let ((new-database nil))
  1229.     (dolist (current-item-old-db database) ;scan the given database
  1230.       (let ((exists-flag nil)) ; exist flag as nil
  1231.     (dolist (current-item-new-db new-database) ;scan the 'new' database
  1232.       (when (gnus-string-equal
  1233.          (component-name current-item-new-db)
  1234.          (component-name current-item-old-db)) ;when the same name is found
  1235.         (setf (component-quantity current-item-new-db)
  1236.           (+ (component-quantity current-item-new-db)
  1237.              (component-quantity current-item-old-db))) ;simply add the quantity...
  1238.         (push (component-tag current-item-old-db)
  1239.           (component-tag current-item-new-db)) ; add the tags...
  1240.         (setf exists-flag t))) ; and set the flag as t
  1241.     (if (not exists-flag) (push (make-component
  1242.                      :name (component-name current-item-old-db) ;otherwise create a new entry with the same component name as the old database
  1243.                      :section "total" ;give a dummy name - should never really be used
  1244.                      :quantity (component-quantity current-item-old-db) ; take the old quantity
  1245.                      :tag (component-tag current-item-old-db)) new-database)))); finally take the old tags
  1246.     (nreverse new-database)));reverse so it will be in the same order as before
  1247.      
  1248.  
  1249.  
  1250. (defun org-bom-get-current-component (name)
  1251.   "Return the current component from the `org-bom-details' plist."
  1252.    (car (remove-if-not
  1253.     #'(lambda (component)
  1254.     (equal (plist-get component :name) name))
  1255.     org-bom-details)))
  1256.  
  1257. (defun org-bom-get-from-database (database selector value)
  1258.   "Return every entry in the database which has the corresponding
  1259. value for a given selector. Can be the database's argument of itself
  1260. in case of multiple selectors"
  1261.   (setf value (gnus-replace-in-string value "+" "[+]")) ;In a regexp, has a meaning and isn't considered a "string"
  1262.   (remove-if-not
  1263.    '(lambda (comp)
  1264.       (string-match value (plist-get comp selector))) database))
  1265.  
  1266. (defun org-dblock-write:bom-datasheet (params)
  1267.   "This is used to add used components datasheet (for LaTeX only).
  1268. For more details, see `org-bom-insert-datasheet-table'."
  1269.   (let ((org-bom-details (copy-tree org-bom-details)))
  1270.     (when org-bom-update-enable
  1271.       (org-bom-check-for-details-table)
  1272.       (org-bom-total)); Scan the buffer and refresh the bill of materials
  1273.   (org-bom-insert-datasheet-table params)))
  1274.  
  1275.  
  1276.  
  1277. ;;Horrible function from the time I was learning to code in elisp.
  1278. ;;I haven't found the courage to re-write it yet.
  1279. (defun org-bom-insert-datasheet-table (params)
  1280.   "This is used to add used components datasheet (for LaTeX only). The
  1281. filename will be taken in the org-bom-details plist, with the
  1282. property :datasheet. A latex command such as \"#+LATEX_HEADER:
  1283. \newcommand{\DATASHEETPATH}{Name-of-the-folder/}\" shall be inserted
  1284. at the beginning of the org document, where Name-of-the-folder is
  1285. the folder where the datasheets files are. Note that the entire
  1286. filename must be in the plist; \"datasheet.pdf\". Set
  1287. \":description\" to enable a summary of components before the
  1288. datasheets. As for the BOM dynamic block, the columns names can be
  1289. changed with \":col-name-component\" and \":col-name-description\"."
  1290.   (save-excursion
  1291.     (save-restriction
  1292.       (widen)
  1293.       (let ((temp-database (org-bom-select-in-db org-bom-database 'component-name "" 'remove!))
  1294.         (all-component-names nil)
  1295.         (component-names-for-toc)
  1296.         (all-component-datasheet nil)
  1297.         (temp-filename nil)
  1298.         (temp-name nil))
  1299.     (while temp-database
  1300.       (add-to-list 'all-component-names (component-name (pop temp-database)))); Gather every component used
  1301.     (setf all-component-names (sort all-component-names 'gnus-string<))
  1302.     (dolist (temp-component-name all-component-names )
  1303.       (let ((current-datasheet (plist-get (org-bom-get-current-component temp-component-name) :datasheet-pdf)))
  1304.         (if (> (length current-datasheet) 1)
  1305.         (or (member current-datasheet all-component-datasheet)
  1306.             (progn (setq all-component-datasheet (cons current-datasheet all-component-datasheet))
  1307.                (setq component-names-for-toc (cons temp-component-name component-names-for-toc)))))))
  1308.     (setf all-component-datasheet (nreverse all-component-datasheet))
  1309.     (setf component-names-for-toc (nreverse component-names-for-toc))
  1310.     (if (plist-get params :description)
  1311.         (progn (let ((col-name-component (or (if (plist-get params :col-name-component)
  1312.                              (symbol-name (plist-get params :col-name-component))) "Component"))
  1313.              (col-name-description (or (if (plist-get params :col-name-description)
  1314.                                (symbol-name (plist-get params :col-name-description))) "Description"))
  1315.              (temp-all-component-names all-component-names)
  1316.              (temp-all-component-filename all-component-datasheet))
  1317.              (org-table-create (concat (int-to-string 2)"x" (int-to-string (+ 1 (length all-component-names))))) ;create a table
  1318.              (org-table-goto-column 1)
  1319.              (insert col-name-component)
  1320.              (org-table-goto-column 2)
  1321.              (insert col-name-description)
  1322.              (while temp-all-component-names
  1323.                (setf temp-name (pop temp-all-component-names))
  1324.                (org-table-goto-line (+ (org-table-current-line) 1))
  1325.                (org-table-goto-column 1)
  1326.                (if (> (length (setf temp-filename (plist-get (org-bom-get-current-component temp-name) :datasheet-pdf))) 1)
  1327.                (insert (concat "\\hyperref["temp-filename"]{"temp-name"}"))
  1328.              (insert temp-name))
  1329.                (org-table-goto-column 2)
  1330.                (insert (or (plist-get (org-bom-get-current-component temp-name) :description) "N/A"))))
  1331.            (org-table-align)
  1332.            (end-of-line)
  1333.            (newline)))
  1334.     (newline)
  1335.     (setf all-component-datasheet all-component-datasheet)
  1336.     (while all-component-datasheet ; for every datasheet
  1337.       (if (> (length (setf temp-filename-and-component (pop all-component-datasheet))) 1) ; get the filename associated with the component's name
  1338.           (progn(insert (concat "#+LaTeX: " "\\includepdf[pages=-,landscape=true,addtotoc={1, subsection, 1, " (pop component-names-for-toc) ","temp-filename-and-component"}]{\\DATASHEETPATH/" temp-filename-and-component "}"))
  1339.             (newline))
  1340.         (message (concat "#### " temp-name " doesn't have a datasheet... moving on."))))))))
  1341.  
  1342.  
  1343. (provide 'org-bom)
  1344.  
  1345. ;========================================
  1346. ; The program ends here
  1347. ;========================================
  1348.  
  1349.  
RAW Paste Data