【Python插件入门】第7篇:简单账表服务插件原创
金蝶云社区-CQ周玉立
CQ周玉立
203人赞赏了该文章 13187次浏览 未经作者许可,禁止转载编辑于2022年08月22日 16:44:44
封面

往期回顾:

【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),可以在此方法中处理,本示例未作处理
def GetSummaryColumsSQL(summaryFields):
    listSummarFlds=summaryFields;#获取汇总字段
    for fld in summaryFields:
        summaryType=fld.SummaryType;#汇总类型
    return "";
#报表关闭触发,通常在此处清理报表过程产生的临时表
def CloseReport():
    this.DropTempTable();


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

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

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

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

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

发布于 金蝶云星空BOS开发交流圈 社群

赞 203