人财事物信息化 - exchange_rate_revaluation.py
该网页是erpnext项目中exchange_rate_revaluation.py
文件的代码内容,用于实现汇率重估功能,核心要点如下:
类定义
ExchangeRateRevaluation
类:继承自Document
,包含公司、过账日期、汇兑损益科目、舍入损失允许值等属性,以及关联的accounts
表格(记录各账户重估数据)。
主要方法
- 验证与计算
validate
:调用validate_rounding_loss_allowance
(校验舍入损失允许值需在0-1之间)和set_total_gain_loss
(计算总汇兑损益,区分已过账和未过账损益)。set_total_gain_loss
:遍历账户数据,根据新旧本位币余额差异计算单个账户损益,汇总为总损益。
通过以上修改,系统将根据公司所在国家自动调用本地化汇率源,提升国际化场景下的准确性。实际部署需根据目标网站的具体结构调整代码。
- 提交前处理
before_submit
:调用remove_accounts_without_gain_loss
,过滤掉无汇兑损益的账户行,若结果为空则报错。
- 数据获取与计算
fetch_and_calculate_accounts_data
:调用get_accounts_data
获取并处理账户数据,填充到accounts
表格。get_accounts_data
:- 校验公司和过账日期必填(
validate_mandatory
)。 - 通过
get_account_balance_from_gle
从GL分录获取账户余额数据,筛选非集团账户、资产/负债/权益类、非库存账户且账户货币与公司货币不同的账户。 - 调用
calculate_new_account_balance
计算重估后的新本位币余额,对比旧余额生成汇兑损益。
- 其他功能
on_cancel
:取消时忽略关联的GL分录。check_journal_entry_condition
:校验已过账的日记账分录与总损益是否一致。
核心逻辑
- 数据来源:从GL分录(
GL Entry
)获取账户的原币余额和本位币余额,筛选出存在汇兑差异的账户。 - 重估逻辑:根据最新汇率计算新本位币余额,与原余额的差异作为汇兑损益,区分已结清(
zero_balance
)和未结清账户的损益处理。 - 精度处理:基于货币精度对余额进行舍入,并允许一定范围内的舍入损失(
rounding_loss_allowance
)。
依赖与工具
- 调用
erpnext
模块的get_company_currency
、get_exchange_rate
等工具函数获取公司货币和汇率。 - 使用
frappe.query_builder
构建复杂查询,处理多条件筛选和聚合计算。
以下是针对 exchange_rate_revaluation.py
的修改方案,新增根据公司国家获取本地化汇率的逻辑(以中国和新加坡为例),需结合实际网站接口调整解析逻辑:
一、新增依赖与工具函数
在文件顶部添加以下代码,引入本地化汇率获取逻辑:
import requests
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from erpnext.setup.doctype.company.company import get_company_country # 假设存在获取公司国家的函数
二、修改 get_accounts_data
方法
在 get_accounts_data
中,计算新余额时调用本地化汇率接口:
def get_accounts_data(self):
self.validate_mandatory()
account_details = self.get_account_balance_from_gle(
company=self.company,
posting_date=self.posting_date,
account=None,
party_type=None,
party=None,
rounding_loss_allowance=self.rounding_loss_allowance,
)
# 获取公司所在国家(需根据实际实现调整,假设公司文档有国家字段)
company_country = get_company_country(self.company) # 示例函数,需实际实现
accounts_with_new_balance = self.calculate_new_account_balance(
self.company, self.posting_date, account_details, company_country # 传递国家参数
)
if not accounts_with_new_balance:
self.throw_invalid_response_message(account_details)
return accounts_with_new_balance
三、新增汇率获取逻辑
在 ExchangeRateRevaluation
类中添加 get_localized_exchange_rate
方法,根据国家调用不同接口:
class ExchangeRateRevaluation(Document):
# ... 原有代码 ...
def calculate_new_account_balance(self, company, posting_date, account_details, company_country):
company_currency = erpnext.get_company_currency(company)
accounts_with_new_balance = []
for acc in account_details:
# 获取本地化汇率
exchange_rate = self.get_localized_exchange_rate(
acc.account_currency, company_currency, company_country, posting_date
)
# 计算新本位币余额(示例逻辑,需根据实际汇率接口返回调整)
new_balance_in_base = flt(acc.balance_in_account_currency * exchange_rate, self.precision("new_balance_in_base_currency"))
# 组装结果
accounts_with_new_balance.append({
"account": acc.account,
"party_type": acc.party_type,
"party": acc.party,
"account_currency": acc.account_currency,
"balance_in_base_currency": acc.balance,
"new_balance_in_base_currency": new_balance_in_base,
"zero_balance": acc.zero_balance,
})
return accounts_with_new_balance
def get_localized_exchange_rate(self, from_currency, to_currency, country, date):
"""根据国家获取本地化汇率(示例实现,需适配实际网站)"""
if country == "China":
return self.fetch_china_money_rate(from_currency, to_currency, date)
elif country == "Singapore":
return self.fetch_mas_rate(from_currency, to_currency, date)
else:
# fallback to原有逻辑或其他服务
return get_exchange_rate(from_currency, to_currency, date) # 假设原有函数
# 中国外汇交易中心汇率抓取(示例,需根据实际网页结构调整)
def fetch_china_money_rate(self, from_currency, to_currency, date):
url = f"https://www.chinamoney.com.cn/fe/rmb/{from_currency}{to_currency.lower()}{date.strftime('%Y%m%d')}.html"
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
# 假设汇率在<table>的特定行,需根据实际HTML解析
rate_element = soup.find("td", text=f"{from_currency}/{to_currency}").find_next("td")
return float(rate_element.text)
except Exception as e:
frappe.log_error(f"中国汇率抓取失败: {e}", "Exchange Rate Revaluation")
return get_exchange_rate(from_currency, to_currency, date) # 失败时回退
# 新加坡金管局汇率抓取(示例,需处理动态加载)
def fetch_mas_rate(self, from_currency, to_currency, date):
chrome_options = Options()
chrome_options.add_argument("--headless")
try:
with webdriver.Chrome(options=chrome_options) as driver:
driver.get(f"https://www.mas.gov.sg/exchange-rates/{date.strftime('%Y-%m-%d')}")
# 假设汇率通过JavaScript加载,需等待元素出现
rate_element = driver.find_element_by_xpath(f"//td[text()='{from_currency}/{to_currency}']/following-sibling::td")
return float(rate_element.text)
except Exception as e:
frappe.log_error(f"新加坡汇率抓取失败: {e}", "Exchange Rate Revaluation")
return get_exchange_rate(from_currency, to_currency, date) # 失败时回退
四、关键调整说明
- 公司国家获取:
- 需实现
get_company_country
函数(例如从Company
文档中读取country
字段)。 - 若ERP系统未存储国家信息,可通过IP定位或手动设置补充。
- 汇率接口适配:
- 实际使用时需分析目标网站的API或HTML结构,调整解析逻辑(如XPath、CSS选择器)。
- 动态加载的页面(如新加坡金管局)需使用Selenium或反爬策略。
- 异常处理:
- 添加日志记录(
frappe.log_error
)和回退机制,避免因抓取失败导致程序中断。 - 建议为敏感操作添加超时控制(如
requests.get(timeout=10)
)。
- 性能优化:
- 对于频繁调用的场景,可添加汇率缓存(如Redis),避免重复抓取。
- Selenium建议使用无头模式(
--headless
)提升效率。
五、后续扩展建议
- 支持更多国家:
- 在
get_localized_exchange_rate
中添加其他国家的分支(如elif country == "USA": ...
)。 - 抽象汇率获取逻辑为独立模块,方便扩展。
- 合规与反爬:
- 确保抓取行为符合目标网站的robots协议,优先使用官方API(如有)。
- 避免高频请求,添加随机延迟(如
time.sleep(2)
)。
- 测试与调试:
- 在开发环境中模拟不同国家的公司数据,验证汇率抓取逻辑。
- 使用
frappe.throw
或日志输出调试信息,排查解析错误。