跳到主要内容

发票金额的四舍五入处理

發票金額的四捨五入

文章来源: https://hackmd.io/@Lin301046/Sye2ylCKY

由於台灣的發票金額,稅費等都是整數,而 ERPNext 的金額預設都會有小數點,因此需要透過一些設定來讓 ERPNext 在計算時將金額與稅費時自行四捨五入,以符合台灣的使用習慣。

1. 設定 Regional

ERPNext 會根據當前的區域,若為特地區域會有不一樣的金額計算方式(ERPNext 的 Regionl 設定),因此我們可以按照 ERPNext 的做法,在 hooks.py 中新增一個 Taiwan,並且設定要 Override 的 method,如下:

# 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。

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,這個表達方式在某些數字的進位時,可能就會出現進位錯誤的情況 例如:

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,因此為了解決這個問題,建議以下的方式去處理:

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 的使用方法:

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 下的頁面

在自定義的模組下,可以按照下圖的方式去放 img

這樣子就可以透過 http://127.0.0.1/sales_invoic 這個 URL 進入 index.html,而在進入 index.html 前,Frappe 會先執行 index.py,該 index.py 中會需要一個叫做 get_context(context) 的 method,如下:

# index.py
def get_context(context):
context.test = 'Hello World'
<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 進行資料前處理,解決方法如下:

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)
}
}