人财事物信息化 - regional
一、regional模块作用机制分析
ERPNext的 regional 模块用于适配不同国家/地区的本地化需求,包含:
- 地址模板( address_template ):定义符合当地格式的地址字段。
- 凭证类型/报表( doctype/report ):配置会计凭证、税务报表等业务文档结构。
- 打印格式( print_format ):规范票据、报表的打印样式。
- 国家特定目录:如 italy 、 united_states 等目录存储对应国家的本地化配置文件。
二、按中国规范生成 china 目录及文件
- 目录结构
erpnext/regional/china/
├── address_template # 地址模板(含中国行政区划层级)
├── doctype # 会计凭证及税务相关单据类型
│ ├── 记账凭证.json # 符合《会计基础工作规范》的凭证结构
│ ├── 增值税发票.json # 依据发票管理办法的电子发票字段
│ └── 纳税申报表.json # 适配增值税、企业所得税等报表格式
├── print_format # 打印模板(含电子签章位置、含税/不含税标识)
│ ├── 凭证打印格式.xml
│ └── 发票打印格式.xml
├── report # 税务及财务报表
│ ├── 资产负债表.py # 依据《企业会计准则》的报表生成逻辑
│ └── 增值税申报表.py # 对接税务系统的申报数据结构
└── __init__.py # 初始化文件(注册中国本地化配置)
- 核心文件内容示例
- 记账凭证(doctype/记账凭证.json)
{
"fields": [
{"fieldname": "凭证编号", "fieldtype": "Data", "required": true},
{"fieldname": "日期", "fieldtype": "Date", "default": "CURRENT_DATE"},
{"fieldname": "摘要", "fieldtype": "Small Text"},
{"fieldname": "会计科目", "fieldtype": "Link", "options": "会计科目表"},
{"fieldname": "借方金额", "fieldtype": "Currency", "options": "CNY"},
{"fieldname": "贷方金额", "fieldtype": "Currency", "options": "CNY"}
],
"validate": "检查借贷方金额平衡"
}
- 增值税申报表(report/增值税申报表.py)
def generate_vat_report(fiscal_period):
# 依据《增值税暂行条例》计算销项税额、进项税额、应纳税额
data = {
"纳税人识别号": "XXX",
"所属期": fiscal_period,
"销项税额": calculate_output_tax(),
"进项税额": calculate_input_tax(),
"应纳税额": "销项税额 - 进项税额"
}
return format_for_tax_authority(data) # 适配税务系统接口格式
}
三、关键合规点说明
- 会计凭证规范
- 遵循《会计基础工作规范》,凭证需包含日期、编号、摘要、科目、金额等要素,且借贷方金额必须相等。
- 支持电子凭证存储,符合《电子会计档案管理办法》。
- 税务报表对接
- 增值税报表需区分一般计税和简易计税,包含销售额、税额、价税合计等字段。
- 企业所得税报表需关联成本费用、收入明细,符合《企业所得税法》汇算清缴要求。
- 地址模板 采用“省-市-区-街道-门牌号”五级结构,支持“中国境内地址”和“港澳台地址”分类管理。
如需进一步细化某个文件内容或补充特定法规条款,请提供具体需求。
以下是为 regional 目录新增 china 子目录的完整文件结构及代码,遵循现有项目的组织逻辑:
regional/china 目录结构
regional/
└── china/
├── address_template/
│ └── china_address.html
├── doctype/
│ ├── china_tax_info/
│ │ ├── china_tax_info.doctype.json
│ │ └── __init__.py
│ └── __init__.py
├── print_format/
│ ├── chinese_invoice.html
│ ├── chinese_invoice.js
│ └── __init__.py
├── report/
│ ├── chinese_tax_report/
│ │ ├── chinese_tax_report.py
│ │ └── __init__.py
│ └── __init__.py
└── __init__.py
各文件代码实现
- regional/china/init.py
# 中国区域模块初始化文件
from __future__ import unicode_literals
- regional/china/addresstemplate/chinaaddress.html
<!-- 中国地址模板 -->
{% set address_format = "{% if address_line1 %}{{ address_line1 }}<br>{% endif %}{% if address_line2 %}{{ address_line2 }}<br>{% endif %}{{ city_name }}<br>{{ province_name }}<br>{{ pincode }}<br>{{ country_name }}" %}
{{ address_format }}
- regional/china/doctype/chinataxinfo/chinataxinfo.doctype.json
{
"name": "China Tax Info",
"module": "ERPNext",
"doctype": "China Tax Info",
"is_table": 0,
"fields": [
{
"fieldname": "taxpayer_identification_number",
"label": "纳税人识别号",
"fieldtype": "Data",
"reqd": 1,
"length": 20
},
{
"fieldname": "tax_office",
"label": "主管税务机关",
"fieldtype": "Data",
"length": 100
},
{
"fieldname": "tax_office_code",
"label": "税务机关代码",
"fieldtype": "Data",
"length": 11
}
],
"indexes": [],
"permissions": [],
"custom": 0,
"menu_label": "中国税务信息",
"route": "China Tax Info",
"has_quick_entry": 0
}
- regional/china/doctype/chinataxinfo/init.py
# 中国税务信息文档类型初始化
from __future__ import unicode_literals
- regional/china/doctype/init.py
# 中国区域文档类型初始化
from __future__ import unicode_literals
- regional/china/printformat/chineseinvoice.html
<!-- 中文发票打印格式 -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>增值税发票</title>
<style>
/* 发票样式 */
body { font-family: "SimSun", serif; font-size: 14px; }
.invoice-header { text-align: center; border-bottom: 2px solid #000; padding-bottom: 10px; }
.company-info { margin: 10px 0; }
.table-container { width: 100%; border-collapse: collapse; margin: 15px 0; }
.table-container th, .table-container td { border: 1px solid #000; padding: 8px; text-align: center; }
.table-container th { background-color: #f2f2f2; }
.total-row { font-weight: bold; }
.footer { text-align: right; margin-top: 20px; }
</style>
</head>
<body>
<div class="invoice-header">
<h1>增值税普通发票</h1>
<p>发票代码:{{ doc.invoice_code }}</p>
<p>发票号码:{{ doc.invoice_number }}</p>
</div>
<div class="company-info">
<p><strong>销售方:</strong>{{ doc.seller_name }}</p>
<p>纳税人识别号:{{ doc.seller_tax_id }}</p>
<p>地址、电话:{{ doc.seller_address }} {{ doc.seller_phone }}</p>
<p>开户行及账号:{{ doc.seller_bank }} {{ doc.seller_bank_account }}</p>
</div>
<div class="company-info">
<p><strong>购买方:</strong>{{ doc.buyer_name }}</p>
<p>纳税人识别号:{{ doc.buyer_tax_id }}</p>
<p>地址、电话:{{ doc.buyer_address }} {{ doc.buyer_phone }}</p>
<p>开户行及账号:{{ doc.buyer_bank }} {{ doc.buyer_bank_account }}</p>
</div>
<table class="table-container">
<thead>
<tr>
<th>商品名称</th>
<th>规格型号</th>
<th>单位</th>
<th>数量</th>
<th>单价</th>
<th>金额</th>
<th>税率</th>
<th>税额</th>
</tr>
</thead>
<tbody>
{% for item in doc.items %}
<tr>
<td>{{ item.item_name }}</td>
<td>{{ item.specification }}</td>
<td>{{ item.unit }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.rate }}</td>
<td>{{ item.amount }}</td>
<td>{{ item.tax_rate }}%</td>
<td>{{ item.tax_amount }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr class="total-row">
<td colspan="5">合计</td>
<td>{{ doc.total_amount }}</td>
<td>--</td>
<td>{{ doc.total_tax }}</td>
</tr>
<tr class="total-row">
<td colspan="7">价税合计(大写)</td>
<td>{{ doc.total_in_words }}</td>
</tr>
</tfoot>
</table>
<div class="footer">
<p>开票日期:{{ doc.posting_date }}</p>
<p>备注:{{ doc.remarks }}</p>
</div>
</body>
</html>
- regional/china/printformat/chineseinvoice.js
// 中文发票打印脚本
frappe.ui.form.on('Sales Invoice', {
refresh: function(frm) {
// 格式化中文金额大写
frm.set_query('total_in_words', function() {
return {
query: 'erpnext.regional.china.doctype.china_tax_info.china_tax_info.get_chinese_amount_in_words',
args: {
amount: frm.doc.total_amount + frm.doc.total_tax
}
};
});
// 打印时添加中文表头
frm.add_custom_button(__('打印中文发票'), function() {
frappe.route_options = {
docname: frm.doc.name,
print_format: 'Chinese Invoice'
};
frappe.set_route('printview');
});
}
});
// 金额转中文大写工具函数(简化版)
function numberToChinese(num) {
const digits = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖'];
const units = ['', '拾', '佰', '仟', '万', '拾', '佰', '仟', '亿'];
let chinese = '';
let numStr = num.toString();
let decimal = '';
// 处理小数部分
if (numStr.includes('.')) {
const parts = numStr.split('.');
numStr = parts[0];
decimal = parts[1].substring(0, 2); // 保留两位小数
}
// 处理整数部分
let unitIndex = 0;
for (let i = numStr.length - 1; i >= 0; i--) {
const digit = parseInt(numStr[i]);
if (digit === 0) {
// 连续零只显示一个
if (numStr[i-1] && parseInt(numStr[i-1]) !== 0) {
chinese = digits[digit] + chinese;
}
} else {
chinese = digits[digit] + units[unitIndex] + chinese;
}
unitIndex++;
}
// 添加"元"和小数部分
chinese = chinese || digits[0] + '元';
if (decimal) {
chinese += '整';
if (parseInt(decimal[0]) > 0) {
chinese += digits[parseInt(decimal[0])] + '角';
}
if (parseInt(decimal[1]) > 0) {
chinese += digits[parseInt(decimal[1])] + '分';
}
} else {
chinese += '整';
}
return chinese;
}
- regional/china/print_format/init.py
# 中国区域打印格式初始化
from __future__ import unicode_literals
- regional/china/report/chinesetaxreport/chinesetaxreport.py
# 中国税务报表
from __future__ import unicode_literals
import frappe
from frappe import _
def execute(filters=None):
# 报表列定义
columns = [
{
"label": _("日期"),
"fieldname": "posting_date",
"fieldtype": "Date",
"width": 120
},
{
"label": _("发票号码"),
"fieldname": "invoice_number",
"fieldtype": "Data",
"width": 150
},
{
"label": _("销售方"),
"fieldname": "seller_name",
"fieldtype": "Data",
"width": 180
},
{
"label": _("购买方"),
"fieldname": "buyer_name",
"fieldtype": "Data",
"width": 180
},
{
"label": _("应税项目"),
"fieldname": "item_name",
"fieldtype": "Data",
"width": 200
},
{
"label": _("金额"),
"fieldname": "amount",
"fieldtype": "Currency",
"width": 120
},
{
"label": _("税率"),
"fieldname": "tax_rate",
"fieldtype": "Percent",
"width": 100
},
{
"label": _("税额"),
"fieldname": "tax_amount",
"fieldtype": "Currency",
"width": 120
}
]
# 报表数据查询
conditions = "1=1"
if filters.get("from_date"):
conditions += " AND si.posting_date >= %(from_date)s"
if filters.get("to_date"):
conditions += " AND si.posting_date <= %(to_date)s"
if filters.get("company"):
conditions += " AND si.company = %(company)s"
data = frappe.db.sql("""
SELECT
si.posting_date, si.name as invoice_number,
si.seller_name, si.buyer_name,
sii.item_name, sii.amount,
tax.tax_rate, tax.tax_amount
FROM
`tabSales Invoice` si
JOIN
`tabSales Invoice Item` sii ON si.name = sii.parent
JOIN
`tabSales Taxes and Charges` tax ON si.name = tax.parent
AND tax.category = 'Total'
WHERE
{conditions}
ORDER BY
si.posting_date DESC
""".format(conditions=conditions), filters or {}, as_dict=1)
# 计算合计
total_amount = sum(item.amount for item in data)
total_tax = sum(item.tax_amount for item in data)
# 添加合计行
data.append({
"invoice_number": _("合计"),
"amount": total_amount,
"tax_amount": total_tax
})
return columns, data
- regional/china/report/chinesetaxreport/init.py
# 中国税务报表初始化
from __future__ import unicode_literals
- regional/china/report/init.py
# 中国区域报表初始化
from __future__ import unicode_literals
目录说明
- address_template/:存放中国地址格式模板,符合国内地址书写规范。
- doctype/:定义中国特有的税务信息文档类型(如纳税人识别号)。
- print_format/:包含中文发票的打印格式HTML和脚本,支持金额大写转换。
- report/:提供中国税务报表查询功能,可按日期、公司筛选税务数据。
以上代码遵循ERPNext现有国家模块的结构,如需进一步适配中国财务或税务规则,可在此基础上扩展功能。
全电发票入账归档代码实现
根据ERPNext系统架构,以下是全电发票(全面数字化电子发票)入账归档的完整代码实现,包含文档类型定义、入账逻辑和归档管理功能:
一、新增文档类型:全电发票入账记录
regional/china/doctype/etaxinvoicerecord/etaxinvoicerecord.doctype.json
{
"name": "ETAX Invoice Record",
"module": "ERPNext",
"doctype": "ETAX Invoice Record",
"is_table": 0,
"fields": [
{
"fieldname": "invoice_code",
"label": "发票代码",
"fieldtype": "Data",
"reqd": 1,
"length": 20,
"unique": 1,
"description": "全电发票唯一代码"
},
{
"fieldname": "invoice_number",
"label": "发票号码",
"fieldtype": "Data",
"reqd": 1,
"length": 20
},
{
"fieldname": "invoice_date",
"label": "开票日期",
"fieldtype": "Date",
"reqd": 1
},
{
"fieldname": "amount",
"label": "金额",
"fieldtype": "Currency",
"reqd": 1
},
{
"fieldname": "tax_amount",
"label": "税额",
"fieldtype": "Currency",
"reqd": 1
},
{
"fieldname": "total_amount",
"label": "价税合计",
"fieldtype": "Currency",
"reqd": 1,
"calculated": 1,
"formula": "amount + tax_amount"
},
{
"fieldname": "invoice_pdf",
"label": "发票PDF",
"fieldtype": "Attach",
"reqd": 1,
"description": "上传全电发票PDF原件"
},
{
"fieldname": "invoice_xml",
"label": "发票XML",
"fieldtype": "Attach",
"description": "上传全电发票XML元数据"
},
{
"fieldname": "status",
"label": "状态",
"fieldtype": "Select",
"options": "待入账\n已入账\n已归档",
"default": "待入账",
"reqd": 1
},
{
"fieldname": "booking_date",
"label": "入账日期",
"fieldtype": "Date",
"depends_on": "eval:doc.status==\"已入账\""
},
{
"fieldname": "archiving_date",
"label": "归档日期",
"fieldtype": "Date",
"depends_on": "eval:doc.status==\"已归档\""
},
{
"fieldname": "link_to_transaction",
"label": "关联交易",
"fieldtype": "Dynamic Link",
"options": "DocType",
"description": "关联采购/销售发票等交易记录"
}
],
"indexes": [
["invoice_code", "invoice_number", "unique", 1]
],
"permissions": [
{
"role": "Accounts User",
"read": 1
},
{
"role": "Accounts Manager",
"write": 1,
"create": 1,
"delete": 1
}
],
"custom": 0,
"menu_label": "全电发票入账记录",
"route": "ETAX Invoice Record",
"has_quick_entry": 0
}
二、入账归档核心逻辑
regional/china/doctype/etaxinvoicerecord/etaxinvoicerecord.py
# 全电发票入账归档处理
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import nowdate, get_files_path
import os
import shutil
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import PurchaseInvoice
class ETAXInvoiceRecord(frappe.Document):
def before_save(self):
# 自动计算价税合计
if self.amount and self.tax_amount:
self.total_amount = self.amount + self.tax_amount
def on_submit(self):
# 提交时自动入账
self.book_invoice()
def book_invoice(self):
"""全电发票入账处理"""
if self.status != "待入账":
frappe.throw(_("仅待入账状态的发票可执行入账操作"))
# 1. 验证发票唯一性(通过发票代码+号码)
existing_invoice = frappe.db.exists(
"ETAX Invoice Record",
{"invoice_code": self.invoice_code, "invoice_number": self.invoice_number}
)
if existing_invoice and existing_invoice != self.name:
frappe.throw(_("该发票已存在于系统中,请勿重复入账"))
# 2. 关联交易记录(如有)
if self.link_to_transaction and self.link_to_transaction_doctype:
self.validate_linked_transaction()
# 3. 更新状态为"已入账"
self.status = "已入账"
self.booking_date = nowdate()
self.save()
# 4. 记录入账日志
frappe.log_error(
message=f"全电发票入账成功: {self.invoice_code}-{self.invoice_number}",
title="ETAX Invoice Booking"
)
def validate_linked_transaction(self):
"""验证关联交易的金额一致性"""
if self.link_to_transaction_doctype in ["Sales Invoice", "Purchase Invoice"]:
transaction = frappe.get_doc(self.link_to_transaction_doctype, self.link_to_transaction)
if self.total_amount != transaction.grand_total:
frappe.throw(_("发票金额与关联交易金额不一致,请检查"))
def archive_invoice(self):
"""全电发票归档处理"""
if self.status != "已入账":
frappe.throw(_("仅已入账状态的发票可执行归档操作"))
# 1. 生成归档路径(按年-月-日结构)
archive_path = os.path.join(
get_files_path(),
"etax_archive",
nowdate().split("-")[0], # 年
nowdate().split("-")[1], # 月
nowdate().split("-")[2] # 日
)
os.makedirs(archive_path, exist_ok=True)
# 2. 复制发票文件到归档目录
for file_field in ["invoice_pdf", "invoice_xml"]:
if self.get(file_field):
file_doc = frappe.get_doc("File", {"attached_to_name": self.name, "attached_to_field": file_field})
if file_doc:
source_file = os.path.join(get_files_path(), file_doc.file_url.lstrip("/"))
target_file = os.path.join(archive_path, f"{self.invoice_code}_{self.invoice_number}_{file_field}{os.path.splitext(file_doc.file_url)[1]}")
shutil.copy2(source_file, target_file)
# 3. 更新状态为"已归档"
self.status = "已归档"
self.archiving_date = nowdate()
self.save()
# 4. 记录归档日志
frappe.log_error(
message=f"全电发票归档成功: {self.invoice_code}-{self.invoice_number}",
title="ETAX Invoice Archiving"
)
@frappe.whitelist()
def auto_validate_invoice(self):
"""自动验证发票真伪(模拟接口,实际需对接税务平台)"""
# 模拟税务平台验证逻辑
# 实际应用中需调用税务总局电子发票查验接口
validation_result = {
"valid": True,
"message": "发票验证通过",
"verify_date": nowdate()
}
self.db_set("is_verified", 1)
self.db_set("verification_message", validation_result["message"])
self.db_set("verification_date", validation_result["verify_date"])
return validation_result
三、全电发票批量处理脚本
regional/china/doctype/etaxinvoicebatch/etaxinvoicebatch.doctype.json
{
"name": "ETAX Invoice Batch",
"module": "ERPNext",
"doctype": "ETAX Invoice Batch",
"is_table": 0,
"fields": [
{
"fieldname": "batch_name",
"label": "批次名称",
"fieldtype": "Data",
"reqd": 1,
"default": "自动批次-" + frappe.utils.nowdate(),
"read_only": 1
},
{
"fieldname": "batch_date",
"label": "批次日期",
"fieldtype": "Date",
"reqd": 1,
"default": frappe.utils.nowdate()
},
{
"fieldname": "invoice_count",
"label": "发票数量",
"fieldtype": "Int",
"read_only": 1
},
{
"fieldname": "total_amount",
"label": "总金额",
"fieldtype": "Currency",
"read_only": 1
},
{
"fieldname": "etax_invoices",
"label": "全电发票列表",
"fieldtype": "Table",
"options": "ETAX Invoice Record",
"get_items": "get_etax_invoices"
},
{
"fieldname": "status",
"label": "批次状态",
"fieldtype": "Select",
"options": "待处理\n处理中\n已完成\n部分失败\n全部失败",
"default": "待处理",
"read_only": 1
}
],
"permissions": [
{
"role": "Accounts Manager",
"read": 1,
"write": 1,
"create": 1
}
],
"custom": 0,
"menu_label": "全电发票批次处理",
"route": "ETAX Invoice Batch",
"has_quick_entry": 0
}
regional/china/doctype/etaxinvoicebatch/etaxinvoicebatch.py
# 全电发票批量入账归档
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import now, nowdate
class ETAXInvoiceBatch(frappe.Document):
def get_etax_invoices(self):
"""获取待处理的全电发票列表"""
if self.from_date and self.to_date:
invoices = frappe.get_list(
"ETAX Invoice Record",
filters={
"status": "待入账",
"invoice_date": ("between", [self.from_date, self.to_date])
},
fields=["name", "invoice_code", "invoice_number", "amount", "tax_amount"]
)
self.set("etax_invoices", [{"etax_invoice": d.name} for d in invoices])
self.invoice_count = len(invoices)
self.total_amount = sum(d.amount + d.tax_amount for d in invoices)
def process_batch(self):
"""批量处理全电发票"""
if self.status != "待处理":
frappe.throw(_("仅待处理状态的批次可执行处理操作"))
self.status = "处理中"
self.save()
success_count = 0
failure_logs = []
for d in self.etax_invoices:
try:
invoice = frappe.get_doc("ETAX Invoice Record", d.etax_invoice)
invoice.book_invoice() # 入账
invoice.archive_invoice() # 归档
success_count += 1
except Exception as e:
failure_logs.append(f"发票 {invoice.invoice_code}-{invoice.invoice_number} 处理失败: {str(e)}")
# 更新批次状态
if not failure_logs:
self.status = "已完成"
elif len(failure_logs) < len(self.etax_invoices):
self.status = "部分失败"
else:
self.status = "全部失败"
self.save()
# 生成处理报告
report = f"批次处理完成: {now()}\n总发票数: {len(self.etax_invoices)}\n成功: {success_count}\n失败: {len(failure_logs)}"
if failure_logs:
report += "\n失败详情:\n" + "\n".join(failure_logs)
frappe.log_error(message=report, title="ETAX Invoice Batch Process")
return report
四、自动归档后台任务
regional/china/utils/etaxarchivingscheduler.py
# 全电发票自动归档调度任务
from __future__ import unicode_literals
import frappe
from frappe.utils import nowdate, get_files_path
import os
import shutil
from erpnext.regional.china.doctype.etax_invoice_record.etax_invoice_record import ETAXInvoiceRecord
@frappe.whitelist()
def schedule_etax_archiving():
"""定时任务:自动归档已入账超过30天的全电发票"""
# 获取已入账超过30天的发票
cutoff_date = frappe.utils.add_days(nowdate(), -30)
invoices = frappe.get_list(
"ETAX Invoice Record",
filters={
"status": "已入账",
"booking_date": ("<", cutoff_date)
},
fields=["name", "invoice_code", "invoice_number", "invoice_pdf", "invoice_xml"]
)
if not invoices:
frappe.log_error(message="无待归档的全电发票", title="ETAX Archiving Scheduler")
return
# 执行归档
archive_path = os.path.join(
get_files_path(),
"etax_archive_scheduled",
nowdate().split("-")[0],
nowdate().split("-")[1]
)
os.makedirs(archive_path, exist_ok=True)
success_count = 0
for invoice in invoices:
try:
doc = frappe.get_doc("ETAX Invoice Record", invoice.name)
doc.archive_invoice()
success_count += 1
except Exception as e:
frappe.log_error(
message=f"发票 {invoice.invoice_code}-{invoice.invoice_number} 自动归档失败: {str(e)}",
title="ETAX Archiving Error"
)
# 记录归档日志
frappe.log_error(
message=f"自动归档任务完成: 成功归档 {success_count} 张发票",
title="ETAX Archiving Scheduler"
)
五、注册后台调度任务
regional/china/init.py
# 中国区域模块初始化
from __future__ import unicode_literals
import frappe
# 注册全电发票归档调度任务(每天凌晨1点执行)
def setup_scheduler():
frappe.clear_scheduled_jobs("schedule_etax_archiving")
frappe.schedule_job(
"erpnext.regional.china.utils.etax_archiving_scheduler.schedule_etax_archiving",
title="全电发票自动归档",
frequency="Daily",
time="01:00:00"
)
功能说明
1. 全电发票入账: - 支持手动/自动入账,验证发票唯一性和金额一致性 - 关联销售/采购发票等交易记录,确保财务数据闭环 2. 电子归档管理: - 自动生成按日期层级的归档路径(年/月/日) - 同时归档PDF原件和XML元数据,满足税务存储要求 - 归档后更新发票状态为"已归档",防止重复处理 3. 批量处理能力: - 通过批次功能一次性处理多张发票,提升财务效率 - 自动统计批次处理结果,记录失败日志便于追溯 4. 自动调度机制: - 定时任务每日自动归档已入账超30天的发票 - 支持手动触发归档,灵活应对特殊业务场景
部署说明
1. 将上述代码添加到 regional/china 目录下,确保目录结构正确 2. 在ERPNext后台执行"重新加载文档类型",生成ETAX Invoice Record和ETAX Invoice Batch 3. 手动触发一次 setup_scheduler() 函数,注册自动归档任务 4. 测试单张发票入账归档流程,再验证批量处理功能
此方案符合《电子会计凭证归档管理规范》要求,确保全电发票从入账到归档的全流程电子化管理。
电子回单与电子对账单入账归档及对账代码实现
针对企业财务数字化需求,以下是电子回单入账归档和电子对账单月结对账的完整代码实现,基于ERPNext系统架构设计:
一、电子回单入账归档模块
- 电子回单文档类型定义
regional/china/doctype/ereceipt/ereceipt.doctype.json
{
"name": "Electronic Receipt",
"module": "ERPNext",
"doctype": "Electronic Receipt",
"is_table": 0,
"fields": [
{
"fieldname": "receipt_number",
"label": "回单编号",
"fieldtype": "Data",
"reqd": 1,
"unique": 1,
"description": "银行电子回单唯一编号"
},
{
"fieldname": "transaction_date",
"label": "交易日期",
"fieldtype": "Date",
"reqd": 1
},
{
"fieldname": "account_number",
"label": "银行账号",
"fieldtype": "Data",
"reqd": 1
},
{
"fieldname": "counterparty_name",
"label": "对方户名",
"fieldtype": "Data",
"reqd": 1
},
{
"fieldname": "counterparty_account",
"label": "对方账号",
"fieldtype": "Data",
"description": "对方银行账号"
},
{
"fieldname": "amount",
"label": "金额",
"fieldtype": "Currency",
"reqd": 1
},
{
"fieldname": "transaction_type",
"label": "交易类型",
"fieldtype": "Select",
"options": "收入\n支出\n转账",
"reqd": 1
},
{
"fieldname": "bank_name",
"label": "开户银行",
"fieldtype": "Data",
"reqd": 1
},
{
"fieldname": "receipt_pdf",
"label": "回单PDF",
"fieldtype": "Attach",
"reqd": 1,
"description": "上传电子回单PDF"
},
{
"fieldname": "status",
"label": "状态",
"fieldtype": "Select",
"options": "待入账\n已入账\n已归档",
"default": "待入账",
"reqd": 1
},
{
"fieldname": "booking_date",
"label": "入账日期",
"fieldtype": "Date",
"depends_on": "eval:doc.status==\"已入账\""
},
{
"fieldname": "archiving_date",
"label": "归档日期",
"fieldtype": "Date",
"depends_on": "eval:doc.status==\"已归档\""
},
{
"fieldname": "linked_transaction",
"label": "关联交易",
"fieldtype": "Dynamic Link",
"options": "DocType",
"description": "关联凭证、发票等交易记录"
}
],
"indexes": [["receipt_number", "unique", 1]],
"permissions": [
{"role": "Accounts User", "read": 1},
{"role": "Accounts Manager", "write": 1, "create": 1, "delete": 1}
],
"menu_label": "电子回单",
"route": "Electronic Receipt",
"has_quick_entry": 0
}
- 电子回单入账归档逻辑
regional/china/doctype/ereceipt/ereceipt.py
# 电子回单入账归档处理
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import nowdate, get_files_path
import os
import shutil
from erpnext.accounts.doctype.journal_entry.journal_entry import JournalEntry
class ElectronicReceipt(frappe.Document):
def before_save(self):
# 自动验证交易类型与金额方向
if self.transaction_type == "支出" and self.amount < 0:
self.amount = abs(self.amount)
elif self.transaction_type == "收入" and self.amount > 0:
self.amount = abs(self.amount)
def on_submit(self):
# 提交时自动入账
self.book_receipt()
def book_receipt(self):
"""电子回单入账处理"""
if self.status != "待入账":
frappe.throw(_("仅待入账状态的回单可执行入账操作"))
# 1. 验证回单唯一性
existing_receipt = frappe.db.exists(
"Electronic Receipt", {"receipt_number": self.receipt_number}
)
if existing_receipt and existing_receipt != self.name:
frappe.throw(_("该回单已存在于系统中,请勿重复入账"))
# 2. 关联交易记录(如有)
if self.linked_transaction and self.linked_transaction_doctype:
self.validate_linked_transaction()
# 3. 创建会计凭证(简化示例,实际需根据科目配置生成)
if self.transaction_type == "收入":
self.create_income_jv()
else:
self.create_expense_jv()
# 4. 更新状态为"已入账"
self.status = "已入账"
self.booking_date = nowdate()
self.save()
def validate_linked_transaction(self):
"""验证关联交易金额一致性"""
if self.linked_transaction_doctype in ["Sales Invoice", "Purchase Invoice", "Journal Entry"]:
transaction = frappe.get_doc(self.linked_transaction_doctype, self.linked_transaction)
if hasattr(transaction, "grand_total") and self.amount != transaction.grand_total:
frappe.throw(_("回单金额与关联交易金额不一致,请检查"))
def create_income_jv(self):
"""生成收入类会计凭证"""
jv = frappe.new_doc("Journal Entry")
jv.voucher_type = "Bank Entry"
jv.posting_date = self.transaction_date
jv.company = frappe.defaults.get_defaults().company
# 银行科目(借)
bank_account = frappe.db.get_value(
"Bank Account", {"account_number": self.account_number}, "account"
)
if not bank_account:
frappe.throw(_("未找到对应的银行科目,请先配置"))
jv.append("accounts", {
"account": bank_account,
"debit": self.amount,
"debit_in_account_currency": self.amount,
"reference_type": "Electronic Receipt",
"reference_name": self.name
})
# 收入科目(贷)- 实际应用中应通过科目映射表获取
income_account = frappe.db.get_value(
"Company", frappe.defaults.get_defaults().company, "default_income_account"
)
jv.append("accounts", {
"account": income_account,
"credit": self.amount,
"credit_in_account_currency": self.amount,
"reference_type": "Electronic Receipt",
"reference_name": self.name
})
jv.save(ignore_permissions=True)
jv.submit()
# 关联凭证
self.linked_transaction = jv.name
self.linked_transaction_doctype = "Journal Entry"
def create_expense_jv(self):
"""生成支出类会计凭证(逻辑类似收入,略)"""
pass
def archive_receipt(self):
"""电子回单归档处理"""
if self.status != "已入账":
frappe.throw(_("仅已入账状态的回单可执行归档操作"))
# 1. 生成归档路径
archive_path = os.path.join(
get_files_path(), "e_receipt_archive",
self.transaction_date.split("-")[0],
self.transaction_date.split("-")[1]
)
os.makedirs(archive_path, exist_ok=True)
# 2. 复制回单文件到归档目录
if self.receipt_pdf:
file_doc = frappe.get_doc("File", {"attached_to_name": self.name, "attached_to_field": "receipt_pdf"})
if file_doc:
source_file = os.path.join(get_files_path(), file_doc.file_url.lstrip("/"))
target_file = os.path.join(
archive_path,
f"{self.receipt_number}_{self.transaction_date.replace('-', '')}.pdf"
)
shutil.copy2(source_file, target_file)
# 3. 更新状态为"已归档"
self.status = "已归档"
self.archiving_date = nowdate()
self.save()
二、电子对账单月结对账模块
- 电子对账单文档类型
regional/china/doctype/estatement/estatement.doctype.json
{
"name": "Electronic Statement",
"module": "ERPNext",
"doctype": "Electronic Statement",
"is_table": 0,
"fields": [
{
"fieldname": "statement_month",
"label": "对账单月份",
"fieldtype": "Date",
"reqd": 1,
"format": "YYYY-MM",
"default": frappe.utils.nowdate().split("-")[0] + "-" + frappe.utils.nowdate().split("-")[1]
},
{
"fieldname": "account_number",
"label": "银行账号",
"fieldtype": "Data",
"reqd": 1
},
{
"fieldname": "bank_name",
"label": "开户银行",
"fieldtype": "Data",
"reqd": 1
},
{
"fieldname": "beginning_balance",
"label": "期初余额",
"fieldtype": "Currency",
"reqd": 1
},
{
"fieldname": "ending_balance",
"label": "期末余额",
"fieldtype": "Currency",
"reqd": 1
},
{
"fieldname": "statement_pdf",
"label": "对账单PDF",
"fieldtype": "Attach",
"reqd": 1
},
{
"fieldname": "reconciliation_status",
"label": "对账状态",
"fieldtype": "Select",
"options": "未对账\n对账中\n已对账\n对账不平",
"default": "未对账",
"reqd": 1
},
{
"fieldname": "reconciliation_date",
"label": "对账日期",
"fieldtype": "Date",
"depends_on": "eval:doc.reconciliation_status!=\"未对账\""
},
{
"fieldname": "reconciliation_notes",
"label": "对账说明",
"fieldtype": "Small Text"
},
{
"fieldname": "statement_transactions",
"label": "对账单交易明细",
"fieldtype": "Table",
"options": "E Statement Transaction"
}
],
"indexes": [["account_number", "statement_month", "unique", 1]],
"permissions": [
{"role": "Accounts User", "read": 1},
{"role": "Accounts Manager", "write": 1, "create": 1, "delete": 1}
],
"menu_label": "电子对账单",
"route": "Electronic Statement",
"has_quick_entry": 0
}
- 对账单交易明细子表
regional/china/doctype/estatementtransaction/estatementtransaction.doctype.json
{
"name": "E Statement Transaction",
"module": "ERPNext",
"doctype": "E Statement Transaction",
"is_table": 1,
"fields": [
{
"fieldname": "transaction_date",
"label": "交易日期",
"fieldtype": "Date",
"reqd": 1
},
{
"fieldname": "transaction_number",
"label": "交易流水号",
"fieldtype": "Data"
},
{
"fieldname": "counterparty_name",
"label": "对方户名",
"fieldtype": "Data"
},
{
"fieldname": "amount",
"label": "金额",
"fieldtype": "Currency",
"reqd": 1
},
{
"fieldname": "transaction_type",
"label": "交易类型",
"fieldtype": "Data"
},
{
"fieldname": "matched_with",
"label": "匹配记录",
"fieldtype": "Dynamic Link",
"options": "DocType",
"description": "关联系统中的交易记录"
},
{
"fieldname": "is_matched",
"label": "是否匹配",
"fieldtype": "Check",
"default": 0
}
]
}
- 月结对账核心逻辑
regional/china/doctype/estatement/estatement.py
# 电子对账单月结对账处理
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import add_months, nowdate
from erpnext.accounts.doctype.bank_reconciliation.bank_reconciliation import BankReconciliation
from erpnext.accounts.doctype.bank_account.bank_account import get_bank_account
class ElectronicStatement(frappe.Document):
def before_save(self):
# 自动计算期间发生额
self.calculate_period_amounts()
def calculate_period_amounts(self):
"""计算对账单期间收入/支出总额"""
if not self.statement_transactions:
return
income = sum(t.amount for t in self.statement_transactions if t.amount > 0)
expense = sum(abs(t.amount) for t in self.statement_transactions if t.amount < 0)
self.total_income = income
self.total_expense = expense
# 验证余额计算正确性
expected_ending_balance = self.beginning_balance + income - expense
if abs(expected_ending_balance - self.ending_balance) > 0.01:
frappe.msgprint(_("对账单余额计算不一致,请检查明细"), alert=True)
def reconcile_statement(self):
"""执行对账单自动对账"""
if self.reconciliation_status != "未对账":
frappe.throw(_("仅未对账状态的对账单可执行对账操作"))
self.reconciliation_status = "对账中"
self.save()
matched_count = 0
unmatched_transactions = []
# 1. 遍历对账单交易明细
for trans in self.statement_transactions:
if trans.is_matched:
continue
# 2. 查找系统中匹配的交易记录(按金额+日期+对方户名)
match = self.find_matching_transaction(trans)
if match:
# 3. 标记为已匹配
trans.matched_with = match.name
trans.matched_with_doctype = match.doctype
trans.is_matched = 1
matched_count += 1
else:
unmatched_transactions.append({
"date": trans.transaction_date,
"amount": trans.amount,
"counterparty": trans.counterparty_name
})
# 4. 更新对账状态
if not unmatched_transactions:
self.reconciliation_status = "已对账"
else:
self.reconciliation_status = "对账不平"
self.reconciliation_date = nowdate()
self.save()
# 5. 生成对账报告
report = f"对账完成: {nowdate()}\n总交易数: {len(self.statement_transactions)}\n已匹配: {matched_count}\n未匹配: {len(unmatched_transactions)}"
if unmatched_transactions:
report += "\n未匹配明细:\n" + "\n".join([
f"日期: {t['date']}, 金额: {t['amount']}, 对方: {t['counterparty']}"
for t in unmatched_transactions
])
frappe.log_error(message=report, title="电子对账单对账报告")
return report
def find_matching_transaction(self, trans):
"""查找匹配的系统交易记录"""
# 简化匹配逻辑:按金额、日期、对方户名模糊匹配
# 实际应用中可增加流水号、账号等精确匹配条件
# 收入交易匹配
if trans.amount > 0:
match = frappe.db.sql("""
SELECT name, 'Sales Invoice' as doctype
FROM `tab