diff --git a/CHANGELOG.md b/CHANGELOG.md index 68c391e..91d9d82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.6.0 + +* Add a parser option `options.price_formula.enabled` in definition to be set to `True` for allowing to set price as formula. The formula starts with `=` and follow with valid Python code. For eg. `prestations.0.price: =1000*0.7` will yield a price of 700 on the first prestation. + # 0.5.0 * Add a `rounding-decimals` kwarg to `parse_invoices` and `parse_quote`, which default to `2` diff --git a/setup.py b/setup.py index d39c196..8bff537 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ def package_files(directory): setup( name="tqwgp-parser", - version="0.5.0", + version="0.6.0", url="https://github.com/YtoTech/talk-quote-work-getpaid-parser", license="AGPL-3.0", author="Yoan Tournade", diff --git a/tests/test_definitions_parsing.py b/tests/test_definitions_parsing.py index 511ab5e..62a8c2e 100644 --- a/tests/test_definitions_parsing.py +++ b/tests/test_definitions_parsing.py @@ -271,6 +271,25 @@ def test_parse_simple_quote_rounding(): assert quote["price"]["vat"] == 7465.87 assert quote["price"]["total_vat_incl"] == 44795.2 +def test_parse_simple_quote_rounding(): + """ + Prestation prices can include formulas. (opt-in) + """ + definition = copy.deepcopy(TESLA_16_01_QUOTE) + definition["options"] = { + **definition.get("options", {}), + "price_formula": { + "enabled": True + } + } + definition["prestations"][0]["price"] = "=1000*0.3" + quote = parse_quote(definition) + checkQuote(quote) + assert len(quote["prestations"]) == 4 + assert quote["prestations"][0]["total"] == 300 + assert quote["prestations"][0]["price"] == 300 + assert quote["price"]["total_vat_excl"] == 35300.0 + def test_parse_simple_quote_no_optional_tva(): """ diff --git a/tqwgp_parser/parser.hy b/tqwgp_parser/parser.hy index 11ebb40..cdfd5a2 100644 --- a/tqwgp_parser/parser.hy +++ b/tqwgp_parser/parser.hy @@ -27,17 +27,31 @@ (get (parse-all-prestations (get prestation "prestations") vat-rate options prestation) 0)) (.append sections (parse-section prestation section-prestations vat-rate options)) (.extend all-prestations section-prestations)) - (.append all-prestations (parse-prestation prestation section)) + (.append all-prestations (parse-prestation prestation section options)) )) prestation)) (, all-prestations sections)) -(defn parse-prestation [prestation section] +(defn apply-any-price-formula [price price-formula] + (if (and + (get-in price-formula ["enabled"] False) + (string? price) + (= (get price 0) "=")) + (eval (cut price 1 None)) + price)) + +(defn parse-prestation [prestation section options] (merge-dicts [ (parse-dict-values prestation ["title"] ["price" "quantity" "description" "batch" "optional"]) { - "total" (compute-price [prestation] :count-optional True) + "price" (apply-any-price-formula + (get-default prestation "price" None) + (get options "price_formula")) + "total" (compute-price [prestation] + :count-optional True + :rounding-decimals (get options "rounding-decimals") + :price-formula (get options "price_formula")) "quantity" (get-default prestation "quantity" 1) "section" (get-default (if (none? section) {} section) "title" None) "batch" (parse-batch (get-default prestation "batch" (get-default (if (none? section) {} section) "batch" None))) @@ -54,18 +68,23 @@ "price" (compute-price-vat prestations :count-optional False :vat-rate vat-rate - :rounding-decimals (get options "rounding-decimals")) + :rounding-decimals (get options "rounding-decimals") + :price-formula (get options "price_formula")) "optional_price" (compute-price-vat prestations :count-optional True :vat-rate vat-rate - :rounding-decimals (get options "rounding-decimals")) + :rounding-decimals (get options "rounding-decimals") + :price-formula (get options "price_formula")) ;; TODO Normalize batch here: only set if all section prestation has same batch ;; (alternative: set a list of batches). "batch" (parse-batch (get-default section "batch" None)) "optional" (get-default section "optional" False) }])) -(defn compute-price [prestations [count-optional False] [rounding-decimals None]] +(defn compute-price + [prestations + [count-optional False] [rounding-decimals None] + [price-formula {}]] """ Parse price of a flattened list of prestations (actually any list with object containing a price property), @@ -76,7 +95,7 @@ (rounded-number (reduce (fn [total prestation] - (setv price (get prestation "price")) + (setv price (apply-any-price-formula (get prestation "price") price-formula)) (setv add-price (and (numeric? price) (or count-optional (not (get-default prestation "optional" False))))) (setv prestation-total (if (numeric? price) (* price (get-default prestation "quantity" 1)))) (cond @@ -99,7 +118,10 @@ (simplest-numerical-format (* (/ vat-rate 100) price)) rounding-decimals)) -(defn compute-price-vat [prestations [count-optional False] [vat-rate None] [rounding-decimals None]] +(defn compute-price-vat + [prestations + [count-optional False] [vat-rate None] + [rounding-decimals None] [price-formula {}]] """ Compute price, as an object including VAT component, total with VAT excluded, total with VAT included ; from a list of objects containing a price (numerical) property. @@ -107,7 +129,8 @@ ;; TODO Handle price object in element list, taking total_vat_excl for the summation? (setv total-vat-excl (compute-price prestations :count-optional count-optional - :rounding-decimals rounding-decimals)) + :rounding-decimals rounding-decimals + :price-formula price-formula)) (if (numeric? vat-rate) (do (setv vat (if (none? total-vat-excl) None (compute-vat total-vat-excl vat-rate @@ -209,6 +232,11 @@ "logo" (parse-logo (get-default sect "logo" None)) }])) +(defn parse-parser-options [definition] + {"price_formula" { + "enabled" (get-in definition ["options" "price_formula" "enabled"] False) + }}) + (defn parse-quote [definition #** kwargs] """ Parse and normalize a quote definition. @@ -218,6 +246,7 @@ "rounding-decimals" 2 } kwargs + (parse-parser-options definition) ])) (setv vat-rate (get-default definition "vat_rate" None)) (setv (, all-prestations sections) @@ -235,7 +264,8 @@ "vat_rate" vat_rate "price" (compute-price-vat all-prestations :vat-rate vat-rate - :rounding-decimals (get options "rounding-decimals")) + :rounding-decimals (get options "rounding-decimals") + :price-formula (get options "price_formula")) ;; Derive sections from all-prestations (and sections too). "batches" (recompose-batches all-prestations) "all_prestations" all-prestations @@ -248,7 +278,8 @@ "optional_price" (compute-price-vat all-optional-prestations :count-optional True :vat-rate vat-rate - :rounding-decimals (get options "rounding-decimals")) + :rounding-decimals (get options "rounding-decimals") + :price-formula (get options "price_formula")) "display_project_reference" (none-or-true? (get-default definition "display_project_reference" True)) }])) @@ -259,9 +290,13 @@ ["description" "quantity"]) { "quantity" (get-default line "quantity" 1) + "price" (apply-any-price-formula + (get-default line "price" None) + (get options "price_formula")) "total" (compute-price [line] :count-optional True - :rounding-decimals (get options "rounding-decimals")) + :rounding-decimals (get options "rounding-decimals") + :price-formula (get options "price_formula")) } ])) @@ -290,7 +325,8 @@ "vat_rate" (get merged-invoice "vat_rate") "price" (compute-price-vat lines :vat-rate (get merged-invoice "vat_rate") - :rounding-decimals (get options "rounding-decimals")) + :rounding-decimals (get options "rounding-decimals") + :price-formula (get options "price_formula")) "display_project_reference" (none-or-true? (get-default merged-invoice "display_project_reference" True)) }])) @@ -303,6 +339,7 @@ "rounding-decimals" 2 } kwargs + (parse-parser-options definition) ])) (defn parse-invoice-closure [invoice] (parse-invoice invoice definition options)) diff --git a/tqwgp_parser/utils.hy b/tqwgp_parser/utils.hy index 852d9b9..ac78b9f 100644 --- a/tqwgp_parser/utils.hy +++ b/tqwgp_parser/utils.hy @@ -9,6 +9,9 @@ (defn numeric? [v] (isinstance v (, int float))) +(defn string? [v] + (isinstance v (, str bytes))) + (defn none? [v] (= v None)) @@ -68,6 +71,18 @@ (get value key) default)) +(defn get-in [value key-path default] + (setv current-key (if (= (len key-path) 0) None (get key-path 0))) + (if (none? current-key) + default + (if (in current-key value) + (if (= (len key-path) 1) + (get value current-key) + (get-in + (get value current-key) + (cut key-path 1 None) + default)) + default))) ; Lists.