--- title: ERPNext 開發雜記 --- # ERPNext 開發雜記 不知道怎麼分類的紀錄都會丟到這邊來 ## 發票金額的四捨五入 由於台灣的發票金額,稅費等都是整數,而 ERPNext 的金額預設都會有小數點,因此需要透過一些設定來讓 ERPNext 在計算時將金額與稅費時自行四捨五入,以符合台灣的使用習慣。 #### 1. 設定 Regional ERPNext 會根據當前的區域,若為特地區域會有不一樣的金額計算方式([ERPNext 的 Regionl 設定](https://github.com/frappe/erpnext/blob/version-13/erpnext/hooks.py#L447)),因此我們可以按照 ERPNext 的做法,在 hooks.py 中新增一個 Taiwan,並且設定要 Override 的 method,如下: ```python= # hooks.py regional_overrides = { 'Taiwan': { 'erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts': 'my_app.utils.get_regional_round_off_accounts' } } # my_app.utils.get_regional_round_off_accounts def get_regional_round_off_accounts(company, account_list): frappe.flags.round_off_applicable_accounts = ["2161 - 應付所得稅 - Company", "VAT - Company"] return frappe.flags.round_off_applicable_accounts ``` get_regional_round_off_accounts 中,若 accounts 的 name 存在於 frappe.flags.round_off_applicable_accounts 中,金額便會四捨五入,因此可以將需要四捨五入的 accounts 放入 round_off_applicable_accounts,如此一來在計算特定 accounts 的金額時,便都會四捨五入了 #### 2. 設定 Precision 若將 doctype 中的指定欄位精度設為 0,例如 total、amount... 也會有一樣的效果。 這部分可以利用 patch 的方式,並且透過 bench migrate 去處理,便不用每換一個環境都需要特地去執行一個 SQL。 ```python= frappe.db.sql(f""" UPDATE `tabDocField` SET `precision` = 0 WHERE fieldname IN ('amount', 'base_amount', 'total', 'base_total', 'grand_total', 'base_grand_total', 'net_total', 'base_net_total', 'taxes_and_charges', 'base_taxes_and_charges', 'total_taxes_and_charges') AND (parent like 'Purchase%' or parent like 'Delivery%' or parent like 'Sales%' or parent like 'Advance%') AND fieldtype = 'Currency'; """) ``` #### 3. 新增欄位存放自行計算的金額與稅費 雖然透過前兩個方法,可以讓金額與稅費都四捨五入,但是上述兩個方法在未稅額的計算並不會起作用,因為 ERPNext 的未稅額是使用含稅額去回推的,勢必一定會出現小數點,而前面有提到,台灣的金額與稅費基本上都是整數,我們的客戶對於這部分也有特別要求一定要整數,因此在產出報表時,此情況就很容易因為四捨五入的關係而出現些微的誤差,因此為了再不影響 ERPNext 運作的前提下處理這個問題,我們選擇自行新增欄位,並且利用 Frappe 的 DocEvent,當發票新增前(before_insert)或更新前(before_save)時,先依照我們的需求去計算未稅額與稅額,之後的報表如果有需要抓未稅額的資訊時,都抓這個我們自行新增的欄位,如此便能再不影響系統運作的前提下處理好這個問題。 ## Python 的小數點進位 由於浮點數在電腦中是用非常近似的一個數字去表達,例如 1.5 可能會表達為 1.499999999,這個表達方式在某些數字的進位時,可能就會出現進位錯誤的情況 例如: ```python= round(8.125, 2) # 8.12 round(8.135, 2) # 8.13 ``` 8.125 四捨五入到小數點第二位應該要是 8.13,8.135 則應該要是 8.14,但是 Python 內建的 round,卻分別會是 8.12 與 8.13,因此為了解決這個問題,建議以下的方式去處理: ```python= from frappe.utils.data import flt flt(8.125, 2) # 8.13 flt(8.135, 2) # 8.14 ``` 使用 Frappe 內建的 flt 去處理進位,便不會發生這種錯誤了。 ## Cache 的應用 Frappe 已經有內建的 cache 能用,該 cache 是基於 Redis 所建立的而成的,在產生一些大型的報表時,可以將某些結果先存放於 Cache 中,以加快執行的速度。 Frappe Cache 的使用方法: ```python= import frappe cache = frappe.cache() # 設定一個快取 快取的 Key 為 Cache Key , 值為 Cache Value # 3600 秒後過期 cache.set_value("Cache Key", "Cache Value", expires_in_sec=3600) # 回傳 Cache Key 的 Value 並且不會將該值存在 frappe.local 中 # 若找不到 Cache 則回傳 None cache.get_value(cache_key, expires=True) ``` ## www 下的頁面 在自定義的模組下,可以按照下圖的方式去放 ![](https://i.imgur.com/lkz00oB.png) 這樣子就可以透過 http://127.0.0.1/sales_invoic 這個 URL 進入 index.html,而在進入 index.html 前,Frappe 會先執行 index.py,該 index.py 中會需要一個叫做 get_context(context) 的 method,如下: ```python= # index.py def get_context(context): context.test = 'Hello World' ``` ```html= <html> <body> <!-- context 中所設定的 Hello World 會被渲染到畫面上 --> {{ test }} </body> </html> ``` 這個 context 會被傳到 index.html 中,並且可利用 jinja 的語法來進行使用,因此可以依照需求將需要的資料放入 context 以呈現不一樣的資訊。 ### No Module Named bz2 安裝 frappe docker 時,有可能會因為一開始編譯的 python 少了 libbz2-dev 的關係,使得 import pandas 時,會發生 No Module Named bz2 的錯誤,目前實測過後主要解決方式為安裝 libbz2-dev 後重新 build Python 即可解決。 由於 Frappe Docker 中有內建 pyenv,因此可以參考此網站的做法進行 (https://realpython.com/intro-to-pyenv/#build-dependencies) #### Step1. ``` sudo apt-get install -y make build-essential libssl-dev zlib1g-dev \ libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev \ libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev ``` #### Step2. ``` pyenv install -v <Python Version> ``` 重新安裝完之後,在安裝一次 pandas 並且 import,問題基本上就可以解決了。 ## Form Script 沒有類似 hooks.py 中的 on_update_after_submit 的事件 由於 doc 在提交後,若要更新資料,frappe 的 form script 並沒有像後端那樣有 on_update_after_submit 的事件可以呼叫,因此無法對提交後的表單在更新前使用 JS 進行資料前處理,解決方法如下: ```javascript= validate_and_save(save_action, callback, btn, on_error, resolve, reject) { var me = this; if(!save_action) save_action = "Save"; this.validate_form_action(save_action, resolve); var after_save = function(r) { // to remove hash from URL to avoid scroll after save history.replaceState(null, null, ' '); if(!r.exc) { if (["Save", "Update", "Amend"].indexOf(save_action)!==-1) { frappe.utils.play_sound("click"); } me.script_manager.trigger("after_save"); if (frappe.route_hooks.after_save) { let route_callback = frappe.route_hooks.after_save; delete frappe.route_hooks.after_save; route_callback(me); } // submit comment if entered if (me.comment_box) { me.comment_box.submit(); } me.refresh(); } else { if(on_error) { on_error(); reject(); } } callback && callback(r); resolve(); }; var fail = (e) => { if (e) { console.error(e) } btn && $(btn).prop("disabled", false); if(on_error) { on_error(); reject(); } }; if(save_action != "Update") { // validate frappe.validated = true; frappe.run_serially([ () => this.script_manager.trigger("validate"), () => this.script_manager.trigger("before_save"), () => { if(!frappe.validated) { fail(); return; } frappe.ui.form.save(me, save_action, after_save, btn); } ]).catch(fail); } else { // 修改此處的原始碼 // https://github.com/frappe/frappe/blob/version-13/frappe/public/js/frappe/form/form.js#L689 frappe.run_serially([ () => this.script_manager.trigger("on_update_after_submit"), () => frappe.ui.form.save(me, save_action, after_save, btn) ]).catch(fail) } } ```