很早之前就想写这篇文章与大家分享一下自己在吉特仓储管理系统中开发打印和报表的功能,在GitHub(https://github.com/hechenqingyuan/gitwms)上公开下载的代码中很多人觉得在线设计报表这个功能比较不错,但是很多人也会有疑问。这边文章就简单讲解一下如何开发这个功能的,供大家学习参考,如果有任何疑问可以直接联系我,当然也有很多不足之处希望大家多多谅解和指点。
一. 各种需求报表以及打印
最开始之初在Web上做打印是每个打印也都会做一个页面,利用的是浏览器自身带的打印功能,当时做的也津津有味的感觉比较爽,但是后面做的打印页面多了想死的心都有,特别是遇到了复杂的打印。后面就想着用一个打印组件试试,这样开发打印可能方便很多,于是后面使用了lodop 打印组件(收费),这是一个非常不错的打印组件,刚开始觉得这个组件也挺不错的,后面用着也发现很多东西都有局限性。于是后面专门弄了一个在线报表设计组件(FastReport),相信很多人都使用过这个组件,可以很方便的做在线报表以及打印功能。
在吉特仓储管理系统中涉及到打印的部分主要是如下部分:
(1) 入库单 (2) 出库单[有些客户喜欢用作送货单] (3) 报损单 (4) 调拨单 (5) 销售订单 (6) 采购单 (7) 各种报表功能
以单据为主的相关打印功能:
另外一种是以表格为主的报表统计功能:
还有一种以图表为主的报表功能:
二. 如何统一报表和单据的打印
使用FastReport 可以方便的做在线Web的打印,所以这里打印功能就不是问题了,无论是做单据的打印还是做报表的打印直接利用这个组件即可。在分析一下打印以及报表的过程:
(1) 获取数据源:数据可能来自多方面的,比如入库单,出库单,以及自己定义的数据源
(2) 数据显示到页面中:不同单据显示不一样,这里就需要不同的打印模板
(3) 打印页面中内容:打印功能比较单一,打印显示的内容即可
只要能够解决上面上个问题,那么做一个公共的打印功能就比较方便了,提供一个公共格式的数据源,根据公共格式的数据源能够在线设计不同形式的模板,打印显示的内容。
不同的单据其字段是不一样的,所以在统计数据格式的时候可以使用DataTable ,DataTable是比较方便的能够解决这种问题的,至于打印模板无非就是一个在线设计的问题,FastReport已经完全解决了这种问题。最关键点就是提供数据源了, 在吉特仓储管理系统中数据源都是使用的实体模型, 相信各位做开发的也能够理解在开发过程中使用DataTable 带来的不便,单是在这里我们要反其道而行将实体模型转换为DataTable 。
三. 分类处理
本文章只做几个分类的处理,这个分类比较零散,这里以其中一个客户实际案例作为讲解。
在报表的分类中我们定义了 入库单,出库单,盘点单,报表和施工单几个分类, 都是定义的枚举值。 我们可以根据具体的业务需求来修改这里的值。当然这里也制定了数据源提供的方式,主要是两种数据源:SQL 语句以及存储过程。其实这里我们有第三种数据源,那就是各种单据数据,因为系统中已经提供了各种单据数据访问的接口,我们没有必要去再次写SQL语句以及存储过程,存储过程和SQL语句只是给自定义报表来使用的,因为自定义报表不清楚系统是否已经提供了响应的接口。
FastReport利用的数据源其实也就是DataSet,这里我们可以很方便的衔接起来。如果使用SQL语句或者存储过程我们就只需要将返回的结果集指定为DataSet,而其他的单据则只需要将List<T>集合转换为DataTable 即可。 这里就解决了字段不确定的问题,在技术上解决如下几个问题:
(1) List<T> 转换为DataTable 的问题: 这种问题非常容易解决,有过.NET开发经验的应该都知道
(2) SQL 语句中带有参数的问题: 这里规定SQL语句中可以带参数或者不带参数,如果有参数那必须使用@占位符
(3) 存储过程中带有参数的问题:存储过程同样可以带参数或者不带参数
(4) 调用SQL语句和存储过程执行问题: 如何确定调用的是SQL语句还是存储过程, 这里在数据源格式上做了分类,上图可以看得出
四. SQL数据源
使用SQL数据源遵循如下几个步骤:
(1) 编写SQL语句,SQL语句中如果有参数则必须使用占位符参数
(2) 添加占位符参数信息,占位符参数必须和SQL语句中的占位符参数一致
SELECT * FROM [dbo].[ConDetail] WHERE [email protected]
假设我们定义报表的数据源如上,当然也可以不适用参数的。以上是客户实际案例中择选的,更多的细节就不透露,反正可以说明问题。
有几个参数则添加几个参数,不能多也不能少,必须唯一对应,否则在执行的过程中就无法得到正确的数据。基本信息填好之后保存则可以生成一个报表的数据行,接下来要走的就是设计模板了。
在线设计器打开之后就可以看到数据源中包含的所有数据字段了,这样就可以设计我们想要的报表格式。至于如何设计报表这里不做过的阐述,本文主要讲解设计这边功能的一个思路,报表工具的使用可以到网上查找相关的资料学习。设计好报表之后保存相关的设计即可,然后就可以打开预览查看了。
打开预览页面可以看到相应的参数输入框,输入参数点击搜索即可展示报表的内容,完全根据自己的条件需要展示不同的数据。上面这个案例虽然有点简单,但是能够说明问题,在实际的客户过程中肯定不只是显示两列数据的,可能有多个语句更加复杂的操作在里面,但是都大同小异。
五. 存储过程的使用
存储过程相对SQL语句来说是一样的,这里唯一一个比较偏门的就是如何或者存储过程的参数问题,我们以入库单审核存储过程为例: Proc_AuditeInStorage
数据源类型选择存储过程,在数据源中输入存储过程的名称,然后回车则会自动加载存储过程中的所有参数信息,然后自己将这些信息补充完整即可,比如显示的名称,页面显示的元素类型。
SELECT [SPECIFIC_CATALOG],[SPECIFIC_NAME],[ORDINAL_POSITION],[PARAMETER_MODE],[PARAMETER_NAME],[DATA_TYPE],[CHARACTER_MAXIMUM_LENGTH] FROM [INFORMATION_SCHEMA].[PARAMETERS] WHERE [SPECIFIC_NAME]=@SPECIFIC_NAME
上面这段SQL语句是获取存储过程的相关参数信息的,如果有类似的功能需求可以参考利用一下这个SQL语句。SQL以及存储过程的相关执行都是依赖于Git.Framework.ORM 这个组件,当然你也可以使用其他的方式来实现。
六. 单据打印的数据源
单据打印的数据源有点特殊,他不需要自己写SQL或者存储过程来提供数据源,当然你执意要这么做也是没问题的。
public override DataSet GetPrint(string argOrderNum) { DataSet ds = new DataSet(); InStorageEntity entity = new InStorageEntity(); entity.SnNum = argOrderNum; entity = GetOrder(entity); if (entity != null) { List<InStorageEntity> list = new List<InStorageEntity>(); list.Add(entity); DataTable tableOrder = list.ToDataTable(); ds.Tables.Add(tableOrder); InStorDetailEntity detail = new InStorDetailEntity(); detail.OrderSnNum = argOrderNum; List<InStorDetailEntity> listDetail = GetOrderDetail(detail); listDetail = listDetail.IsNull() ? new List<InStorDetailEntity>() : listDetail; DataTable tableDetail = listDetail.ToDataTable(); ds.Tables.Add(tableDetail); } else { List<InStorageEntity> list = new List<InStorageEntity>(); List<InStorDetailEntity> listDetail = new List<InStorDetailEntity>(); DataTable tableOrder = list.ToDataTable(); ds.Tables.Add(tableOrder); DataTable tableDetail = listDetail.ToDataTable(); ds.Tables.Add(tableDetail); } return ds; }
入库单打印数据源提供
入库单打印的数据都是List<T> ,我们这里需要将其转换为DataTable
public DataSet GetPrint(string SnNum) { DataSet ds = new DataSet(); ConBookEntity entity = GetBook(SnNum); if (entity != null) { List<ConBookEntity> listBook = new List<ConBookEntity>(); listBook.Add(entity); DataTable tableBook = listBook.ToDataTable(); ds.Tables.Add(tableBook); List<ConDetailEntity> listDetail = GetDetailList(SnNum); listDetail = listDetail.IsNull() ? new List<ConDetailEntity>() : listDetail; DataTable tableDetail = listDetail.ToDataTable(); ds.Tables.Add(tableDetail); List<BookMaterialEntity> listMaterial = GetMaterialList(SnNum); listMaterial = listMaterial.IsNull() ? new List<BookMaterialEntity>() : listMaterial; DataTable tableMaterial = listMaterial.ToDataTable(); ds.Tables.Add(tableMaterial); } else { List<ConBookEntity> listBook = new List<ConBookEntity>(); entity = new ConBookEntity(); listBook.Add(entity); DataTable tableBook = listBook.ToDataTable(); ds.Tables.Add(tableBook); List<ConDetailEntity> listDetail = null; listDetail = listDetail.IsNull() ? new List<ConDetailEntity>() : listDetail; DataTable tableDetail = listDetail.ToDataTable(); ds.Tables.Add(tableDetail); List<BookMaterialEntity> listMaterial = null; listMaterial = listMaterial.IsNull() ? new List<BookMaterialEntity>() : listMaterial; DataTable tableMaterial = listMaterial.ToDataTable(); ds.Tables.Add(tableMaterial); } return ds; }
施工单打印的数据源
下载过github上代码看过的人,其实一看就明白这里都是套路,套路。 所有的单据都是这个套路,同时也遵循这个套路。
在报表管理的页面中新建施工单打印的模板,报表类型要选择施工单,这里不能随意选一定要选择正确,相关的数据源都可以不指定,或者随意制定以下SQL或者存储过程即可。
各种单据的打印我们需要提供的参数就是单据的唯一编号,这里是需要明确的,在吉特仓储系统中唯一编号使用的是GUID,其实这样可以很方便的解决这个问题。
; (function ($) { $.fn.CusReportDialog = function (options) { var defaultOption = { title:"选择打印模板", data: {}, Mult: false, EventName: "click", callBack: undefined, ReportType:undefined }; defaultOption = $.extend(defaultOption, options); var current=undefined; var target=$(this); var DataServer={ Server: function () { var config = (function () { var URL_GetList = "/Report/ManagerAjax/GetList"; return { URL_GetList: URL_GetList }; })(); //数据操作服务 var dataServer = (function ($, config) { //查询分页列表 var GetList=function(data,callback){ $.gitAjax({ url: config.URL_GetList, data: data, type: "post", dataType: "json", success: function (result) { if(callback!=undefined && typeof callback=="function"){ callback(result); } } }); } return { GetList: GetList } })($, config); return dataServer; }, SetTable:function(result){ current.find("#tabInfo").DataTable({ destroy: true, data:result.Result, paging:false, searching:false, scrollX: false, bAutoWidth:true, bInfo:false, ordering:false, columns: [ { data: ‘SnNum‘ ,render:function(data, type, full, meta){ return "<input type=‘checkbox‘ name=‘item_report‘ value=‘"+data+"‘ data-full=‘"+JSON.stringify(full)+"‘/>"; }}, { data: ‘ReportNum‘}, { data: ‘ReportName‘}, { data: ‘Remark‘} ], aoColumnDefs:[ { "sWidth": "15px", "aTargets": [0] } ], oLanguage:{ sEmptyTable:"没有查询到任何数据" } }); var pageInfo=result.PageInfo; if(pageInfo!=undefined){ current.find("#myMinPager").minpager({ pagenumber: pageInfo.PageIndex, recordCount: pageInfo.RowCount, pageSize: pageInfo.PageSize, buttonClickCallback: DataServer.PageClick }); } DataServer.BindEvent(); }, BindEvent:function(){ if(defaultOption.Mult){ current.find("#tabInfo").find("input[name=‘item_all‘]").click(function(event) { var flag=$(this).attr("checked"); if(flag){ current.find("#tabInfo").find("input[name=‘item_report‘]").attr("checked",true); }else{ current.find("#tabInfo").find("input[name=‘item_report‘]").attr("checked",false); } }); } else{ current.find("#tabInfo").find("input[name=‘item_all‘]").hide(); current.find("#tabInfo").find("input[name=‘item_report‘]").click(function(event) { current.find("#tabInfo").find("input[name=‘item_report‘]").attr(‘checked‘, false); $(this).attr("checked",true); }); } }, GetSelect:function(){ var list=[]; current.find("#tabInfo").find("input[name=‘item_report‘]").each(function(i,item){ var flag=$(item).attr("checked"); if(flag){ var value=$(item).attr("data-full"); var item=JSON.parse(value); list.push(item); } }); return list; } } var submit = function (v, h, f) { if (v == 1) { var list=DataServer.GetSelect(); if (defaultOption.callBack != undefined && typeof (defaultOption.callBack) == "function") { if(defaultOption.Mult){ defaultOption.callBack.call(target,list); }else{ defaultOption.callBack.call(target,list[0]); } } } }; $(this).bind(defaultOption.EventName, function () { var Server=DataServer.Server(); var search={}; search["ReportType"]=defaultOption.ReportType; Server.GetList(search,function(result){ var data=result.Result; if(data!=undefined && data.length>1){ $.jBox.open("get:/Report/Manager/Dialog", defaultOption.title, 650, 400, { buttons: { "选择": 1, "关闭": 2 }, submit: submit, loaded: function (h) { current=h; DataServer.SetTable(result); } }); }else{ defaultOption.callBack.call(target,data[0]); } }); }); }; })(jQuery);
单据打印模板的选择插件
不同的单据再打印的时候可以选择不同的模板,这里可以使用这个插件来实现
$(‘#tabList‘).find(‘a.print‘).each(function(i,item){ $(item).CusReportDialog({ ReportType:1, callBack:function(result){ if(result!=undefined){ var SN=data[i].OrderSnNum; var SnNum=result.SnNum; var url="/Report/Manager/Show?SnNum="+SnNum+"&OrderNum="+SN; window.location.href=url; } } }); });
调用打印功能示例
七. 模板设计过程中如何加载结构
有个很显示的问题,在模板设计的过程中如果数据源为空,那模板设计就失去了设计的基础数据源结构,这样是毫无意义的。而很多情况下很多数据在后台过程中只有得到了数据才能得到结构,这是要命的问题。所以在这里走一定技巧性的处理,如果查询的数据源结果集为空,那么久默认添加一个空的数据进去,填充其结构,这里需要特别注意。
List<InStorageEntity> list = new List<InStorageEntity>(); List<InStorDetailEntity> listDetail = new List<InStorDetailEntity>(); DataTable tableOrder = list.ToDataTable(); ds.Tables.Add(tableOrder); DataTable tableDetail = listDetail.ToDataTable(); ds.Tables.Add(tableDetail);
默认处理数据源结构