分类 工作 下的文章

summary-icon摘要

由AI智能服务提供

本文是对“Python插件入门”系列中的一篇关于“简单账表服务插件”的扩展,主要介绍了账表表单插件的概念、应用场景、新建方法及常用事件。账表表单插件通过继承AbstractDynamicFormPlugIn基类实现,用于丰富报表界面功能,如设置数据颜色、修改报表单元格数据、单据关联查询等。文中提供了账表表单插件的详细开发步骤和代码示例,包括初始化、加载、数据绑定、按钮点击等事件的处理方法,展示了如何通过Python代码对报表进行自定义和扩展。

有用

反馈

往期回顾:

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

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

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

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

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

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

【Python插件入门】第7篇:简单账表服务插件


    接上一篇,简单账表服务插件主要是完成了报表查询数据的构建,已经完成了简单账表查询功能的开发,那么对于查询界面在做一些"锦上添花"的功能,会让报表功能更加丰富,或者基于报表查询结果,再做一些单据联查、穿透查询等功能,可以通过账表表单插件来完成。

一、账表表单插件简介

    C#开发时,账表表单插件的基类是AbstractSysReportPlugIn与单据的表单插件基类不同,C#开始时要注意一下!Python插件开发的话,了解就行了,他也是AbstractDynamicFormPlugIn的派生类,和单据表单插件算是"兄弟"关系。账表表单插件是依赖于账表界面触发的,也属于界面类插件,既然都是继承于AbstractDynamicFormPlugIn,所以很多表单插件中的事件方法也是可以使用的。

    账表表单插件中的一些重要属性和成员这里就不单独介绍了,下面的常用事件讲解中会做介绍,继续往下看!

  • 账表表单插件常见应用场景

    ①账表查询界面设置数据颜色。

    ②修改报表单元格数据:转换百分数、千分号分隔符、修改底部合计结算逻辑等。

    ③单据关联查询。

    ④报表穿透查询,关联弹出其他账表。

    ⑤关闭表头排序功能

    ⑥报表表头字段赋值

    ⑦...

二、新建一个账表表单插件

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

  • Python账表表单插件注册方法:如下图所示,以上一篇测试示例报表为例,其他账表类似。

image.png


三、账表表单插件常用事件介绍

    这里做了表单插件的示例,对报表界面做了一些丰富,测试效果图如下:

    image.png

  • 下面看下账表表单插件中常用事件介绍:

#初始化
def OnInitialize(e):
    openPara=this.View.OpenParameter;#获取打开报表时传入的参数
    myPara=openPara.GetCustomParameter("参数标识");#根据参数标识获取某个打开参数
    if(myPara<>None):#若参数不为空
        myParaStr=("{0}").format(myPara);#将参数转换为字符串
        #将参数传递给过滤框,可在过滤框注册表单插件获取此参数
        param=SysReportShowParameter();
        param.CustomParams.Add("参数标识",myParaStr);
#账表页面加载完毕
def OnLoad(e):
    return;#这里示例不执行后面的代码
    #若报表没有显示表头字段,可通过如下代码隐藏表头部分,以免显示一块空白区域,影响用户体验
    sp=this.View.GetControl[SplitContainer]("FSpliteContainer");
    sp.HideFirstPanel(True);#折叠分割容器上面部分

#报表页面数据加载完毕
def AfterBindData(e):
    listGrid=this.View.GetControl("FList");
    listGrid.SetCustomPropertyValue("AllowSorting"False);#关闭表头的排序功能
    reportModel=this.Model;#SysReportModel类型,报表数据模型
    billObj=reportModel.DataObject;#报表页面实体数据包
    reportView=this.View;#SysReportView类型,报表界面View
    titles=reportModel.ReportTitles;#获取账表服务插件中构建的报表表头字段集
    msg="";
    for title in titles:
        fldKey=title.TitleKey;#表头字段标识
        fldValue=title.TitleValue;#表头字段值
        fldCtl=reportView.GetControl(title.TitleKey);#获取表头字段控件
        #fldCtl.Enabled=True;#默认是锁定的,可通过此代码放开表头字段,允许编辑
        #fldCtl.Visible=False;#隐藏表头字段
    customFilterObj=reportModel.FilterParameter.CustomFilter;#报表过滤框快捷过滤实体数据包
    orgObj=customFilterObj["F_BPW_OrgId"];#获取组织过滤字段
    orgId=orgObj["Id"if(orgObj<>Noneelse "0";#获取组织Id
    this.Model.SetItemValueByID("F_BPW_OrgId",orgId,0);#给表头组织字段赋值成基础资料
    #this.View.ShowMessage(msg);

#按钮点击事件
def ButtonClick(e):
    key=e.Key.ToUpperInvariant();#按钮标识大写
    if(key=="F_BPW_TestBtn".ToUpperInvariant()):
        this.Model.SetItemValueByID("F_BPW_OrgId",0,0);
        reportModel=this.Model;#SysReportModel类型,报表数据模型
        customFilterObj=reportModel.FilterParameter.CustomFilter;#报表过滤框快捷过滤实体数据包
        customFilterObj["F_BPW_OrgId"]=None;#清空过滤数据包中的组织字段
        customFilterObj["F_BPW_OrgId_Id"]=0;
        this.View.Refresh();#清空组织后刷新,获取所有组织数据
        
#表头菜单点击事件
def BarItemClick(e):
    key=e.BarItemKey.ToUpperInvariant();#菜单标识大写
    if(key=="F_BPW_MyBtn".ToUpperInvariant()):
        reportModel=this.Model;#SysReportModel类型,报表数据模型
        rowCount=reportModel.GetRowCount();#报表数据总行数
        tab=reportModel.DataSource;#账表数据源DataTable,报表所有行数据
        x=rowCount;
        mytab=reportModel.GetData(0,x);#获取报表0-x行数据,返回DataTable对象
        tempTabName=tab.TableName;#账表临时表名称,每一次查询报表都会不一样
        msg=("表头菜单{0}点击啦!").format(key);
        this.View.ShowMessage(msg);

#格式化行数据,每一行会运行一次 
#在此事件中可以设置报表数据的颜色
def OnFormatRowConditions(args):
    if(args.DataRow.ColumnContains("orgName")==True):#根据字段标识判断是否显示了该字段
        ss= ("{0}").format(args.DataRow["orgName"]);    #获取字段值,这里是取组织名称
        if("蓝海" in ss):#组织名称包含"蓝海"
            fc=FormatCondition();
            fc.BackColor="#ccffff";#背景色
            fc.ForeColor = "#FF0000";#前景色
            #fc.ColorField = "orgName";#颜色字段
            #fc.ApplayRow = False;#适用整行,默认值为True,设置ColorField时,才会单独对字段设置颜色,否则应用整行
            args.FormatConditions.Add(fc);
    reportModel=this.Model;
    tab=reportModel.DataSource;
    allFlds=tab.Columns;#报表所有字段
    #循环对动态列进行判断,数量不为0显示颜色
    for fld in allFlds:
        fldKey=fld.ColumnName;
        if("_" not in fldKey or fldKey.StartsWith("F")==False):#非数量动态列,不做处理
            continue;#自定义的动态列名规则,我示例报表动态数量列名是F开头且包含下划线,其他字段均不满足此条件
        if(args.DataRow.ColumnContains(fldKey)==True):
            qty= float(args.DataRow[fldKey]);
            if(qty<>0):
                fc=FormatCondition();
                fc.BackColor="#CCCCCC";#背景色
                fc.ForeColor ="#FF0000";#前景色
                fc.ColorField = fldKey;#颜色字段
                fc.ApplayRow = False;
                args.FormatConditions.Add(fc);

#去除尾0,自定义方法供参考
def remove_exponent(i):
    t=str(i);
    return t.rstrip('0').strip('.'if '.' in t else t;
#单元数据格式化
#在这个事件里面可以对报表原输出的单元格内容做修改
def FormatCellValue(args):
    fldkey=args.Header.Key;#字段标识
    if(fldkey=="F2018_1_4" ):
        dr=args.DataRow;#获取当前行的数据
        valueStr= ("{0}").format(args.FormateValue);#获取该字段原输出值
        args.FormateValue=("{0}%").format(round(float(valueStr)*100,2));#转换成百分数
    elif(fldkey=="orgName"):
        dr=args.DataRow;#获取当前行的数据
        valueStr= ("{0}").format(args.FormateValue);#获取该字段原输出值
        args.FormateValue=valueStr.Replace("蓝海","***");#将组织名称中的"蓝海"替换成***
    elif("_" in fldkey and fldkey.StartsWith("F")==True):
        args.FormateValue=remove_exponent(args.FormateValue);#去除尾0
        #oldValue=Decimal.Parse(args.FormateValue);#转换成Decimal
        #args.FormateValue =oldValue.ToString("N");#设置千分位分隔符
    cellTye=int(args.CellType);#单元格类型,0:普通,1:合计,2:小计
    if(cellTye==1 and fldkey=="F2018_1_6"):#判断底部合计行,可对底部合计行做自定义计算修改
        #这里演示将F2018_1_6底部合计修改成:F2018_1_4+F2018_1_5
        newSumValue=float(args.DataRow["F2018_1_4"])+float(args.DataRow["F2018_1_5"]);
        args.FormateValue=("{0}").format(newSumValue);

#单元格超链接点击事件
def EntryButtonCellClick(e):
    row=e.Row;#点击超链接所在序号,从1开始
    fldKey=e.FieldKey.ToUpperInvariant();#点击单元格字段标识大写
    msg=("点击了第{0}行的[{1}]").format(row,fldKey);
    this.View.ShowMessage(msg);
    #e.Cancel=True;#取消点击事件
    #获取当前单元格的值
    reportModel=this.Model;
    tab=reportModel.DataSource;
    value=("{0}").format(tab.Rows[row-1][e.FieldKey]);

#单元格双击事件
#简单账表 表单不会触发EntityRowDoubleClick事件,用此事件代替
def CellDbClick(Args):
    #Args.Cancel=True;#取消事件,若二开标准报表,可以此取消标准功能自带的双击事件
    row=Args.CellRowIndex;#双击序号,从1开始
    fldKey=Args.Header.FieldName;#双击单元格的字段名
    #获取当前单元格的值
    reportModel=this.Model;
    tab=reportModel.DataSource;
    value=("{0}").format(tab.Rows[row-1][fldKey]);#也可以传其他字段名,即可获取其他字段值
    msg=("点击了第{0}行的[{1}],{1}值={2}").format(row,fldKey,value);
    this.View.ShowMessage(msg);

#行点击事件
def EntityRowClick(e):
    row=e.Row;#点击的行序号,从1开始
    #msg=("点击了第{0}行!").format(row);
    #this.View.ShowMessage(msg);
    reportView=this.View;
    selectedDataRows=reportView.SelectedDataRows;#获取选中行,若启用了允许多选,则可获取到所有选中行
    if(selectedDataRows==None or selectedDataRows.Length==0):#无选中行数据
        return;
    #下面实现选中行合计功能
    dt=selectedDataRows[0].Table;
    allFlds=dt.Columns;#报表所有字段
    jsonArr=JSONArray();
    DIC={};
    DIC["F2018_1_4"]="2018-1-4";#添加需要合计的字段标识和字段标题的字典
    DIC["F2018_1_5"]="2018-1-5";
    for fldKey in DIC.keys():
        columnIndex=allFlds.IndexOf(fldKey);
        sumValue=0;
        for selectRow in selectedDataRows:
            fldValue=float(selectRow.ItemArray[columnIndex]);#取出选中行对应字段的值
            sumValue=sumValue+fldValue;
        jsonObj=JSONObject();
        jsonObj.Put("Value",remove_exponent(sumValue));
        jsonObj.Put("Caption",("{0}:").format(DIC[fldKey]));
        jsonArr.Add(jsonObj);
    listGrid=this.View.GetControl("FList");
    listGrid.SetSelectRowsTips(jsonArr);#输出到前端,按下alt键并把鼠标放到选中行的区域,即可看到效果

四、经典实践案例参考

    有了账表表单插件确实可以丰富很多功能,以下是整理的各位大佬分享的一些账表表单插件的常用案例。

    大多是C#插件示例,但是结合上面常用事件介绍中的Python示例,使用Python插件实现也不是难事。


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

用2篇文章把Python账表开发的核心介绍完了,应该能满足95%以上的报表需求了。

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

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


下一篇:【Python插件入门】第9篇-单据转换插件

简单账表-表单插件示例(全).rar(3.86KB)


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

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