人财事物信息化 - exchange_rate_revaluation.py

该网页是erpnext项目中exchange_rate_revaluation.py文件的代码内容,用于实现汇率重估功能,核心要点如下:

类定义

  • ExchangeRateRevaluation类:继承自Document,包含公司、过账日期、汇兑损益科目、舍入损失允许值等属性,以及关联的accounts表格(记录各账户重估数据)。

主要方法

  1. 验证与计算
  • validate:调用validate_rounding_loss_allowance(校验舍入损失允许值需在0-1之间)和set_total_gain_loss(计算总汇兑损益,区分已过账和未过账损益)。
  • set_total_gain_loss:遍历账户数据,根据新旧本位币余额差异计算单个账户损益,汇总为总损益。

通过以上修改,系统将根据公司所在国家自动调用本地化汇率源,提升国际化场景下的准确性。实际部署需根据目标网站的具体结构调整代码。

  1. 提交前处理
  • before_submit:调用remove_accounts_without_gain_loss,过滤掉无汇兑损益的账户行,若结果为空则报错。
  1. 数据获取与计算
  • fetch_and_calculate_accounts_data:调用get_accounts_data获取并处理账户数据,填充到accounts表格。
  • get_accounts_data
  • 校验公司和过账日期必填(validate_mandatory)。
  • 通过get_account_balance_from_gle从GL分录获取账户余额数据,筛选非集团账户、资产/负债/权益类、非库存账户且账户货币与公司货币不同的账户。
  • 调用calculate_new_account_balance计算重估后的新本位币余额,对比旧余额生成汇兑损益。
  1. 其他功能
  • on_cancel:取消时忽略关联的GL分录。
  • check_journal_entry_condition:校验已过账的日记账分录与总损益是否一致。

核心逻辑

  • 数据来源:从GL分录(GL Entry)获取账户的原币余额和本位币余额,筛选出存在汇兑差异的账户。
  • 重估逻辑:根据最新汇率计算新本位币余额,与原余额的差异作为汇兑损益,区分已结清(zero_balance)和未结清账户的损益处理。
  • 精度处理:基于货币精度对余额进行舍入,并允许一定范围内的舍入损失(rounding_loss_allowance)。

依赖与工具

  • 调用erpnext模块的get_company_currencyget_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)  # 失败时回退

四、关键调整说明

  1. 公司国家获取:
  • 需实现 get_company_country 函数(例如从 Company 文档中读取 country 字段)。
  • 若ERP系统未存储国家信息,可通过IP定位或手动设置补充。
  1. 汇率接口适配:
  • 实际使用时需分析目标网站的API或HTML结构,调整解析逻辑(如XPath、CSS选择器)。
  • 动态加载的页面(如新加坡金管局)需使用Selenium或反爬策略。
  1. 异常处理:
  • 添加日志记录(frappe.log_error)和回退机制,避免因抓取失败导致程序中断。
  • 建议为敏感操作添加超时控制(如requests.get(timeout=10))。
  1. 性能优化:
  • 对于频繁调用的场景,可添加汇率缓存(如Redis),避免重复抓取。
  • Selenium建议使用无头模式(--headless)提升效率。

五、后续扩展建议

  1. 支持更多国家:
  • get_localized_exchange_rate 中添加其他国家的分支(如elif country == "USA": ...)。
  • 抽象汇率获取逻辑为独立模块,方便扩展。
  1. 合规与反爬:
  • 确保抓取行为符合目标网站的robots协议,优先使用官方API(如有)。
  • 避免高频请求,添加随机延迟(如time.sleep(2))。
  1. 测试与调试:
  • 在开发环境中模拟不同国家的公司数据,验证汇率抓取逻辑。
  • 使用frappe.throw或日志输出调试信息,排查解析错误。
Discard
Save
Review Changes ← Back to Content
Message Status Space Raised By Last update on