预付款申请
一、背景
客户购买一批材料,做了采购订单。需要支付一笔定金。这个定金的操作不能直接到财务的付款。需要完善审批流程,所以做了本单据,预付款申请
通过此案例我们来学习开发技术,业务逻辑并不完善,请注意
二、能力清单
编号 |
能力清单 |
1 |
业务流:原厂推自建 |
2 |
开放平台接口调用 |
3 |
页面设计器常用操作 |
4 |
常用函数使用 |
三、业务流程

四、操作流程

五、数据建模
5.1、【款项类型】枚举
名称: |
款项类型 |
编码: |
quickType |
类型: |
字符 |
枚举内容: |
枚举值 |
名称 |
|
5 |
预付款 |
|
6 |
应付款 |
5.2、【是否同步】枚举
名称: |
是否同步 |
编码: |
isSync |
类型: |
字符 |
枚举内容: |
枚举值 |
名称 |
|
1 |
是 |
|
2 |
否 |
5.3、【预付款申请】实体
操作 |
描述 |
|
|
|
|
|
名称 |
预付款申请 |
|
|
|
|
|
编码 |
prepayment |
|
|
|
|
|
父实体 |
|
|
|
|
|
|
引用接口 |
审批 |
业务流 |
自动编码 |
交易类型 |
|
|
|
属性 |
编码 |
名称 |
类型 |
引用 |
|
|
|
vouchdate |
单据日期 |
日期 |
|
|
|
|
accentity |
会计主体 |
单选引用 |
会计主体 |
bd.adminOrg.FinanceOrgVO |
|
|
oriSum |
付款金额 |
数值 |
|
|
|
|
natSum |
本币金额 |
数值 |
|
|
|
|
balance |
余额 |
数值 |
|
|
|
|
supplier |
供应商 |
单选引用 |
供应商 |
aa.vendor.Vendor |
|
|
supplierbankname |
收款银行 |
文本 |
|
|
|
|
supplierbankaccount_accountname |
收款银行账户 |
文本 |
|
|
|
|
period |
会计期间 |
单选引用 |
会计期间 |
bd.period.Period |
|
|
settlemode |
结算方式 |
单选引用 |
结算方式 |
aa.settlemethod.SettleMethod |
|
|
currency |
币种 |
单选引用 |
币种 |
bd.currencytenant.CurrencyTenantVO |
|
|
currency_priceDigit |
币种单价精度 |
数值 |
|
|
|
|
currency_moneyDigit |
币种金额精度 |
数值 |
|
|
|
|
natCurrency |
本币币种 |
单选引用 |
币种 |
bd.currencytenant.CurrencyTenantVO |
|
|
natCurrency_priceDigit |
本币币种单价精度 |
数值 |
|
|
|
|
natCurrency_moneyDigit |
本币币种金额精度 |
数值 |
|
|
|
|
exchRate |
汇率 |
数值 |
|
|
|
|
dept |
部门 |
单选引用 |
部门 |
bd.adminOrg.AdminOrgVO |
|
|
operator |
业务员 |
单选引用 |
员工 |
bd.staff.StaffNew |
|
|
exchangeRateType_digit |
汇率类型金额精度 |
数值 |
|
|
|
|
exchangeRateType |
汇率类型 |
单选引用 |
汇率类型 |
bd.exchangeRate.ExchangeRateTypeVO |
|
|
project |
项目 |
单选引用 |
项目 |
bd.project.ProjectVO |
|
|
enterprisebankaccount |
付款银行账户id |
单选引用 |
企业银行账户 |
bd.enterprise.OrgFinBankacctVO |
|
|
description |
备注 |
文本 |
|
|
|
|
noteno |
票据号 |
文本 |
|
|
|
|
org |
采购组织 |
单选引用 |
采购组织 |
bd.adminOrg.PurchaseOrgVO |
|
|
cashaccount |
付款现金账户 |
单选引用 |
企业现金账户 |
bd.enterprise.OrgFinCashacctVO |
|
|
isSync |
是否同步 |
单选 |
是否同步 |
|
|
|
orderno |
订单编号 |
文本 |
|
|
|
|
sourcebillnum |
原单金额 |
数值 |
|
|
5.4、【预付款支付明细】实体
操作 |
描述 |
|
|
|
|
|
名称 |
预付款支付明细 |
|
|
|
|
|
编码 |
prepaybillb |
|
|
|
|
|
父实体 |
预付款申请 |
|
|
|
|
|
引用接口 |
业务流 |
|
|
|
|
|
|
属性 |
编码 |
名称 |
类型 |
引用 |
|
|
|
expenseitem |
费用项目 |
单选引用 |
费用项目 |
bd.expenseitem.ExpenseItem |
|
|
oriSum |
金额 |
数值 |
|
|
|
natSum |
本币金额 |
数值 |
|
|
|
customer_code |
客户编码 |
文本 |
|
|
|
customer |
客户 |
单选引用 |
客户档案 |
aa.merchant.Merchant |
|
|
supplier |
供应商 |
单选引用 |
供应商 |
aa.vendor.Vendor |
|
|
employee |
员工 |
单选引用 |
员工 |
bd.staff.StaffNew |
|
|
dept |
部门 |
单选引用 |
部门 |
bd.adminOrg.AdminOrgVO |
|
|
project |
项目 |
单选引用 |
项目 |
bd.project.ProjectVO |
|
|
orderno |
订单编号 |
文本 |
|
|
|
description |
备注 |
文本 |
|
|
|
product |
物料 |
单选引用 |
物料 |
pc.product.Product |
|
|
quickType_code |
款项类型 |
单选 |
款项类型 |
|
六、页面建模

- 调整【预付款申请列表】页面布局;显示样式调整不必完全一致

- 调整【预付款申请】页面布局;显示样式调整不必完全一致



- 配置交易类型,设置默认值为采购付款,选择第一个即可


七、【采购订单】下推【预付款申请】业务流
7.1、创建预付款申请业务流


- 选择领域单据-【采购订单】拖进中间操作台
- 选择应用构建-【预付款申请】拖进中间操作台
- 选择应用构建【采购订单】流程指向【预付款申请】


- 【基本信息】-【规则名称】: 预付款
- 【触发设置】-【推送时机】: 手工触发
- 【生单方式】如下图

- 【规则转换】如下图; 注意此处只是简单的配置了一下转换,并不代表真正的业务逻辑


7.2、【预付款申请】保存前校验

let AbstractAPIHandler = require('AbstractAPIHandler');
class MyAPIHandler extends AbstractAPIHandler {
execute(request){
var orderno = request.orderno;
var sql = "select oriSum from GT12951AT32.GT12951AT32.prepaymentnishch where orderno = '"+orderno+"'";
var res = ObjectStore.queryByYonQL(sql);
return {res};
}
}
exports({"entryPoint":MyAPIHandler});

function (event) {
var viewModel = this;
viewModel.on('beforeSave',function(){
var sourcebillnum = viewModel.get('sourcebillnum').getValue();
var oriSum = viewModel.get('oriSum').getValue();
if(oriSum>=sourcebillnum){
cb.utils.alert("付款金额要小于原单金额");
return false;
}
var orderno = viewModel.get('orderno').getValue();
var returnPromise = new cb.promise();
cb.rest.invokeFunction("GT12951AT32.prepay.prepaylist", {orderno},
function(err, res) {
if(err!=null){
cb.utils.alert(JSON.stringify(err));
returnPromise.reject();
}else{
console.log(res);
var arrData = res.res;
var finallyValue = 0 ;
for(let i in arrData){
if(arrData[i].oriSum!=undefined)finallyValue +=arrData[i].oriSum;
}
if(oriSum>=sourcebillnum-finallyValue){
cb.utils.alert("累计生成的预付款单金额要小于原单金额");
returnPromise.reject();
}else{
returnPromise.resolve();
}
}
})
return returnPromise;
})
}
7.3、设置金额联动

function (event) {
var viewModel = this;
var gridModel = viewModel.getGridModel();
if(event.params.cellName=="oriSum"){
var value = event.params.value;
gridModel.setCellValue(event.params.rowIndex,"natSum",value);
viewModel.get("oriSum").setValue(value);
viewModel.get("natSum").setValue(value);
viewModel.get("balance").setValue(value);
}
}
7.4、测试【采购订单】下推【预付款申请】
- 打开【采购订单】节点,选择一条数据,点击下推,选择自己配置的业务流即可 【采购订单】为原厂单据,此处暂不做限制及校验




八、【预付款申请】生成【付款】单
目前财务领域未提供【领域单据】扩展方式,因此需要转换思路,我们可以通过调用【开放平台】接口实现功能
8.1、 创建API公共函数,作为配置文件,节省环境迁移成本

let AbstractAPIHandler = require('AbstractAPIHandler');
class MyAPIHandler extends AbstractAPIHandler {
execute(request){
var config = {
"appKey":"827e9923533e41787456c74b9ae54a8",
"appSecret":"9bdc5f2814914cab81561759a8acfd0",
"baseUrl":"https://open-api-dbox.yyuap.com"
}
return {config};
}
}
exports({"entryPoint":MyAPIHandler});
8.2、 创建API公共函数,获取开放平台token

新增API-用友开放API-选择付款相关接口

let AbstractAPIHandler = require('AbstractAPIHandler');
class MyAPIHandler extends AbstractAPIHandler {
execute(request){
let configfun = extrequire("GT12951AT32.config.baseConfig");
let config = configfun.execute(request);
var access_token = getToken(config.config.appKey,config.config.appSecret,config.config.baseUrl);
function getToken(yourappkey,yourappsecrect,baseurl) {
var access_token;
const token_url = baseurl+"/open-auth/selfAppAuth/getAccessToken";
const appkey = yourappkey;
const appsecrect = yourappsecrect;
let timestamp = new Date().getTime();
const secrectdata = 'appKey' + appkey + 'timestamp' + timestamp;
var CryptoJS = CryptoJS || function (h, i) {
var e = {},
f = e.lib = {},
l = f.Base = function () {
function a() {}
return {
extend: function (j) {
a.prototype = this;
var d = new a;
j && d.mixIn(j);
d.$super = this;
return d
},
create: function () {
var a = this.extend();
a.init.apply(a, arguments);
return a
},
init: function () {},
mixIn: function (a) {
for (var d in a) a.hasOwnProperty(d) && (this[d] = a[d]);
a.hasOwnProperty("toString") && (this.toString = a.toString)
},
clone: function () {
return this.$super.extend(this)
}
}
}(),
k = f.WordArray = l.extend({
init: function (a, j) {
a =
this.words = a || [];
this.sigBytes = j != i ? j : 4 * a.length
},
toString: function (a) {
return (a || m).stringify(this)
},
concat: function (a) {
var j = this.words,
d = a.words,
c = this.sigBytes,
a = a.sigBytes;
this.clamp();
if (c % 4)
for (var b = 0; b < a; b++) j[c + b >>> 2] |= (d[b >>> 2] >>> 24 - 8 * (b % 4) & 255) << 24 - 8 * ((c + b) % 4);
else if (65535 < d.length)
for (b = 0; b < a; b += 4) j[c + b >>> 2] = d[b >>> 2];
else j.push.apply(j, d);
this.sigBytes += a;
return this
},
clamp: function () {
var a = this.words,
b = this.sigBytes;
a[b >>> 2] &= 4294967295 << 32 - 8 * (b % 4);
a.length = h.ceil(b / 4)
},
clone: function () {
var a =
l.clone.call(this);
a.words = this.words.slice(0);
return a
},
random: function (a) {
for (var b = [], d = 0; d < a; d += 4) b.push(4294967296 * h.random() | 0);
return k.create(b, a)
}
}),
o = e.enc = {},
m = o.Hex = {
stringify: function (a) {
for (var b = a.words, a = a.sigBytes, d = [], c = 0; c < a; c++) {
var e = b[c >>> 2] >>> 24 - 8 * (c % 4) & 255;
d.push((e >>> 4).toString(16));
d.push((e & 15).toString(16))
}
return d.join("")
},
parse: function (a) {
for (var b = a.length, d = [], c = 0; c < b; c += 2) d[c >>> 3] |= parseInt(a.substr(c, 2), 16) << 24 - 4 * (c % 8);
return k.create(d, b / 2)
}
},
q = o.Latin1 = {
stringify: function (a) {
for (var b =
a.words, a = a.sigBytes, d = [], c = 0; c < a; c++) d.push(String.fromCharCode(b[c >>> 2] >>> 24 - 8 * (c % 4) & 255));
return d.join("")
},
parse: function (a) {
for (var b = a.length, d = [], c = 0; c < b; c++) d[c >>> 2] |= (a.charCodeAt(c) & 255) << 24 - 8 * (c % 4);
return k.create(d, b)
}
},
r = o.Utf8 = {
stringify: function (a) {
try {
return decodeURIComponent(escape(q.stringify(a)))
} catch (b) {
throw Error("Malformed UTF-8 data");
}
},
parse: function (a) {
return q.parse(unescape(encodeURIComponent(a)))
}
},
b = f.BufferedBlockAlgorithm = l.extend({
reset: function () {
this._data = k.create();
this._nDataBytes = 0
},
_append: function (a) {
"string" == typeof a && (a = r.parse(a));
this._data.concat(a);
this._nDataBytes += a.sigBytes
},
_process: function (a) {
var b = this._data,
d = b.words,
c = b.sigBytes,
e = this.blockSize,
g = c / (4 * e),
g = a ? h.ceil(g) : h.max((g | 0) - this._minBufferSize, 0),
a = g * e,
c = h.min(4 * a, c);
if (a) {
for (var f = 0; f < a; f += e) this._doProcessBlock(d, f);
f = d.splice(0, a);
b.sigBytes -= c
}
return k.create(f, c)
},
clone: function () {
var a = l.clone.call(this);
a._data = this._data.clone();
return a
},
_minBufferSize: 0
});
f.Hasher = b.extend({
init: function () {
this.reset()
},
reset: function () {
b.reset.call(this);
this._doReset()
},
update: function (a) {
this._append(a);
this._process();
return this
},
finalize: function (a) {
a && this._append(a);
this._doFinalize();
return this._hash
},
clone: function () {
var a = b.clone.call(this);
a._hash = this._hash.clone();
return a
},
blockSize: 16,
_createHelper: function (a) {
return function (b, d) {
return a.create(d).finalize(b)
}
},
_createHmacHelper: function (a) {
return function (b, d) {
return g.HMAC.create(a, d).finalize(b)
}
}
});
var g = e.algo = {};
return e
}(Math);
(function (h) {
var i = CryptoJS,
e = i.lib,
f = e.WordArray,
e = e.Hasher,
l = i.algo,
k = [],
o = [];
(function () {
function e(a) {
for (var b = h.sqrt(a), d = 2; d <= b; d++)
if (!(a % d)) return !1;
return !0
}
function f(a) {
return 4294967296 * (a - (a | 0)) | 0
}
for (var b = 2, g = 0; 64 > g;) e(b) && (8 > g && (k[g] = f(h.pow(b, 0.5))), o[g] = f(h.pow(b, 1 / 3)), g++), b++
})();
var m = [],
l = l.SHA256 = e.extend({
_doReset: function () {
this._hash = f.create(k.slice(0))
},
_doProcessBlock: function (e, f) {
for (var b = this._hash.words, g = b[0], a = b[1], j = b[2], d = b[3], c = b[4], h = b[5], l = b[6], k = b[7], n = 0; 64 >
n; n++) {
if (16 > n) m[n] = e[f + n] | 0;
else {
var i = m[n - 15],
p = m[n - 2];
m[n] = ((i << 25 | i >>> 7) ^ (i << 14 | i >>> 18) ^ i >>> 3) + m[n - 7] + ((p << 15 | p >>> 17) ^ (p << 13 | p >>> 19) ^ p >>> 10) + m[n - 16]
}
i = k + ((c << 26 | c >>> 6) ^ (c << 21 | c >>> 11) ^ (c << 7 | c >>> 25)) + (c & h ^ ~c & l) + o[n] + m[n];
p = ((g << 30 | g >>> 2) ^ (g << 19 | g >>> 13) ^ (g << 10 | g >>> 22)) + (g & a ^ g & j ^ a & j);
k = l;
l = h;
h = c;
c = d + i | 0;
d = j;
j = a;
a = g;
g = i + p | 0
}
b[0] = b[0] + g | 0;
b[1] = b[1] + a | 0;
b[2] = b[2] + j | 0;
b[3] = b[3] + d | 0;
b[4] = b[4] + c | 0;
b[5] = b[5] + h | 0;
b[6] = b[6] + l | 0;
b[7] = b[7] + k | 0
},
_doFinalize: function () {
var e = this._data,
f = e.words,
b = 8 * this._nDataBytes,
g = 8 * e.sigBytes;
f[g >>> 5] |= 128 << 24 - g % 32;
f[(g + 64 >>> 9 << 4) + 15] = b;
e.sigBytes = 4 * f.length;
this._process()
}
});
i.SHA256 = e._createHelper(l);
i.HmacSHA256 = e._createHmacHelper(l)
})(Math);
(function () {
var h = CryptoJS,
i = h.enc.Utf8;
h.algo.HMAC = h.lib.Base.extend({
init: function (e, f) {
e = this._hasher = e.create();
"string" == typeof f && (f = i.parse(f));
var h = e.blockSize,
k = 4 * h;
f.sigBytes > k && (f = e.finalize(f));
for (var o = this._oKey = f.clone(), m = this._iKey = f.clone(), q = o.words, r = m.words, b = 0; b < h; b++) q[b] ^= 1549556828, r[b] ^= 909522486;
o.sigBytes = m.sigBytes = k;
this.reset()
},
reset: function () {
var e = this._hasher;
e.reset();
e.update(this._iKey)
},
update: function (e) {
this._hasher.update(e);
return this
},
finalize: function (e) {
var f =
this._hasher,
e = f.finalize(e);
f.reset();
return f.finalize(this._oKey.clone().concat(e))
}
})
})();
function Base64stringify (wordArray) {
var words = wordArray.words;
var sigBytes = wordArray.sigBytes;
var map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
wordArray.clamp();
var base64Chars = [];
for (var i = 0; i < sigBytes; i += 3) {
var byte1 = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
var byte2 = (words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff;
var byte3 = (words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff;
var triplet = (byte1 << 16) | (byte2 << 8) | byte3;
for (var j = 0; (j < 4) && (i + j * 0.75 < sigBytes); j++) {
base64Chars.push(map.charAt((triplet >>> (6 * (3 - j))) & 0x3f));
}
}
var paddingChar = map.charAt(64);
if (paddingChar) {
while (base64Chars.length % 4) {
base64Chars.push(paddingChar);
}
}
return base64Chars.join('');
}
var sha256 = CryptoJS.HmacSHA256(secrectdata, appsecrect);
const base64 = Base64stringify(sha256);
const signature = encodeURIComponent(base64);
const requestUrl = token_url + '?appKey=' + appkey + '×tamp=' + timestamp + '&signature=' + signature;
const header = {
'Content-Type': 'application/json'
}
var strResponse = postman("GET", requestUrl,JSON.stringify(header),null);
var responseObj = JSON.parse(strResponse);
if("00000"==responseObj.code){
access_token = responseObj.data.access_token;
}
return access_token;
}
return {"access_token":access_token};
}
}
exports({"entryPoint":MyAPIHandler});
8.2、【开放平台】接口【付款保存】

let AbstractAPIHandler = require('AbstractAPIHandler');
class MyAPIHandler extends AbstractAPIHandler {
execute(request){
var main = request.params.billData;
main._status = "Insert";
var ch = request.params.billData.prepaybillb0526List;
for(let i in ch){
ch[i]._status = "Insert";
}
main.PayBillb = ch;
delete main.prepaybillb0526List;
var apiData = {"data":[main]};
let func1 = extrequire("GT10894AT82.common.getOpenApiToken");
let res = func1.execute(request);
let configfun = extrequire("GT10894AT82.common.config");
let config = configfun.execute(request);
var token = res.access_token;
var requrl = config.config.baseUrl+"/yonbip/fi/payment/save?access_token="+token;
var header = {'Content-Type':"application/json;charset=UTF-8"};
var strResponse = postman("POST", requrl,JSON.stringify(header),JSON.stringify(apiData));
var responseObj = JSON.parse(strResponse);
if(responseObj.data.failCount==0){
var object = {id:main.id,isSync:"1"};
ObjectStore.updateById("GT10894AT82.GT10894AT82.prepayment",object,"338ec443");
}
return {responseObj};
}
}
exports({"entryPoint":MyAPIHandler});
8.3、【预付款申请】卡片页面创建【付款】按钮


function (event) {
var viewModel = this;
var state = viewModel.get("verifystate").getValue();
if(state!=2){
cb.utils.alert("单据未审批!")
return false;
}
var isSync = viewModel.get("isSync").getValue();
if(isSync=="1"){
cb.utils.alert("不能重复付款!")
return false;
}
cb.rest.invokeFunction("GT10894AT82.prepayment.prepayment", {params:viewModel.getParams()},
function(err, res) {
if(err!=null){
cb.utils.alert(err);
}else{
cb.utils.alert("下推成功!");
viewModel.get("isSync").setValue("1");
}
})
}
8.3、根据单据状态控制【付款】按钮显示隐藏
- 修改初始化函数-新增控制按钮功能:审批通过后的单据【付款】按钮显示,否则不显示

viewModel.on("afterLoadData",function(){
var verifystate = viewModel.get("verifystate").getValue();
if(verifystate!="2"){
viewModel.get("button83zf").setVisible(false);
}else{
viewModel.get("button83zf").setVisible(true);
}
})

九、测试

