在系统用户交费后,需要打印发票,可以选择普票或者机打票(票据信息在系统中自定义设置的),也可以打印电子发票,这里对接的是航信的电子发票,请求方式非web服务,而是使用servlet通过HTTP请求的方式获取报文。
整个开票流程如下:
本地组装发票明细信息到报文(内部报文加密)——》将组装好的发票信息发往税控服务器——》成功的话解析返回的信息——》发票打印
报文格式:
实际测试报文如下:
xml version="1.0" encoding="utf-8"?> <SERVICE xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <HEAD> <nsrsbh>140115728183815nsrsbh> <serviceversion>1.3serviceversion> <serviceid>jy.dzptfpkj.hcserviceid> <iszip>Niszip> <issyn>Yissyn> <encryptcode>0encryptcode> <RTNINF/> HEAD> <BODY>PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KCjxSRVFVRVNUX0ZQS0pYWD4KICA8RlBLSlhYX0ZQVFhYPgogICAgPGRqcnEvPgogICAgPHhzZGgvPgogICAgPGZwbHg+MTA8L2ZwbHg+CiAgICA8Z2ZtYz7otK3kubDmlrk8L2dmbWM+CiAgICA8Z2Zuc3JzYmg+MTIzNDU2Nzg5MTIzNDU2PC9nZm5zcnNiaD4KICAgIDxnZmR6ZGg+6LSt5Lmw5pa55Zyw5Z2A55S16K+dPC9nZmR6ZGg+CiAgICA8Z2Z5aGp6aD7otK3kubDmlrnpk7booYzpk7booYw8L2dmeWhqemg+CiAgICA8Z2Zzai8+CiAgICA8Z2Z5eC8+CiAgICA8Yno+MTIzPC9iej4KICAgIDxrcHk+57O757uf566h55CG5ZGYPC9rcHk+CiAgICA8c2t5PmpjY3M8L3NreT4KICAgIDxmaHI+6KKB57+g6JCNPC9maHI+CiAgICA8aHNiej4xPC9oc2J6PgogICAgPHNsLz4KICAgIDx3Y3R6PjA8L3djdHo+CiAgICA8cnlkbT5hZG1pbjwvcnlkbT4KICAgIDxjaHl5PjEyMzwvY2h5eT4KICAgIDxmcHh6PjA8L2ZweHo+CiAgICA8dHNjaGJ6PjE8L3RzY2hiej4KICAgIDx5ZnBkbT4wMTQwMDEzMDAxMTE8L3lmcGRtPgogICAgPHlmcGhtPjIyMDM0MDk4PC95ZnBobT4KICAgIDxieXpkMS8+CiAgICA8Ynl6ZDIvPgogICAgPGJ5emQzLz4KICAgIDxieXpkNC8+CiAgICA8Ynl6ZDUvPgogIDwvRlBLSlhYX0ZQVFhYPgogIDxGUEtKWFhfWE1YWFM+CiAgICA8RlBLSlhYX1hNWFg+CiAgICAgIDxzcGZsZG0vPgogICAgICA8c3Bsd21jPuWfuuehgOawtOi0uTwvc3Bsd21jPgogICAgICA8Z2d4aD7llYbkuJo8L2dneGg+CiAgICAgIDxkdz4xPC9kdz4KICAgICAgPHNtLz4KICAgICAgPGNvdW50Pi0xPC9jb3VudD4KICAgICAgPHByaWNlPjkuNzA4NzM4PC9wcmljZT4KICAgICAgPGplPi05LjcxPC9qZT4KICAgICAgPHNsPjAuMDM8L3NsPgogICAgICA8c2U+LTAuMjk8L3NlPgogICAgICA8c3NmbGJtPjExMDAzMDEwMTAwMDAwMDAwMDA8L3NzZmxibT4KICAgICAgPGJtYmJoLz4KICAgICAgPGxzbGJzLz4KICAgICAgPHloemNicz4wPC95aHpjYnM+CiAgICAgIDx5aHpjc20vPgogICAgICA8ZnBoeHo+MDwvZnBoeHo+CiAgICAgIDxieXpkMS8+CiAgICAgIDxieXpkMi8+CiAgICAgIDxieXpkMy8+CiAgICAgIDxieXpkNC8+CiAgICAgIDxieXpkNS8+CiAgICA8L0ZQS0pYWF9YTVhYPgogIDwvRlBLSlhYX1hNWFhTPgo8L1JFUVVFU1RfRlBLSlhYPgo=BODY> SERVICE>
主要代码如下:
1. 页面选打印电子发票后,确定进入下面方法
public Result printElectronic(PrintInvoiceEntity entity) { // 电子发票不允许进行预览 if (entity.getIsPreview()) { return new Result(Status.ERROR, null, "电子发票不允许进行预览!"); } if (PrintCallingTypeEnum.HEATING.getCode().equals(entity.getPrintCallingType())) { //打印热费的电子发票 return this.printElectronicForHeating(entity); } else { return new Result(Status.ERROR, null, "未指定打印票据的调用方式!"); } }
附:相关实体类
public class PrintInvoiceEntity { /* ------------------- 非必填字段 ---------------------*/ private String volumeCode;//票据册号(当票据类型为电子发票时非必填) /* ------------------- 必填字段 ---------------------*/ private String companyCode;//开票公司编码 private String gmf_mc;//购买方名称 private String gmf_nsrsbh;//购买方纳税人识别号 private String gmf_dzdh;//购买方地址电话 private String gmf_yhzh;//购买方银行账户 private String bz;//备注 private String card_no; private String create_time; private String printCallingType;//打印调用方式 private Boolean isPreview;//是否预览模式 private Boolean isPreprint;//是否预开模式 private String invoiceType;//票据类型 ListpjItemEntities;//票据打印明细
2. 打印电子发票方法
private Result printElectronicForHeating(PrintInvoiceEntity entity) { // 正常采暖费交费时,获取上年结余 printInvoiceService.initSurplus(entity.getPjItemEntities()); // 根据交易明细组装发票明细信息 Listinvoices = printInvoiceService.splitInvoice( SessionUtil.getUser(), entity); // 调用航信税控进行打票 List
3. 组装发票明细信息
// 将交易明细拆分成发票 public ListsplitInvoice(User operator, PrintInvoiceEntity entity) { List<Invoice> invoices = new ArrayList (); // 1.初始化字典项数据 Map chargeItemDict = initChargeItemDict();// 收费项目 // Map unitPriceTypeDict = initUnitPriceTypeDict(); // 单价类别 MapareaTypeDict = initAreaTypeDict();// 面积类别 Map taxRateDict = initTaxRateDict(); // 税率 Map otherCostDict = initOtherCostDict(); // 第三方费用 List<PjItemEntity> pjItemEntities = entity.getPjItemEntities(); String customerIds = pjItemEntities.get(0).getSysattachment() .get(InvoiceInfoConstant.CUSTOMER_COLLECTION_ALIAS); String[] idArray = customerIds.split(","); String companyCode = this.getCompanyCode(entity, idArray[0]); String prjName = DeployConfigUtil.getJcDeployConfig().getProjectName(); JcCustomer jccustomer = this.getJccustomer(entity, idArray[0]); String userKindType =null; if (jccustomer !=null) { userKindType= jccustomer.getUserKindCode(); } // 4.获取公共的发票抬头模板 InvoiceSummary commonSummary = this.createInvoiceSummary(operator, companyCode); for (int i = 0; i < pjItemEntities.size(); i++) { // 浅复制发票抬头对象 InvoiceSummary summary = commonSummary.clone(); // 发票请求流水号 summary.setFpqqlsh(SerialNumberUtil.getNextNumber(2)); // 1.同步购买方信息 summary.setGmf_mc(entity.getGmf_mc());// 销售方-名称 summary.setGmf_nsrsbh(entity.getGmf_nsrsbh());// 销售方-纳税人识别号 summary.setGmf_dzdh(entity.getGmf_dzdh());// 销售方-地址电话 summary.setGmf_yhzh(entity.getGmf_yhzh());// 销售方-银行账户 summary.setCard_no (entity.getCard_no());// 销售方-银行账户 summary.setCreate_time (entity.getCreate_time());// 销售方-银行账户 if ("0".equals(prjName)) { summary.setBz(entity.getBz()); } else if("1".equals(prjName)){ if (jccustomer!=null && "user_type_2".equals(jccustomer.getUserTypeCode())) { // 二部制用户 summary.setBz(entity.getBz()+", 上年结余:"+pjItemEntities.get(i).getSurplus());// 备注 }else{ summary.setBz(entity.getBz()); } } Invoice invoice = this.splitInvoiceDetail( entity.getPrintCallingType(), summary, pjItemEntities.subList(i, i + 1), chargeItemDict, areaTypeDict, taxRateDict, otherCostDict, userKindType); invoices.add(invoice); } return invoices; }
将交易明细拆分成发票上的多个明细项
public Invoice splitInvoiceDetail(String printCallingType, InvoiceSummary summary, ListpjItemEntities, Map chargeItemDict, Map areaTypeDict, Map taxRateDict, Map otherCostDict, String userKindType) { List invoiceDetails = new ArrayList (); for (PjItemEntity pjItemEntity : pjItemEntities) { // 交易明细为热费 List list = this.createInvoiceDetailsByHeatingCost( printCallingType, pjItemEntity, chargeItemDict, areaTypeDict, taxRateDict , userKindType); invoiceDetails.addAll(list); } // 2.同步合计金额 BigDecimal hjje = BigDecimal.ZERO;// 合计金额 BigDecimal hjse = BigDecimal.ZERO;// 合金税额 for (InvoiceDetail invoiceDetail : invoiceDetails) { hjje = BigDecimalUtil.add(hjje, new BigDecimal(invoiceDetail.getXmje())); hjse = BigDecimalUtil.add(hjse, new BigDecimal(invoiceDetail.getSe())); } summary.setHjje(hjje.toString()); summary.setHjse(hjse.toString()); BigDecimal jshj = BigDecimalUtil.add(hjje, hjse);// 价税合计 summary.setJshj(jshj.toString()); // 3.组装发票 Invoice invoice = new Invoice(); invoice.setSummary(summary); invoice.setDetails(invoiceDetails); return invoice; }
相关实体:
public class Invoice {//发票实体 private InvoiceSummary summary; //发票抬头信息 private Listdetails; //发票项目明细信息
public class InvoiceSummary implements Cloneable{//发票抬头信息实体 private Long pjInfoId; //票据表ID private String fp_dm; // 发票代码 private String fp_hm; // 发票号码 private String fp_ch; // 发票册号 private String fpqqlsh; //发票请求流水号 private String kplx; //开票类型 private String xsf_nsrsbh; //销售方纳税人识别号 private String xsf_mc; //销售方名称 private String xsf_dzdh; //销售方地址、电话 private String xsf_yhzh; //销售方银行账号 否 private String gmf_nsrsbh; //购买方纳税人识别号 否 private String gmf_mc; //购买方名称 private String gmf_dzdh; //购买方地址、电话 否 private String gmf_yhzh; //购买方银行账号 否 private String kpr; //开票人 private String skr; //收款人 否 private String fhr; //复核人 否 private String yfp_dm; //原发票代码 红字发票时必须填写 private String yfp_hm; //原发票号码 红字发票时必须填写 private String jshj; //价税合计 单位:元(2位小数) private String hjje; //合计金额 不含税,单位:元(2位小数) private String hjse; //合计税额 单位:元(2位小数) private String bmb_bbh;//编码表版本号 目前为1.0 private String qd_bz;//清单标志 0:根据项目名称字数,自动产生清单,保持目前逻辑不变1:取清单对应票面内容字段打印到发票票面上,将项目信息 XMXX 打印到清单上。默认为 0。 1 暂不支持 private String qdxmmc;//清单项目名称 否 需要打印清单时对应发票票面项目名称清单标识( QD_BZ)为 1 时必填。为 0不进行处理。 private String ghf_sj; //购货方手机 否 private String ghf_email; //购货方邮箱 否 private String bz; //备注 否 private String card_no; //用户卡号 否 private String create_time; //交易时间 否
4. 调用航信税控进行打票
public List
相关实体:
public class InvoiceReturnEntity { //电子发票-请求开具发票返回报文对应的实体 private String fpqqlsh; //发票请求流水号 private String jqbh;//税控设备编号 private String fp_dm;//发票代码 private String fp_hm;//发票号码 private String kprq;//开票日期 private String fp_mw;//发票密文 private String jym;//校验码 private String ewm;//二维码 private String returnCode;//返回代码 private String returnMsg;//返回信息 private String ssyf;// 所属月份 private String kplx;// 开票类型1-正票 2-红票 private String hjbhsje;// 合计不含税金额 private String kphjse;// 开票合计税额 private String czdm;// 10-正常开具 20-红票 private String pdfFile;// PDF文件 private String pdfUrl;// PDF下载路径
5. 封装航信电子发票所需报文——》发送到航信接口开具报文——》解析包含开票信息的返回报文
5.1 生成电子发票开具报文
public static String createEInvoiceXml(Invoice invoice, String userKindType){ Document document = DocumentHelper.createDocument(); document.setXMLEncoding("utf-8"); // 默认utf-8 Element rootElement = getRoolElement(document, ""); String cdataXml =createEInvoiceCDATA(invoice, userKindType); //内部报文加密 Base64Encoder encoder = new Base64Encoder(); String comment = encoder.encode(cdataXml.getBytes()); Element dataElement = rootElement.element("Data"); dataElement.element("content").addText(comment); //xml文件"< >"禁止转义,保留<>样式的方法 String xml = StringEscapeUtils.unescapeXml(document.asXML()); return xml ; }
5.2 调用税控开具发票接口
public static String eInvoice(String wsXml){ String methodName = "eInvoiceCodes"; return EInvoiceWsUtil.invoke(methodName, wsXml); }
//调用远程服务接口的公共方法,获得包含开票信息的返回报文 public static String invoke(String methodName, String wsXml){ CloseableHttpClient httpclient = HttpClients.createDefault(); HttpPost httpPost = new HttpPost(webServiceUrl);// 创建httpPost httpPost.setHeader("Accept", "text/xml"); httpPost.setHeader("Content-Type", "text/xml"); String charSet = "UTF-8"; StringEntity entity = new StringEntity(wsXml, charSet); httpPost.setEntity(entity); CloseableHttpResponse response = null; try { response = httpclient.execute(httpPost); StatusLine status = response.getStatusLine(); int state = status.getStatusCode(); if (state == 200) { HttpEntity responseEntity = response.getEntity(); String jsonString = EntityUtils.toString(responseEntity); System.out.println("---response----"+jsonString); return jsonString; } else{ System.err.print("请求返回:"+state+"("+webServiceUrl+")"); } } catch(IOException e){ e.printStackTrace(); } finally { if (response != null) { try { response.close(); } catch (IOException e) { e.printStackTrace(); } } try { httpclient.close(); } catch (IOException e) { e.printStackTrace(); } } return null; }
5.3 解析返回报文,将信息保存进返回报文实体类
public static InvoiceReturnEntity createInvoiceReturnEntity(String returnXml){ InvoiceReturnEntity entity = new InvoiceReturnEntity(); try { Document document = DocumentHelper.parseText(returnXml); Element rootElement = document.getRootElement(); //business标签 Element element = null; Element headElement = rootElement.element("interface"); Element rtnElement = headElement.element("returnStateInfo"); String rtnCode = rtnElement.element("returnCode").getTextTrim(); Element dataElement = rootElement.element("Data"); //开票成功返回code if("0000".equals(rtnCode)){ // base64解码 String cdataXml = dataElement.element("content").getTextTrim(); String bodyXml = base64Decoder(cdataXml); Document bodyDoc = DocumentHelper.parseText(bodyXml); Element bodyElement = bodyDoc.getRootElement(); element = bodyElement.element("FPQQLSH"); entity.setFpqqlsh(element.getTextTrim()); element = bodyElement.element("FP_DM"); entity.setFp_dm(element.getTextTrim()); element = bodyElement.element("FP_HM"); entity.setFp_hm(element.getTextTrim()); element = bodyElement.element("KPRQ"); entity.setKprq(element.getTextTrim()); element = bodyElement.element("PDF_URL"); entity.setPdfUrl(element.getTextTrim()); } entity.setReturnCode(rtnCode); element = rtnElement.element("RETURNMESSAGE"); entity.setReturnMsg(element.getTextTrim()); } catch (DocumentException e) { System.err.println("电子发票-开具发票请求的返回报文解析失败!"); e.printStackTrace(); } return entity; }
6. 从网络Url中下载发票信息的pdf文件
public void downLoadByUrl(String urlStr) throws IOException{ URL url = new URL(urlStr); String[] str= urlStr.split("/"); String fileName= str[str.length-1]+".pdf"; String savePath = "C://upload"; HttpURLConnection conn = (HttpURLConnection)url.openConnection(); //设置超时间为3秒 conn.setConnectTimeout(2*1000); //防止屏蔽程序抓取而返回403错误 conn.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)"); //得到输入流 InputStream inputStream = conn.getInputStream(); //获取自己数组 byte[] getData = readInputStream(inputStream); //文件保存位置 File saveDir = new File(savePath); if(!saveDir.exists()){ saveDir.mkdir(); } File file = new File(saveDir+File.separator+fileName); FileOutputStream fos = new FileOutputStream(file); fos.write(getData); if(fos!=null){ fos.close(); } if(inputStream!=null){ inputStream.close(); } } // 从输入流中获取字节数组 public byte[] readInputStream(InputStream inputStream) throws IOException { byte[] buffer = new byte[1024]; int len = 0; ByteArrayOutputStream bos = new ByteArrayOutputStream(); while((len = inputStream.read(buffer)) != -1) { bos.write(buffer, 0, len); } bos.close(); return bos.toByteArray(); }
接口规范说明文档下载地址