分类 K3Cloud 下的文章

summary-icon摘要由AI智能服务提供

本文简要介绍了如何使用Python脚本开发简单账表的取数服务插件。文章首先概述了简单账表的概念及其相对于直接SQL账表的优势,然后详细阐述了简单账表的基本开发步骤,包括新建简单账表、添加表头字段、开发取数服务插件等。接着介绍了账表服务插件的关键成员和主要事件,并通过示例代码展示了如何构建报表SQL和临时表。

有用反馈


往期回顾:

【Python插件入门】第1篇:Python插件入门讲解

【Python插件入门】第2篇:基本开发过程介绍

【Python插件入门】第3篇:插件中如何进行数据操作

【Python插件入门】第4篇:单据表单插件

【Python插件入门】第5篇:单据列表插件

【Python插件入门】第6篇:操作服务插件


    前面我们讲了服务端允许的操作服务插件,今天我们来了解一下简单账表的开发,简单账表需要通过取数服务插件来构建报表数据源,是简单账表开发的一个核心,而对于公有云发布.NET插件不方便,Python脚本支持开发简单账表服务插件,可以更加方便快速开发自定义报表。

    今天分享一下如何用Python脚本开发简单账表的取数服务插件。我测试过的最老的版本是V7.6.0.202103,比较老的版本可能会有问题,但是高于此版本的环境应该都是可以的。

一、简单账表简介

    账表分为直接SQL账表、简单账表、树形账表、分页账表,直接SQL账表其实已经可以实现我们的很多个性化报表需求,这种类型的报表开发核心只需要写SQL语句就可以快速完整报表的开发,但还是有一些局限性,例如,复杂的过滤条件、更灵活的数据源构建、动态列报表等。

    简单账表可以开发服务取数插件,来实现灵活的组装报表数据,非常实用,同时可以配合自定义的过滤界面来实现报表查询,在权限控制上面也会更加灵活。但相对直接SQL账表,开发步骤比较多,并需要编写插件代码,初学者掌握起来有点难度,但是我们必须突破这一关,掌握了简单账表的插件开发,在此基础上开发树形账表、分页账表也不是难事。

二、简单账表基本开发步骤

    在账表开发过程中,账表引擎只负责把账表服务端插件取到的数据绑定到设计好的账表模型上,账表开发基本步骤如下:

    ①新建->空白对象->简单账表。

    ②如果需要的话:添加表头字段。添加报表字段(通常都是在取数服务插件中构建,可以不做)

    ③新建过滤界面继承->BOS->应用框架->动态表单->公共过滤

    ④开发取数服务插件,看后面的Python脚本参考代码。核心的报表逻辑都在这一步。

    ⑤账表表单插件开发(可选):设置颜色、单元格数据格式等。(下一篇会对账表表单插件做介绍)

    ⑥权限配置,主控台菜单-账表发布。

 不过多介绍详细的开发步骤,社区已经有很多案例,推荐大家看看@拿了你的糖 老师的文章:账表开发超详细实现步骤

   我这里示例,已经在BOS建好相关的过滤框对象简单账表界面,大家参考上面的文章操作即可。

  • 简单账表Python服务插件注册如下图,可以将附件中的Python示例代码复制进去。
    (注意:与其他插件类型不同,简单账表服务插件,只能启用一个!)

    image.png


三、简单账表服务插件介绍

    账表服务插件是整个简单账表开发的核心过程,在插件中需要通过SQL组装,构建出最终账表数据的临时表,作为账表查询的最终数据源,账表引擎负责把临时表的数据显示到账表页面上,在账表服务插件中,还可以动态构建账表的列头、构建表头显示的字段、设置分组小计、合计等。

    在C#插件开发中,账表服务插件的基类是SysReportBaseService,账表服务插件与前面讲到的操作服务插件类似,也是一条"流水线作业",在不同的环节完成不同步骤,通常我们只需要实现几个关键的事件就可以开发出我们需要的个性化报表了,实现一次之后就可以按照这个模式开发各种报表了,这基本也能满足大部分的报表开发需求了。

    下面我们来看一下账表服务插件中的一些关键成员。

    账表服务插件中同样有this.Context,这个在前面的篇章中介绍了,用法类似,不再过多介绍。

  • this.ReportProperty:账表属性,包含账表开发过程中的全局信息。

    this.ReportProperty.ReportType;#账表类型,默认是简单账表,通常在初始化Initialize事件中对此属性赋值;
    #0: ReportType.REPORTTYPE_NORMAL 普通简单账表,1:ReportType.REPORTTYPE_TREE 树形账表,2:ReportType.REPORTTYPE_MOVE 分页账表

       this.ReportProperty.IsGroupSummary;#是否支持分组汇总
       this.ReportProperty.IsUIDesignerColumns;#账表列头是否是通过BOSIDE设计,默认False,

       #为True时,可在BOS中拖报表列字段(字段名与临时表字段名对应),而不在GetReportHeaders中构建

       this.ReportProperty.IdentityFieldName="FIDENTITYID";#可理解为临时表的主键,默认字段名是FIDENTITYID
       #FIDENTITYID字段是从1开始的整数序列,临时表中一定要有FIDENTITYID字段,

       #且必须从1开始、不能重复,不能跳号。否则报表页面会显示空白。

       this.ReportProperty.DecimalControlFieldList;#精度控制字段信息,预设数值字段的精度
       lstDcf=List[DecimalControlField]();
       dcf=DecimalControlField();
       dcf.ByDecimalControlFieldName="显示的字段名";
       dcf.DecimalControlFieldName = "用于控制精度的字段名";
       lstDcf.Add(dcf);
       this.ReportProperty.DecimalControlFieldList=lstDcf;

       this.ReportProperty.DspInsteadColumnsInfo;#列表格式化列,指示Key列被Value列内容替代
       this.ReportProperty.DspInsteadColumnsInfo.DefaultDspInsteadColumns.Add("列字段""显示字段");

       this.ReportProperty.GroupSummaryInfoData;#分组汇总信息

  • this.ReportTitles:账表表头字段信息,通常在GetReportTitles对表头字段进行传值

  • this.IsCreateTempTableByPlugin:报表是否调用BuilderReportSqlAndTempTable创建临时表,默认True

  • this.SummarySpecialFields:汇总字段信息

  • this.TempTableNameList:临时表列表

四、简单账表服务插件主要事件介绍

  • 账表服务插件通常实现7个关键事件

    初始化、临时表构造、构建报表列头、报表合计列(可选)、合计列计算逻辑(可选)、报表表头字段(可选)、报表关闭

    这里示例开发了一个简单的采购日报表,按照日期横向展示每天的订单数量,实现效果如下图:

image.png

image.png

  • 下面看下简单账表服务插件中主要的事件介绍:

#初始化,在此事件中设置报表的属性全局参数
def Initialize():
    this.ReportProperty.ReportType=ReportType.REPORTTYPE_NORMAL;
    this.IsCreateTempTableByPlugin=True;
    #是否支持分组汇总,在后面GetSummaryColumnInfo方法中添加汇总字段,
    #要在BOS中过滤框的汇总页签配置汇总信息,可参考:简单账表分组汇总设置
    this.ReportProperty.IsGroupSummary=True;
    #IsUIDesignerColumns=False,表示报表的列通过插件控制,后续在GetReportHeaders中构建列头
    #需要在BOS过滤框的显示隐藏列中维护,字段标识与临时表字段保持一致
    #账表列头构建更多详细说明参考:账表列构建
    this.ReportProperty.IsUIDesignerColumns=False;

#创建临时报表,正式进入账表取数sql拼接并取数,把账表取数结果放到创建的临时表中
#如果参数(this.IsCreateTempTableByPlugin=True),即调用BuilderReportSqlAndTempTable构建临时表
#否则调用以下3个接口,完成账表取数逻辑的sql指令即:BuilderSelectFieldSQL、BuilderTempTableOrderBySQL、BuilderFormWhereSQL
#rptfilter:账表参数,可以从这里获取过滤条件、排序字段、显示隐藏列等
#tableName:系统自动创建的账表临时表名,具备唯一性,最终报表页面展示的数据绑定此临时表,所以最终的报表结果数据要写入此临时表中
def BuilderReportSqlAndTempTable(rptfilter,tableName):
    #baseDataTemp=filter.BaseDataTempTable;#基础资料临时表;若设置了数据范围权限,该表会把根据数据范围过滤出来的内码存入临时表;
    #循环获取所有基础资料数据范围的数据,可用来拼接到报表SQL里面实现数据权限过滤
    #for b in baseDataTemp:
    #    baseType=b.BaseDataFormId;#基础资料FormId
    #    PKFldName=b.PKFieldName;#临时表中基础资料主键字段名,例如,FORGID
    #    baseTempTab=b.TempTable;#基础资料数据范围临时表名
    #filterStr=filter.FilterParameter.FilterString;#过滤框条件页签过滤表达式
    #过滤框快捷过滤页签的实体数据包,从这里面获取自定义的过滤字段值
    #DynamicObject类型,用前面讲的实体数据包操作方式取值,用绑定实体属性标识
    custFilter = rptfilter.FilterParameter.CustomFilter;
    if(custFilter==None):
        return;
    orgObj=custFilter["F_BPW_OrgId"];#获取组织
    whereOrgs="";
    if(orgObj<>None):
        orgId=("{0}").format(orgObj["Id"]);#组织ID
        whereOrgs=(" and a.FPURCHASEORGID in ({0}) ").format(orgId);#选择了组织,拼接组织过滤
    materials=custFilter["F_BPW_Materials"];#物料多选过滤
    matList=List[str]();
    if(materials<>None):
        for m in materials:
            materialNum="'"+str(m["F_BPW_Materials"]["Number"])+"'";#取出过滤框选择的多个物料编码
            matList.Add(materialNum);
    whereMat=(" and m.FNumber in ({0})").format(str.Join(",",matList)) if(matList.Count>0else "";#拼接物料多选过滤
    beginDate=str(custFilter["F_BPW_BeginDate"]);#获取开始日期
    EndDate=str(custFilter["F_BPW_EndDate"]);#获取结束日期
    beginDate=str(DateTime.Parse(("{0}").format(beginDate)).AddDays(-1));#开始日期的前一天
    itemDate=beginDate;
    fldsSql=List[str]();#动态列头SQL,相当于Select后面的部分字段是动态拼接的,以此来实现动态列
    #从开始日期起,循环加1天,一直到结束日期,过滤界面要控制录入的开始日期必须<结束日期,在过滤界面注册表单插件即可实现
    while(itemDate<>EndDate):
        myDate=DateTime.Parse(("{0}").format(itemDate));
        fldKey=("F{0}_{1}_{2}").format(myDate.Year,myDate.Month,myDate.Day);#根据每个日期构建唯一的列名
        ss=("{0}=SUM(Case when a.FDATE='{1}' then b.FQty else 0 end)").format(fldKey,myDate);#构建每天订单数量合计SQL
        fldsSql.Add(ss);#将SQL添加到动态列SQL集合中备用
        nextDate=myDate.AddDays(1);
        itemDate=str(nextDate);#迭代+1天
    myDate=DateTime.Parse(("{0}").format(itemDate));#这里是选择的截止日期,由于循环到最后一天跳出了,这里补充一天的数据
    fldKey=("F{0}_{1}_{2}").format(myDate.Year,myDate.Month,myDate.Day);
    ss=("{0}=SUM(Case when a.FDATE='{1}' then b.FQty else 0 end)").format(fldKey,myDate);
    fldsSql.Add(ss);#将SQL添加到动态列SQL集合中备用
    #raise Exception(str.Join(',',fldsSql));#调试时,可用此行代码,看看构建的动态列SQL对不对
    #组装最终写入报表临时表的SQL
    #注意!!!: 最终临时表一定要有FIDENTITYID ,要从1开始,且不重复 ,不断号,不然前台显示空白!!!!
    sql=("""/*dialect*/ select row_Number() Over(order by a.orgName,a.MaterialNum)  FIDENTITYID, a.* 
into {0} 
from 
(select  orgL.FNAME orgName,m.FNUMBER MaterialNum,mL.FNAME materialName,mL.FSPECIFICATION SpecNum,unitL.FNAME unit,
{1}
from t_PUR_POOrder a
inner join T_ORG_ORGANIZATIONS org on org.FORGID=a.FPURCHASEORGID
inner join T_ORG_ORGANIZATIONS_L orgL on orgL.FORGID=org.FORGID and orgL.FLOCALEID=2052
inner join t_PUR_POOrderEntry b on a.FID=b.FID
inner join T_BD_MATERIAL m on m.FMATERIALID=b.FMATERIALID
inner join T_BD_MATERIAL_L mL on mL.FMATERIALID=m.FMATERIALID and mL.FLOCALEID=2052
inner Join T_BD_UNIT_L unitL on unitL.FUNITID=b.FUNITID and  unitL.FLOCALEID=2052
where a.FDATE>='{3}' and a.FDATE<='{4}' {2} {5}
group by orgL.FNAME,m.FNUMBER,mL.FNAME,mL.FSPECIFICATION,unitL.FNAME
) a 
  """).format(tableName,str.Join(',',fldsSql),whereOrgs,beginDate,EndDate,whereMat);
      #raise Exception(sql);#可以通过此方法弹出Sql语句进行调试验证
    DBUtils.Execute(this.Context,sql);#执行SQL,将报表数据写入临时表

#构建账表列头
def GetReportHeaders(Filter):
    header=ReportHeader();
    localEid=this.Context.UserLocale.LCID;#获取当前语言环境代码,中文为2052
    header.AddChild("orgName",LocaleValue("采购组织",localEid));#字段名,列头标题,字段名与临时表中的字段名保持对应,相当于每一个列头对应临时表的哪个字段
    header.AddChild("MaterialNum",LocaleValue("物料编码",localEid));
    header.AddChild("materialName",LocaleValue("物料名称",localEid));
    header.AddChild("SpecNum",LocaleValue("规格型号",localEid));
    header.AddChild("unit",LocaleValue("采购单位",localEid));
    
    #下面根据过滤条件选择的日期区间,动态构建列头,和上面构建SQL字段的逻辑类似
    custFilter = Filter.FilterParameter.CustomFilter;
    beginDate=str(custFilter["F_BPW_BeginDate"]);#获取开始日期
    EndDate=str(custFilter["F_BPW_EndDate"]);#获取结束日期
    itemDate=beginDate;
    fldsSql=List[str]();
    while(itemDate<>EndDate):
        myDate=DateTime.Parse(("{0}").format(itemDate));
        fldKey=("F{0}_{1}_{2}").format(myDate.Year,myDate.Month,myDate.Day);#这里的字段名要和前面构建的SQL字段对应
        header.AddChild(fldKey,LocaleValue(str(("{0}-{1}-{2}").format(myDate.Year,myDate.Month,myDate.Day)),localEid),SqlStorageType.SqlDecimal);
        nextDate=myDate.AddDays(1);
        itemDate=str(nextDate);
    myDate=DateTime.Parse(("{0}").format(itemDate));
    fldKey=("F{0}_{1}_{2}").format(myDate.Year,myDate.Month,myDate.Day);#这里的字段名要和前面构建的SQL字段对应
    header.AddChild(fldKey,LocaleValue(str(("{0}-{1}-{2}").format(myDate.Year,myDate.Month,myDate.Day)),localEid),SqlStorageType.SqlDecimal);
#设置列的索引,使其可以按照以上列头构建的顺序显示
    colIndex=0;
    for child in header.GetChilds():
        if(child.GetChildCount()==0):
            child.ColIndex=colIndex;
            colIndex=colIndex+1;
        else:
            child.ColIndex = colIndex;
            colIndex=colIndex+1;
            for childHeader in child.GetChilds():
                childHeader.ColIndex=colIndex;
                colIndex=colIndex+1;
    return header;

#设置报表表头字段值
#这里主要是把过滤框设置的字段值,显示到报表表头
#注意:这里只能以文本的形式传递到字段上,不能传递基础资料
#若要在表头按照基础资料的形式展示,可以参考:简单账表表单插件中启用表单服务插件中定义的Ttile
def GetReportTitles(Filter):
    reportTitles=ReportTitles();
    custFilter=Filter.FilterParameter.CustomFilter;#获取过滤框的数据包
    orgObj=custFilter["F_BPW_OrgId"];#获取组织
    beginDate=str(custFilter["F_BPW_BeginDate"]);#获取开始日期
    EndDate=str(custFilter["F_BPW_EndDate"]);#获取结束日期
    if(orgObj<>None):
        reportTitles.AddTitle("F_BPW_OrgId", orgObj["Name"]);
    reportTitles.AddTitle("F_BPW_BeginDate", beginDate);
    reportTitles.AddTitle("F_BPW_EndDate", EndDate);
    return reportTitles;

#设置报表底部合计列
def GetSummaryColumnInfo(rptfilter):
    result=List[SummaryField]();
    #由于这里的数量字段是动态构建的,所以也需要动态添加合计列,字段名与前面保持一致
    custFilter = rptfilter.FilterParameter.CustomFilter;
    beginDate=str(custFilter["F_BPW_BeginDate"]);#获取开始日期
    EndDate=str(custFilter["F_BPW_EndDate"]);#获取结束日期
    itemDate=beginDate;
    fldsSql=List[str]();
    while(itemDate<>EndDate):
        myDate=DateTime.Parse(("{0}").format(itemDate));
        fldKey=("F{0}_{1}_{2}").format(myDate.Year,myDate.Month,myDate.Day);#这里的字段名要和前面构建的SQL字段对应
        result.Add(SummaryField(fldKey,BOSEnums.Enu_SummaryType.SUM));
        nextDate=myDate.AddDays(1);
        itemDate=str(nextDate);
    myDate=DateTime.Parse(("{0}").format(itemDate));
    fldKey=("F{0}_{1}_{2}").format(myDate.Year,myDate.Month,myDate.Day);
    result.Add(SummaryField(fldKey,BOSEnums.Enu_SummaryType.SUM));
    return result;

#汇总字段计算逻辑,如果不是单纯的sum(xxx),可以在此方法中处理,本示例未作处理

!!!这个GetSummaryColumsSQL事件返回的内容,最终会拼接到 Select后面,不能return ""

!!!注意:此方法,仅支持2024.11.21及后面的版本

以前的版本,Python插件不会触发GetSummaryColumsSQL,所以一直没报错,以前按照这篇文章编写的代码可能会引发如下图的报错:

相关报错问答:升级至9.0.0.20250109版本后,python脚本二开简单账表都出来异常

image.png

如果不需要干预合并逻辑,那个不要使用这个事件,把该事件的代码删除,即可解决此报错!

如果需要使用这个事件可以参考这个:简单账表Python插件支持GetSummaryColumsSQL方法

#def GetSummaryColumsSQL(summaryFields):

#报表关闭触发,通常在此处清理报表过程产生的临时表
def CloseReport():
    this.DropTempTable();


==========================本篇正文结束=====================================

简单账表服务插件的核心开发过程介绍完了,示例代码已经上传附件,老规矩,大家按需下载!

感谢大家持续关注,点赞、评论、收藏,您的点赞、评论就是我前进的动力。

简单账表服务插件主要是完成了报表查询数据的构建,对查询界面有一些个性化的设置还可以通过账表表单插件来处理。

下一篇:【Python插件入门】第8篇-账表表单插件

简单账表(采购日报表)取数服务插件示例.rar(4.33KB)


webapi数据处理过程:

1. 首先对接受到的客户端数据包中每个字段值进行解析(解析分为取数,赋值,触发值更新事件,调用表单服务等)

2. 解析完,调用APP层操作服务(其中保存接口还会走mvc层中维护插件的beforeSave和aftereSave事件,其他接口不会走维护插件事件)

3. APP层操作服务,主要对数据进行验证,触发操作插件事件,执行关联的业务流程(比如是否走反写引擎)等。

4. 最后,KSql引擎解析sql,通过DB层把数据保存或更新到数据库。


页面数据处理过程:

1.首先用户点击菜单,mvc层接受到菜单动作,触发相应的维护插件事件,调用菜单对应的业务层操作, 业务层的操作服务根据各种配置执行相关的业务逻辑,比如保存后是否提交,提交是否触发工作流等。

2. 业务层的操作服务同样要调用对应的APP操作服务,App层操作服务跟webapi调用的相同,

3. 调用完了APP层服务可能还有其他业务处理,比如提交后是否审核等。


webapi数据处理过程 VS 页面数据处理过程主要区别

1. webapi需要对字段值进行解析,页面不需要

2. webapi不会走业务逻辑层,页面会走


根据上面区别,我们建议可以从下面几点来做性能优化:

1. 服务端收到消息后首先对接收到的客户端数据包中每个字段值进行解析(解析分为取数,赋值,触发值更新事件,调用表单服务等),所以尽可能精简Josn数据包里面的字段。

2. 尽量使用批量保存代替保存接口,因为保存接口会多走beforeSave和aftereSave事件,并且每次只能处理一条数据, 而批量保存接口不仅可以批量处理还可以开后台线程并发执行。需要注意的是如果开启了多个线程并发调用批量保存接口(一般3到5个即可),那么批量保存接口参数里面BatchCount(服务端开启线程数)建议设置为1或2,否则可能造成服务器端请求阻塞。举个例子,5个线程并发调用,BatchCount设置为5,那么服务器端会产生25个线程并发处理,这种情况很容易造成阻塞。

3.业务层面在WebAPI调用时候,一些取价服务、取折扣服务可以考虑禁用,具体参考销售订单、销售出单相关说明:

https://vip.kingdee.com/article/11179

https://vip.kingdee.com/article/42071

4.避免每次调用增删查改接口之前都调用登录接口(会话默认超时是20分钟,没有必要每次都调用登录接口,登录接口本身也是一个耗时的操作)。一次登录多次使用简单示例

https://vip.kingdee.com/knowledge/650363039845203200?specialId=650386937144032256

5. 也是最重要的一点,星空的请求默认是同步模式,是有状态的。在同一个会话下,前一个请求没有结束,后一个请求即使发起了,也是要等待的。如果是不同的会话,相互间就没有影响。第三方集成调用接口,如果在本地做会话保持,并且用单用户调用,比较容易在客户端出现卡顿。可以考虑使用多端多用户多站点(尽量批量调用接口缓解并发调用压力)的方式来调用星空的接口。例如可以在不同的端发起星空的请求,不同端发起请求可以使用同一个用户;如果在一个端发起请求,则建议对请求进行业务区分,不要用一个用户调用所有请求,需要在集成端使用多个用户来调用星空的接口,缓解同一会话导致请求排队的问题。另外也可以增加多站点的调用机制来解决请求排队的问题。



作者:王文亮

来源:金蝶云社区

原文链接:https://vip.kingdee.com/knowledge/590559077445315584?specialId=650386937144032256&productLineId=1&isKnowledge=2&lang=zh-CN

著作权归作者所有。未经允许禁止转载,如需转载请联系作者获得授权。


【应用场景】


WebAPI登录一次默认会保持20分钟会话,如果20分钟内会调用其他接口,会话时间会重新算;同一用户调用接口,建议只登录一次,后续接口调用后判断返回消息代码,如果是消息代码等于1可以尝试重新登录后继续调用接口;

如果每次调用接口前都登录一次,大量调用后服务器上会有大量的上下文实例得不到及时释放,从而可能导致服务器内存高涨。



【案例演示】

通过webapi保存客户,一次登录后多次调用保存接口。


【实现步骤】

<1>创建控制台 , 引用Kingdee.BOS.WebApi.Client和Newtonsoft.Json。 

<2>代码示例。

using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Net.Http; using System.Text; namespace Zlf.WebApiDemo {     class Program     {         static void Main(string[] args)         {             Console.WriteLine("保存开始..");             string url = "http://localhost/k3cloud/";             var httpInvoke = new WebApiHttpInvoke(url, "67468ad9fb64d4", "demo", "2969888_47aD5+sL6kH8WV8u6eSASbzLTs7aRoLL", "566381bd78d64d83b6a2f9be05ccd01e");             for (int i = 0; i < 10; i++)             {                 string billNo = string.Format("20241129-{0}", i + 1);                 httpInvoke.Save("BD_Customer", billNo, CreateJson(billNo));             }             Console.WriteLine("保存完成..");             Console.ReadLine();         }         private static string CreateJson(string billNo)         {             var data = new             {                 IsVerifyBaseDataField = true,                 Model = new                 {                     FCreateOrgId = new                     {                         FNumber = "100"                     },                     FNumber = billNo,                     FUseOrgId = new                     {                         FNumber = "100"                     },                     FName = billNo,                     FCOUNTRY = new                     {                         FNumber = "China"                     },                     FIsDefPayer = false,                     FIsGroup = false,                     FCustTypeId = new                     {                         FNumber = "KHLB001_SYS"                     },                     FTRADINGCURRID = new                     {                         FNumber = "PRE001"                     },                     FTRANSLEADTIME = 0                 }             };             return JsonConvert.SerializeObject(data);         }     } }

<3>WebApiHttpInvoke.cs代码。

using Kingdee.BOS.WebApi.Client; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Zlf.WebApiDemo {     class WebApiHttpInvoke     {         private readonly string _dbId, _userName, _appId, _appSecret;         private readonly object _obj = new object();         private readonly K3CloudApiClient _client;         private bool _isLogin;         public WebApiHttpInvoke(string url, string dbId, string userName, string appId, string appSecret)         {             this._dbId = dbId;             this._userName = userName;             this._appId = appId;             this._appSecret = appSecret;             _client = new K3CloudApiClient(url, 10 * 60);//超时时间设为10分钟         }         //保存单据         public void Save(string formId, string billNo, string data, bool isRetry = false)         {             string saveResult = _client.Save(formId, data);             var responseStatus = JObject.Parse(saveResult)["Result"]["ResponseStatus"];             var isSuccess = responseStatus["IsSuccess"].Value<bool>();             if (!isSuccess)             {                 var msgCode = responseStatus["MsgCode"].Value<int>();                 if (msgCode == 1)//会话丢失,可能未登录或者过期了                 {                     if (isRetry)                     {                         return;                     }                     _isLogin = false;                     Login(); //重新登录                     Save(formId, billNo, data, true);                 }                 else                 {                     //暂存单据                     Console.WriteLine("保存失败,返回结果:{0}", saveResult);                 }             }             else             {                 Console.WriteLine("保存成功,billNo={0} ", billNo);             }         }         /// <summary>         /// 登录操作         /// </summary>         /// <returns></returns>         private bool Login()         {             lock (_obj)             {                 if (!_isLogin)                 {                     //使用第三方系统登录授权                     var loginResult = _client.LoginByAppSecret(this._dbId, this._userName, this._appId, this._appSecret, 2052);                     if (loginResult.Contains("response_error"))                     {                         //系统异常,包括账套id错误                         throw new Exception(loginResult);                     }                     var resultType = JObject.Parse(loginResult)["LoginResultType"].Value<int>();                     if (resultType == 1)                     {                         _isLogin = true;                         return true;                     }                     throw new Exception(loginResult);                 }             }             return true;         }     } }

【功能验证】

<1>运行代码。

上传图片

<2>启用WebAPI日志 , 可以看到只调用一次登录。

上传图片

上一篇:WebAPI性能优化建议   下一篇:使用多用户多站点调用WebAPI接口简单示例

作者:cyoukon

来源:金蝶云社区

原文链接:https://vip.kingdee.com/knowledge/650363039845203200?specialId=650386937144032256&productLineId=1&isKnowledge=2&lang=zh-CN

著作权归作者所有。未经允许禁止转载,如需转载请联系作者获得授权。


【应用场景】

同一个用户身份使用多线程调用WebAPI接口,默认是排队的,只有前面接口返回了结果,后一个接口才能继续处理 ;这是IIS的机制,相同的 session id的时候超过一定数量的时候会出现排队现象,但是我们可以使用多用户多站点调用来提高调用效率。



【注意事项】

线程不是越多越好,应根据具体应用场景和系统资源来权衡。



【案例演示】

使用4个用户2个站点调用WebAPI接口保存客户。


【实现步骤】

<1>基于帖子https://vip.kingdee.com/article/650363039794871552?productLineId=1&isKnowledge=2&lang=zh-CN的代码做修改。 

<2>代码如下。

using Newtonsoft.Json; using System; using System.Collections.Concurrent; using System.Linq; using System.Threading.Tasks; namespace Zlf.WebApiDemo {     class Program     {         const string AppId = "296988_47aD5+sL6kH8WV8u6eSASbzLTs7aRoLL";         const string AppSecret = "566381bd78d64d83b6a2f9be05cdd01e";         const string K3cloudUrl = "http://localhost/k3cloud/";//k3cloud站点         const string K3cloudAppUrl = "http://localhost/k3cloudApp/";//k3cloudapp站点         const string DbId = "67468ad9fb6464";         static readonly ConcurrentDictionary<string, WebApiHttpInvoke> DicUser = new ConcurrentDictionary<string, WebApiHttpInvoke>();         static int _currentUserIndex = -1;         static void Main(string[] args)         {         //本示例创建了4个用户实例,其中用了2个站点。这里采用异步的方式调用,确保4个实例是可以并行同步数据。             Console.WriteLine("保存开始..");             DicUser["demo_k3cloud"] = new   WebApiHttpInvoke(K3cloudUrl, DbId, "demo", AppId,AppSecret);             DicUser["demo2_k3cloudapp"] = new WebApiHttpInvoke(K3cloudUrl, DbId, "demo2", AppId,AppSecret);             DicUser["demo3_k3cloud"] = new WebApiHttpInvoke(K3cloudAppUrl, DbId, "demo3", AppId,AppSecret);             DicUser["demo4_k3cloudapp"] = new WebApiHttpInvoke(K3cloudAppUrl, DbId, "demo4", AppId,AppSecret);             Task[] tasks = new Task[10];             for (int i = 0; i < tasks.Length; i++)             {                 int n = i;                 tasks[i] = Task.Run(() => DoSave(string.Format("20241129-{0}", n)));             }             Task.WhenAll(tasks).Wait();             Console.WriteLine("全部保存完成..");             Console.ReadLine();         }         private static void DoSave(string billNo)          {             var instance = GetInstance();             instance.Save("BD_Customer", billNo, CreateJson(billNo));         }         /// <summary>         /// 按轮询方式获取WebApiHttpInvoke实例         /// </summary>         /// <returns></returns>         private static WebApiHttpInvoke GetInstance()         {             _currentUserIndex = (_currentUserIndex + 1) % DicUser.Count;             var kvp = DicUser.ElementAt(_currentUserIndex);             if (kvp.Key == null || kvp.Value == null)             {                 throw new Exception("GetInstance error");             }             return kvp.Value;         }         private static string CreateJson(string billNo)         {             var data = new             {                 IsVerifyBaseDataField = true,                 Model = new                 {                     FCreateOrgId = new                     {                         FNumber = "100"                     },                     FNumber = billNo,                     FUseOrgId = new                     {                         FNumber = "100"                     },                     FName = billNo,                     FCOUNTRY = new                     {                         FNumber = "China"                     },                     FIsDefPayer = false,                     FIsGroup = false,                     FCustTypeId = new                     {                         FNumber = "KHLB001_SYS"                     },                     FTRADINGCURRID = new                     {                         FNumber = "PRE001"                     },                     FTRANSLEADTIME = 0                 }             };             return JsonConvert.SerializeObject(data);         }     } }

<2>WebApiHttpInvoke.cs代码如下。

using Kingdee.BOS.WebApi.Client; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Zlf.WebApiDemo {     class WebApiHttpInvoke     {         private readonly string _dbId, _userName, _appId, _appSecret;         private readonly object _obj = new object();         private readonly K3CloudApiClient _client;         private bool _isLogin;         public WebApiHttpInvoke(string url, string dbId, string userName, string appId, string appSecret)         {             this._dbId = dbId;             this._userName = userName;             this._appId = appId;             this._appSecret = appSecret;             _client = new K3CloudApiClient(url, 10 * 60);//超时时间设为10分钟         }         //保存单据         public void Save(string formId, string billNo, string data, bool isRetry = false)         {             string saveResult = _client.Save(formId, data);             var responseStatus = JObject.Parse(saveResult)["Result"]["ResponseStatus"];             var isSuccess = responseStatus["IsSuccess"].Value<bool>();             if (!isSuccess)             {                 var msgCode = responseStatus["MsgCode"].Value<int>();                 if (msgCode == 1)//会话丢失,可能未登录或者过期了                 {                     if (isRetry)                     {                         return;                     }                     _isLogin = false;                     Login(); //重新登录                     Save(formId, billNo, data, true);                 }                 else                 {                     //暂存单据                     Console.WriteLine("保存失败,返回结果:{0}", saveResult);                 }             }             else             {                 Console.WriteLine("保存成功,billNo={0} ", billNo);             }         }         /// <summary>         /// 登录操作         /// </summary>         /// <returns></returns>         private bool Login()         {             lock (_obj)             {                 if (!_isLogin)                 {                     //使用第三方系统登录授权                     var loginResult = _client.LoginByAppSecret(this._dbId, this._userName, this._appId, this._appSecret, 2052);                     if (loginResult.Contains("response_error"))                     {                         //系统异常,包括账套id错误                         throw new Exception(loginResult);                     }                     var resultType = JObject.Parse(loginResult)["LoginResultType"].Value<int>();                     if (resultType == 1)                     {                         _isLogin = true;                         return true;                     }                     throw new Exception(loginResult);                 }             }             return true;         }     } }

【功能验证】

<1>运行代码。

上传图片


<1>查看WebAPI日志,确认可以多用户并发保存。


上传图片


上一篇:WebAPI一次登录多次调用示例   下一篇:启用webapi日志

作者:cyoukon

来源:金蝶云社区

原文链接:https://vip.kingdee.com/knowledge/650388592971234560?specialId=650386937144032256&productLineId=1&isKnowledge=2&lang=zh-CN

著作权归作者所有。未经允许禁止转载,如需转载请联系作者获得授权。


summary-icon摘要

由AI智能服务提供

本文是对Python插件开发中关于单据列表插件的详细介绍。文章首先简要回顾了表单插件的内容,并指出列表插件的必要性,指出其是Web服务层插件,依赖于单据列表界面触发。接着,文章详细讲解了列表插件的基本概念、重要成员属性(如ListView、ListModel及其相关功能),并提供了新建列表插件的注意事项和注册方法。此外,还介绍了列表插件中常用的事件及其用法,包括过滤事件、菜单点击事件、列表双击事件、单元格超链接点击事件以及列表条件格式化事件等,并给出了具体的事件处理方法和示例代码。

有用

反馈

往期回顾:

【Python插件入门】第1篇:Python插件入门讲解

【Python插件入门】第2篇:基本开发过程介绍

【Python插件入门】第3篇:插件中如何进行数据操作

【Python插件入门】第4篇:单据表单插件


    前面讲了单据表单插件,相信大家对插件事件有了一定的掌握,今天讲一下单据列表插件。

一、列表插件简介

    上一篇讲表单插件时,提到单据列表是不会触发表单插件的,于是我们需要用列表插件来处理列表相关的一些功能。

    列表插件也是Web服务层插件,需要依赖于单据列表界面(ListView)才能触发,可应用于单据列表、基础资料列表中。

    在C#插件开发时,列表插件的基类是AbstractListPlugIn列表插件和表单插件都是界面类的插件,他们都需要依赖于界面触发,他们有一个共同的父类AbstractDynamicFormPlugIn,所以有些表单插件的用法在列表插件中也同样可以用。

二、列表插件重要成员介绍

    列表插件中同样有this.Context,这个在上一篇表单插件中介绍了,用法类似,不再过多介绍,这里主要介绍一下列表插件特殊的一些成员属性。

  • ListView :列表视图层对象,列表插件除了View之外,还有ListView,通过ListView可以提供一些列表特有的功能。

    this.ListView.BillBusinessInfo.GetForm().Id;#当前列表对应的单据标识
    this.ListView .OpenParameter;#列表入口参数
    this.ListView.OpenParameter.HideListMenu = True;#隐藏列表菜单
    this.ListView.OpenParameter.IsShowFilter = True;#打开列表自动弹出过滤框,默认为False
    this.ListView.OpenParameter.IsShowQuickFilter=False;#是否显示快捷过滤
    this.ListView.OpenParameter.FilterSchemeId;#当前过滤方案ID

    this.ListView.OpenParameter.IsTrackBillList();#是否上下查列表

    #获取自定义参数,可获取发布主控台的参数,例如,将不同单据类型发布成不同列表

    this.ListView.OpenParameter.GetCustomParameter("参数标识");

    this.ListView.SelectedRowsInfo;#当前列表上被选中的行记录,复选框勾选的记录集合
    this.ListView.CurrentSelectedRowInfo;#当前列表上当前选择行数据,即焦点行
    this.ListView.CurrentPageRowsInfo;#当前列表页所有单据的行信息

    #******************************************************************************************

    grid=this.View.GetControl[EntryGrid]("FList");#获取列表表格控件对象

    selectedRowIndexs=List[int]();
    selectedRowIndexs.Add(1);
    selectedRowIndexs.Add(3);
    grid.SelectRows(selectedRowIndexs.ToArray());#设置选中行

    grid.SetRowHeight(80);#设置行高

  • ListModel :列表数据模型,从这里可以获取列表的数据

    this.ListModel;#获取列表数据模型
    selectedRowsInfo = this.ListView.SelectedRowsInfo;
    this.ListModel.GetData(selectedRowsInfo);#获取选中的数据

    selectedRowsInfo.GetPrimaryKeyValues();#获取选中行所有单据ID,字符串数组
    selectedRowsInfo.GetEntryPrimaryKeyValues();#获取所有选中行所有明细ID,字符串数组

    this.ListModel.FieldKeyMap;#列表上显示的字段,其字段名 FieldName 和字段标识Key的对应关系
    this.ListModel.FilterParameter;#列表过滤参数对象
    this.ListModel.FilterParameter.CustomFilter;#列表过滤框实体数据包
    this.ListModel.FilterParameter.FilterRows;#列表条件过滤行数据
    this.ListModel.FilterParameter.FilterString;#列表条件过滤数据系统自动转换出的条件表达式
    this.ListModel.GlobalParameter;#单据参数配置中配置的单据全局参数
    this.ListModel.ParameterData;#用户参数实体数据包,选项菜单界面的配置数据
    this.ListModel.Header;#列表表头对象
    this.ListModel.Header.GetChilds();#列表所有的列头集合
    this.ListModel.StartRow;#开始行索引,从0开始

    this.ListModel.Refresh();#刷新列表
    this.ListModel.RefreshByFilter();#根据过滤条件,重新取数,刷新列表

三、新建一个列表插件

  • 注意:这里提供一个引用比较全的Python列表插件示例模板,在附件中下载示例代码,复制到BOS里面注册!

  • Python列表插件注册方法:如下图所示,以采购订单为例,其他单据类似

image.png

四、列表插件常用事件介绍

    Python插件中如何使用事件,看第4篇中的介绍。

  • 列表插件中的常用事件用法介绍:下面介绍一些列表插件中的常用事件

#列表插件过滤事件,加载列表时会触发
#用法1:对列表过条件进行干预,此用法最广
#用法2:修改快捷过滤和过滤框的条件过滤,案例:动态构建下拉列表-年份
def PrepareFilterParameter(e):
    custfilterObj=e.CustomFilter;#过滤框实体数据包
    filterStr=e.FilterString;#过滤框条件过滤表达式
    sortStr=e.SortString#排序字段表达式
    statusFilterStr=e.StatusFilterString;#状态字段过滤表达式
    #msg=("{0}").format(statusFilterStr);
    #this.View.ShowMessage(msg);
    myFilterStr=("FBillNo like '%{0}%' ").format("1");#单据编号包含1
    e.AppendQueryFilter(myFilterStr);#追加过滤条件

#列表菜单点击事件,列表菜单点击开始时触发
#此事件也是很常用的,可以在此事件中取消菜单的点击事件
#使用时一定要判断菜单标识!!!
def BarItemClick(e):
    key=e.BarItemKey.ToUpperInvariant();
    if(key=="TestBtn".ToUpperInvariant()):
        #e.Cancel=True;#取消菜单的点击,可以阻止后续功能的触发,可完成一些简单校验
        #msg=("菜单[{0}]点击被取消啦!").format(key);
        #this.View.ShowMessage(msg);
        return;
    
#列表菜单点击后事件,列表菜单点击完成后触发
#使用时一定要判断菜单标识!!!
#自定义菜单功能实现建议在此事件中完成
#应用案例:列表插件实现单据批改
def AfterBarItemClick(e):
    key=e.BarItemKey.ToUpperInvariant();
    if(key<>"TestBtn".ToUpperInvariant()):
        return;
    msg=("菜单[{0}]点击完成啦!").format(key);
    #this.View.ShowMessage(msg);
    selectedRowsInfo=this.ListView.SelectedRowsInfo;
    billIDs=selectedRowsInfo.GetPrimaryKeyValues();
    entryIDs=selectedRowsInfo.GetEntryPrimaryKeyValues();
    msg=("选中了[{0}]个单据,[{1}]条明细!").format(billIDs.Length,entryIDs.Length);
    this.View.ShowMessage(msg);

#列表双击事件
def ListRowDoubleClick(e):
    row=e.Row;#双击的行号
    colKey=e.ColKey;#双击单元格列标识,这里取到的是列表显示字段名,不能直接用来取单元格的值,可用来做单元格判断
    currentPageRowsInfo = this.ListView.CurrentPageRowsInfo;#列表当前页的数据
    startRow = this.ListModel.StartRow;#当前页开始序号
    num = row - startRow;#双击行所在当前页的行序号
    listSelectedRow = currentPageRowsInfo[num - 1];#当前双击行数据
    FID=listSelectedRow.PrimaryKeyValue;#双击行单据ID
    entryId=listSelectedRow.EntryPrimaryKeyValue;#双击行明细ID
    #通过QueryBuilderParemeter取数,可以获取当前单元格的值,这里顺便演示一下这种取数方法吧*****************
    queryParam=QueryBuilderParemeter();
    queryParam.FormId=this.View.BillBusinessInfo.GetForm().Id;
    queryParam.BusinessInfo=this.View.BillBusinessInfo;
    #billId=SelectorItemInfo(this.View.BillBusinessInfo.GetForm().PkFieldName);#单据内码
    #queryParam.SelectItems.Add(billId);
    #billNo=SelectorItemInfo("FBillNo");#单据编号
    #queryParam.SelectItems.Add(billNo);
    #supplierId=SelectorItemInfo("FSupplierId");#供应商内码
    #queryParam.SelectItems.Add(supplierId);
    #supplierName=SelectorRefItemInfo("FSupplierId.FName");#取基础资料属性字段写法不一样,要注意
    #supplierName.PropertyName="FSupplierId_FName";估计后台是SQL取数,字段名不能有".",要重命名
    #queryParam.SelectItems.Add(supplierName);
    #以上是通过QueryBuilderParemeter取数示例,下面就是取当前双击单元格的*************************************
    fldKey=colKey;
    if("." in colKey):#说明是基础资料的属性
        colItem=SelectorRefItemInfo(colKey);
        fldKey=colKey.Replace(".","_");
        colItem.PropertyName=fldKey;
        queryParam.SelectItems.Add(colItem);
    else:
        queryParam.SelectItems.Add(SelectorItemInfo(colKey));
    entity=this.View.BillBusinessInfo.GetEntity(listSelectedRow.EntryEntityKey);
    if(entity<>None):#列表显示了单据体
        queryParam.FilterClauseWihtKey=("{0}_{1}={2}").format(entity.Key,entity.EntryPkFieldName,entryId);
    else:#列表只显示了单据头
        queryParam.FilterClauseWihtKey=("{0}={1}").format(this.View.BillBusinessInfo.GetForm().PkFieldName,FID);
    dataRows=QueryServiceHelper.GetDynamicObjectCollection(this.Context, queryParam);#获取数据结果数据包
    colValue="";
    if(dataRows.Count>0):
        colValue=("{0}").format(dataRows[0][fldKey]);
    msg=("双击了第{0}行的[{1}]").format(row,colValue);
    this.View.ShowMessage(msg);
    e.Cancel=True;#取消双击事件,否则双击会打开单据,也可以在BOS中取消列表双击事件绑定的操作

#列表单元格超链接点击事件
#该事件中数据处理方式和上面的列表双击事件中几乎一致,这里不再重复讲解
def EntryButtonCellClick(e):
    row=e.Row;#点击超链接所在序号
    fldKey=e.FieldKey;#超链接所在列字段标识
    msg=("点击了第{0}行的[{1}]").format(row,fldKey);
    this.View.ShowMessage(msg);
    e.Cancel=True;#取消事件,单据编号会自动超链接打开单据

#列表条件格式化事件
#可在此事件中根据不同的条件判断设置颜色,BOS中也可以配置,这里可以实现更灵活的条件判断
def OnFormatRowConditions(args):
    if (args.DataRow.ColumnContains("FMaterialId_Ref")):
        #注意!!!这里根据特别演示基础资料属性字段判断设置颜色,因为【列表条件格式化】不支持配置基础资料属性字段作为条件字段
        matObj=args.DataRow.DynamicObject["FMaterialId_Ref"];#物料字段实体数据包,可以取到引用属性中添加了的属性字段
        matNum=("{0}").format(matObj["Number"])
        if(matNum[0:1]=="1"):#物料编码是1开头的显示颜色,这里是实体数据包取值,别忘了是用绑定实体属性标识
            fc=FormatCondition();
            fc.ForeColor="#FFFF9B98";#前景色
            #fc.BackColor="#FFFF9B98";#背景色
            args.FormatConditions.Add(fc);
    if(args.DataRow.ColumnContains("FDate")):
        #这里是普通字段判断设置颜色,首先需要判断列表是否显示了这个字段,否则没显示该字段时会报错,而且只有显示了这个字段才能判断设置颜色
        dateStr=str(args.DataRow["FDate"]);#创建人Id
        date=DateTime.Parse(dateStr).ToString("yyyy-MM-dd");
        #this.View.ShowMessage(date);
        if(date==DateTime.Now.ToString("yyyy-MM-dd")):#单据日期=今天的单据显示颜色
            fc=FormatCondition();
            #fc.ForeColor="#FFFF9B98";#前景色
            fc.BackColor="#0000FF";#背景色
            args.FormatConditions.Add(fc);


#单据界面执行单据操作前触发,例如,保存,提交,审核等,使用时一定要判断操作代码
#与表单插件类似,不同的是,列表里面是批量处理,通常需要获取勾选的所有单据,参考前面事件中的介绍
def BeforeDoOperation(e):
    opCode=e.Operation.FormOperation.Operation.ToUpperInvariant();#触发操作代码大写,例如保存:SAVE
    if(opCode=="DELETE"):
        e.Cancel=True;#可以取消触发操作
        this.View.ShowWarnningMessage("取消删除!");
#单据界面执行单据操作完成后触发,例如,保存,提交,审核等,使用时一定要判断操作代码
#与表单插件类似,不同的是,列表里面是批量处理,通常需要获取勾选的所有单据,参考前面事件中的介绍
def AfterDoOperation(e):
    opCode=e.Operation.Operation.ToUpperInvariant();#触发操作代码大写,例如保存:SAVE
    this.View.ShowMessage(str(opCode));


好了,列表插件的 常用事件就介绍到这里,了解这些事情也基本能满足常见需求了。

五、列表插件读取单据数据

    对于列表插件开发,通常是对单据进行批量处理,列表插件中读取单据数据的方法也不同,这里简单介绍一下。

    在列表插件中是不能直接读取到完整的单据数据包的,需要变通获取,一般来讲是读取勾选的数据,在列表插件的成员中可以读取到勾选的数据行,但是字段信息不全,所以思路都是根据勾选的构建过滤条件,再去查询单据。

  • 首先,我们再来回忆一下列表插件读取数据要用到的几个成员:

    currentSelectedRow=this.ListView.CurrentSelectedRowInfo;#当前列表上当前选择行数据,即焦点行,单行数据

    selectedRowsInfo=this.ListView.SelectedRowsInfo;#列表勾选的数据集合

    billIDs=selectedRowsInfo.GetPrimaryKeyValues();#可以用来构建单据过滤条件查询单据

    entryIDs=selectedRowsInfo.GetEntryPrimaryKeyValues();
    datas this.ListModel.GetData(selectedRowsInfo);#根据勾选的数据集获取数据包,DynamicObjectCollection类型
    if (datas.Count <= 0):
#根据这个数据包集合来判断列表是否有勾选的数据行
        this.View.ShowWarnningMessage("未选择任何行!");
        return;

    #有时候也需要根据列表的分页数据来获取

     currentPageRowsInfo = this.ListView.CurrentPageRowsInfo;#列表当前页的数据
     startRow = this.ListModel.StartRow;#当前页开始序号

  • 在列表插件中,一般都是基于以上成员变量来读取单据数据,读取数据的方法有下面几种

    ①通过QueryBuilderParemeter取数方法来读取单据数据,参考前面列表双击事件中的介绍,效率较高,推荐!

    ②直接从selectedRowsInfo数据包中获取字段值,参考OnFormatRowConditions事件中的示例代码。

        注意!这种方法只能获取当前列表显示出来的字段,以及单据编号、内码等一些关键字段信息。

        可以通过循环的方式逐行获取字段值,示例代码如下:

    for row in selectedRowsInfo:
        dr=row.DataRow;
        billNo=row.BillNo;#单据编号
        billId=row.PrimaryKeyValue;#单据ID
        entryId=row.EntryPrimaryKeyValue;#明细ID,显示了单据体才会有值
        entityKey=str(row.EntryEntityKey);#当前显示的单据体标识,可用来判断entryId是属于哪个单据体的
        #其他字段要先判断是否显示
        if(dr.ColumnContains("FMaterialId_Id")):#物料Id
            matId=dr.DynamicObject["FMaterialId_Id"];
        if(dr.ColumnContains("FMaterialId_Ref")):#物料数据包
            #可以从这里读取基础资料属性字段,基础资料的取值方式可以参考这里
            matObj=dr.DynamicObject["FMaterialId_Ref"];
            matNum=matObj["Number"];#物料编码
            matName=matObj["Name"];#物料名称
        if(dr.ColumnContains("FDate")):#日期,普通字段参考这里
            date=dr.DynamicObject["FDate"];

    ③或者通过数据包datas中获取数据,方法其实与第②种从selectedRowsInfo中取数类似。

    datas是一个DynamicObjectCollection类型的数据集,是不是很熟悉了呢,学到这里,解析这个应该问题不大了!

    datas和selectedRowsInfo中的字段标识一样,也是只能取到列表显示的字段,并且取数方法也类似。

    for row in datas:
        billNo=row["FBILLNO"];#单据编号
        billId=row["FID"];#单据ID
        #其他字段建议先判断是否存在
        if(row.DynamicObjectType.Properties.ContainsKey("FMaterialId_Id")):#物料Id
            matId=row["FMaterialId_Id"];
        if(row.DynamicObjectType.Properties.ContainsKey("FMaterialId_Ref")):#物料数据包
            #可以从这里读取基础资料属性字段,基础资料的取值方式可以参考这里
            matObj=row["FMaterialId_Ref"];
            matNum=matObj["Number"];#物料编码
            matName=matObj["Name"];#物料名称
        if(row.DynamicObjectType.Properties.ContainsKey("FDATE")):#日期,普通字段参考这里
            date=row["FDate"];

    ④基于取到的单据内码ID和单据明细ID,拼接SQL,直接从数据库查询数据。

==========================本篇正文结束======================================

列表插件示例代码已经上传附件,老规矩,大家按需下载!

大家持续关注,点赞、评论、收藏,您的点赞、评论就是我前进的动力。

下一篇:【Python插件入门】第6篇-操作服务插件



列表插件示例代码.rar(3.52KB)