You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Frappe ERPNext:销售发票自定义总计删除子表行后不递减

问题:Frappe/ERPNext销售发票自定义总计,删除子表行后总计不更新

问题描述

在Frappe/ERPNext v14的自定义销售发票中,需计算每项数量×单价打85折后的总计。目前新增/编辑子表行时总计更新正常,但删除items子表行后,总计不会随之递减。

已尝试方案

  • 客户端JS:绑定qty、rate、items_add、items_remove、refresh、onload、after_save事件触发重新计算,刷新字段,还添加了自定义“重新计算并保存”按钮。
  • 服务端Python:在before_save/before_submit钩子中执行重新计算,用frappe.db.set_value持久化数据,添加了调试用的frappe.msgprint。
  • 自定义字段:已创建custom_g_total(货币型,归属Sales Invoice Item)和custom_grand_total_amount(货币型,归属Sales Invoice),均设置为非只读。
  • 尝试过删除后手动保存、重启bench/清除缓存、执行migrate操作,无控制台或Frappe日志错误;调试信息显示重新计算已执行,但总计字段未变化。
  • 简化代码避免冲突,在全新发票上测试,问题依旧。

代码片段

JavaScript(销售发票自定义表单脚本)

frappe.ui.form.on('Sales Invoice Item', {
    qty: function(frm, cdt, cdn) { 
        recalc(frm);
    },
    rate: function(frm, cdt, cdn) { 
        recalc(frm);
    }
});

frappe.ui.form.on('Sales Invoice', {
    items_add: function(frm) {
        recalc(frm);
    },
    items_remove: function(frm) {
        recalc(frm);
    },
    refresh: function(frm) {
        recalc(frm);
        frm.add_custom_button('Recalculate & Save', function() {
            recalc(frm);
            frm.save();
        });
    }
});

function recalc(frm) {
    let total = 0;
    (frm.doc.items || []).forEach(row => {
        let qty = parseFloat(row.qty) || 0;
        let rate = parseFloat(row.rate) || 0;
        row.custom_g_total = qty * rate * 0.85;
        frappe.model.set_value(row.doctype, row.name, 'custom_g_total', row.custom_g_total);
        total += row.custom_g_total;
    });
    frm.set_value('custom_grand_total_amount', total);
    frm.refresh_field('custom_grand_total_amount');
    frm.refresh_field('items');
}

Python(library_management/doctype/sales_invoice/sales_invoice.py)

import frappe
from frappe.model.document import Document

class SalesInvoice(Document):
    pass

def calculate_grand_total(doc, method=None):
    total = 0
    for row in doc.get("items", []):
        qty = row.qty or 0
        rate = row.rate or 0
        row.custom_g_total = qty * rate * 0.85
        total += row.custom_g_total
    
    doc.custom_grand_total_amount = total
    frappe.db.set_value(doc.doctype, doc.name, 'custom_grand_total_amount', total)
    frappe.msgprint(f"DEBUG: Grand total recalculated to {total}")

复现步骤

  1. 创建新销售发票。
  2. 添加2个商品(数量=1,单价=100 → 每项custom_g_total=85,总计=170)。
  3. 删除1个商品。
  4. 保存或点击“重新计算并保存”按钮。
  5. 查看总计(仍显示170,而非预期的85)。

预期与实际行为

  • 预期:删除子表行后,总计自动递减,匹配剩余商品的折扣后总额。
  • 实际:总计无变化;调试信息显示重新计算得到的新总计正确,但字段未更新。

解决方法

问题根源

  1. 客户端:items_remove事件执行时机:Frappe中items_remove事件触发后,frm.doc.items并未立即同步删除后的行数据,导致计算时仍使用删除前的列表。
  2. 服务端:冗余的frappe.db.set_value:before_save钩子执行时,系统会自动保存doc对象的变更,手动调用set_value会引发数据覆盖冲突。

修正后的代码

客户端JS调整

给items_remove事件添加延迟执行,确保子表数据同步完成:

frappe.ui.form.on('Sales Invoice', {
    // 保留其他事件逻辑
    items_remove: function(frm) {
        // 延迟100ms执行,确保子表数据已更新
        setTimeout(() => recalc(frm), 100);
    },
    // 其他代码不变
});

服务端Python调整

删除冗余的frappe.db.set_value,直接依赖doc对象的变更:

import frappe
from frappe.model.document import Document

class SalesInvoice(Document):
    pass

def calculate_grand_total(doc, method=None):
    total = 0
    for row in doc.get("items", []):
        qty = row.qty or 0
        rate = row.rate or 0
        row.custom_g_total = qty * rate * 0.85
        total += row.custom_g_total
    
    doc.custom_grand_total_amount = total
    frappe.msgprint(f"DEBUG: Grand total recalculated to {total}")

额外验证步骤

  1. 清除前端缓存:执行bench clear-cache和bench build(开发模式下)。
  2. 确认钩子注册:在library_management/hooks.py中添加或检查以下配置:
doc_events = {
    "Sales Invoice": {
        "before_save": "library_management.doctype.sales_invoice.sales_invoice.calculate_grand_total",
        "before_submit": "library_management.doctype.sales_invoice.sales_invoice.calculate_grand_total"
    }
}
  1. 测试时删除行后,可等待自动计算完成再保存,或直接保存由服务端钩子确保数据正确性。

内容的提问来源于stack exchange,提问作者Abhidha Pokharel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.11 22:13:19