对于ERP系统来说,管控的是整个企业的业务和财务数据,实际生产和运行过程中,除了尾差外,我们不允许出现任何形式的的数据运算错误,所以也就对系统的计算提出了很大的挑战。
单方面的加减乘除,对于任何系统来说都很轻易完成,加上精度计算,也不太费力,但是现在如果有一个业务场景:一条业务数据,包含无税金额(原币),含税金额,数量,税率,税额,集团本币(无税,含税,税额),组织本币(无税,含税,税额)汇率,折扣比例,单品折扣等等…
现在我要告诉你,这些业务数据中,如果有一个数据发生变动,所有数据都要重新计算发生变动,当然也有可能是部分变动。
我们讲一下用友NC6系列的一般业务处理逻辑:
一般来说我们把一个事件切分成三个阶段:前、中、后。
对于代码处理来理解就是: 编辑前事件、编辑和编辑后事件
我们首先想到这个问题的解决方案是:这个问题简单,比如说你要修改数量,我在数量的编辑后事件里边计算逻辑,然后重新计算金额、税额等数据。
这个方案怎么样?可行吗?
这个解决方案可以解决实际的计算处理问题,但如果说可行与否,举个栗子,古代的马车也能把人送到目的地,现代的汽车也能把人送到目的地,方案可行,但绝对不是较优,更谈不上最优。如果有2-3个字段需要联动,这样的处理一点问题没有,但是现在20-30个字段需要联动,难道要写20-30个编辑后事件吗?当然不可能。
我们首先可以先做一个约定或者约束,比如说数量就叫做num,金额叫nmny,而且可以通过映射类来做你的命名和我标准命名的映射,计算一般是需要两个数据,JAVA面向对象编程,我们可以把这两个数据称为数值对象和参数对象,参数对象里面主要是指一些参数条件,是否含税优先、是否折扣优先等等
public void calculate(int row, String editKey) {
// 如果编辑字段不会影响到数量单价金额,不进行计算
if (!CalculatorUtil.getInstance().getNeedCalKey().contains(editKey)) {
return;
}
// 1.创建数据映射实例 获得数据项之间的映射关系
IRelationForItems item = new RelationItemForCal();
// 单品折扣字段设为“发票折扣”
item.setNitemdiscountrateKey(SaleInvoiceBVO.NINVOICEDISRATE);
// 2.创建数据集实例 初始化数据关系计算用的数据集
IDataSetForCal data = null;
Calculator tool = null;
ScaleUtils scale = UIScaleUtils.getScaleUtils();
CardKeyValue keyValue = new CardKeyValue(this.cardpanel);
// 创建参数实例,在计算的时候用来获得参数条件:是否含税优先等
Condition cond = SOCalConditionRule.getCondition();
// 设置调单价方式调折扣
String trantypecode =
keyValue.getHeadStringValue(SaleInvoiceHVO.VTRANTYPECODE);
String pk_group = AppContext.getInstance().getPkGroup();
boolean ischgprice =
CalculatorUtil.getInstance().getChgPriceOrDiscount(pk_group,
trantypecode);
cond.setIsChgPriceOrDiscount(false);
SOBuysellTriaRule buysellrule = new SOBuysellTriaRule(keyValue);
cond.setInternational(buysellrule.isHeadBuysellFlagOut());
// 3.计算每行数据
UnitChangeRateRule rule = new UnitChangeRateRule(keyValue);
// 设置业务单位是否固定换算率
cond.setIsFixNchangerate(rule.isAstFixedRate(row));
// 设置报价单位是否固定换算率
cond.setIsFixNqtunitrate(rule.isQtFixedRate(row));
data = new SaleInvoiceCarDataSet(this.cardpanel, row, item);
tool = new Calculator(data, scale);
// 两个参数 cond 为计算时的参数条件
tool.calculate(cond, editKey);
// 数量单价金额编辑后重建前金额处理
// 冲减前金额 = 价税合计 + 费用冲抵金额
UFDouble taxmny =
keyValue.getBodyUFDoubleValue(row, SaleInvoiceBVO.NORIGTAXMNY);
UFDouble origarsubmny =
keyValue.getBodyUFDoubleValue(row, SaleInvoiceBVO.NORIGSUBMNY);
UFDouble nbfsubmny = MathTool.add(taxmny, origarsubmny);
keyValue.setBodyValue(row, "nbforigsubmny", nbfsubmny);
}
我们可以看一下具体的处理操作
我们在编辑字段时,当然一次只能编辑一次,所以我们肯定是针对某个字段进行联动字段的计算处理
- 首先我们获取这个字段与之相关的字段:例如,改动数量的话,金额等字段一定会变动,那么金额字段就是数量字段的相关字段
- 针对某个字段来说,我们可以指定计算策略,比如我在计算金额的时候,我要判断一下当前组织,再确定其他字段是不是要进行改动,那么这种情况,不属于标准的计算逻辑,但是我们可以自己编写策略来实现计算逻辑
- 指定几个特殊的处理操作,如果改动的是集团金额,我们使用集团金额的处理类,如果改动的是全局金额,我们使用全局金额的处理类
- 美元相关字段的处理,避免重复计算
- 计算完成后提交数据结果集
public void calculate(Condition cond, String key) {
this.wrapCondtion(cond, key);
IRelationForItems lItem = this.data.getRelationForItem();
// cond.setUnitPriorType(Condition.MAIN_PRIOR);
IStrategyForCal strategy = this.map.get(key);
if ((strategy != null || ((StringUtils.equalsIgnoreCase(key, lItem.getNexchangerateKey()))
|| (StringUtils.equalsIgnoreCase(key, lItem.getNglobalexchgrateKey())) || (StringUtils
.equalsIgnoreCase(key, lItem.getNgroupexchgrateKey())))) && !cond.isCalLocalPior()) {
if (!StringUtils.equalsIgnoreCase(key, lItem.getNexchangerateKey()) && strategy != null) {
// cond.setIsChgPriceOrDiscount(cond.getIsChgPriceOrDiscount()&&cond.is)
strategy.calculate(cond);
}
this.setNoPermitModifyColumn(key);
if (StringUtils.equalsIgnoreCase(key, lItem.getNglobalexchgrateKey())) {
GlobalMoneyCalculator cal = new GlobalMoneyCalculator(this.data, this.scale);
cal.calculate(cond);
this.data.commit();
return;
}
if (StringUtils.equalsIgnoreCase(key, lItem.getNgroupexchgrateKey())) {
GroupMoneyCalculator cal = new GroupMoneyCalculator(this.data, this.scale);
cal.calculate(cond);
this.data.commit();
return;
}
this.calculateLocalGlobalGroup(cond);
} else {
strategy = this.localmap.get(key);
if (strategy != null && !StringUtils.equalsIgnoreCase(key, lItem.getNexchangerateKey())) {
// if (!key.equals(lItem.getNexchangerateKey())) {
strategy.calculate(cond);
// }
}
// 币种相同并且(有策略或者这本汇率变)
if (strategy != null || StringUtils.equalsIgnoreCase(key, lItem.getNexchangerateKey())) {
boolean isSame =
(this.data.getCorigcurrencyid() != null)
&& (this.data.getCcurrencyid() != null)
&& StringUtils.equalsIgnoreCase(this.data.getCorigcurrencyid(),
this.data.getCcurrencyid());
if (isSame
|| (cond.isCalOrigCurr()
&& !StringUtils.equalsIgnoreCase(key, lItem.getNdeductibletaxKey())
&& !StringUtils.equalsIgnoreCase(key, lItem.getNdeductibleTaxrateKey())
&& !StringUtils.equalsIgnoreCase(key, lItem.getNcaltaxmnyKey()) && !StringUtils
.equalsIgnoreCase(key, lItem.getNtaxKey()))) {
this.setNoPermitModifyColumn(key);
this.calculateOrigGlobalGroup(cond);
}
}
}
if ((this.data.getRelationForItem() instanceof IRelationDollarForItems)
&& (this.data.getDataset() instanceof IDataSetDollarForCal)) {
// 如果key是美元相关字段,则不再重复进行美元计算
IRelationDollarForItems dollarItem = (IRelationDollarForItems) this.data.getRelationForItem();
boolean isDollarKey =
(StringUtils.equalsIgnoreCase(key, dollarItem.getNusdmnyKey()))
|| (StringUtils.equalsIgnoreCase(key, dollarItem.getNqtusdpriceKey()))
|| (StringUtils.equalsIgnoreCase(key, dollarItem.getNusdpriceKey()));
if (!isDollarKey) {
this.calculateDollar(cond);
}
}
// DollarAndPriceFormula daf = new
// DollarAndPriceFormula(this.data,this.scale);
// daf.calculate(cond);
// CheckIsResultRight check = new CheckIsResultRight(data);
// check.checkResult();
// 没有问题,将数据写入到真正的数据源中
this.data.commit();
}
实际上我们在具体计算处理中,还是主要使用策略的模式进行计算,我们匹配了20多种策略方式来进行计算处理,这样能针对高效完成数据计算处理,防止重复计算或者没有计算,一般来说我们的所有业务数据的计算处理都应该是幂等的
我们简单分析一下一个计算策略,在计算策略中我们要考虑的一般有,是否含税优先,调单价还是调折扣,是否进行本辅币计算
public void calculate(Condition cond) {
if (this.isSameCurrencyId()) {
this.data.commit();
this.cloneValueFromOrigValue();
// // 记税金额
// this.calculateNCalTaxMny();
// // 主单位本币税额
// this.calculateNTax();
// 需求变更2012-3-23
if (this.data.getCondition().getIsTaxOrNet()) {
// 无税金额 = 本币无税金额(见本币金额的算法)
new LocalTaxMnyCalculator(this.data, this.scale).calculate(cond);
this.data.setAttributeValue(this.item.getNorigmnyKey(), this.data.getNmny());
} else {
// 价税合计 = 本币价税合计(见本币金额的算法)
new LocalMnyCalculator(this.data, this.scale).calculate(cond);
this.data.setAttributeValue(this.item.getNorigtaxmnyKey(), this.data.getNtaxmny());
}
// 本币不可抵扣税额
// this.calculateNDeductibleTax();
// 记成本金额
// this.calculateNCostmny();
return;
}
if (cond.getIsTaxOrNet()) {
this.calculateNtaxMny();
// this.calculateLocalPriceFromOrigPrize(this.data.getNtaxmny(), cond);
new LocalTaxMnyCalculator(this.data, this.scale).calculate(cond);
} else {
this.calculateNmy();
// this.calculateLocalPriceFromOrigPrize(this.data.getNmny(), cond);
new LocalMnyCalculator(this.data, this.scale).calculate(cond);
}
}
我们不难发现在实际处理计算逻辑中,我们还是需要根据不同的cond进行不同的计算类的处理
以下代码实现
主单位本币税额=主单位本币无税金额*税率
private void calculateNTax() {
String moneyKey = this.item.getNmnyKey();
String taxKey = this.item.getNtaxKey();
String taxRateKey = this.item.getNtaxrateKey();
String ftaxtypeflag = this.item.getFtaxtypeflagKey();
String curridKey = this.item.getCcurrencyidKey();
Object currid = this.data.getAttributeValue(curridKey);
boolean flag = this.data.hasItemKey(moneyKey)
&& this.data.hasItemKey(taxRateKey)
&& this.data.hasItemKey(taxRateKey) && (currid != null);
if (!flag) {
return;
}
TaxTaxRateMoneyFormula formula = new TaxTaxRateMoneyFormula(this.data,
this.scale, moneyKey, taxKey, taxRateKey, ftaxtypeflag,
curridKey);
formula.calculateTax();
}
再进行税额等的计算处理,以及计算后的赋值操作,对于税额的数据计算处理执行完毕
这就是整体计算逻辑的一个很小的分支计算
public void calculateTax() {
UFDouble money = (UFDouble) this.data.getAttributeValue(this.moneyKey);
// UFDouble taxRate = this.getTaxRate();
String curr = (String) this.data.getAttributeValue(this.currid);
UFDouble rate = (UFDouble) this.data.getAttributeValue(this.taxRateKey);
UFDouble value = null;
if (!this.data.getCondition().isInternational()) {
value = this.calTax(money, rate);
} else {
rate = CalculatorUtil.div(rate, new UFDouble(100));
UFDouble calMoney = (UFDouble) this.data
.getAttributeValue(this.data.getRelationForItem()
.getNcaltaxmnyKey());
value = CalculatorUtil.multiply(calMoney, rate);
}
value = this.scale.adjustMnyScale(value, curr);
this.data.setAttributeValue(this.taxKey, value);
}
针对不同情况的针对性处理,获取相关联字段的联动,避免重复计算更新data赋值一套完整的处理流程就处理完成了。