# 单据转换插件

单据转换，能够把A单据的数据，根据转换规则，转换生成B单据。

单据转换过程，大概分为如下几个步骤：

- 读取源单到目标单之间的全部转换规则；
- 匹配转换规则的业务范围，确定适用于当前所选源单的转换规则；
- 提取转换规则上，字段映射页签及其他页签，使用到的源单字段；
- 根据源单内码，生成源单取数条件；
- 读取源单数据行：
  - 只读取转换规则用到的源单字段；
  - 如果源单是整单下推，则读取源单全部行；否则取源单所选行数据；
  - 把单据头、单据体字段组合在一起，生成拉平后的源单数据行；
- 根据转换规则，数据范围的配置，对源单行数据，进行筛选，剔除不符合条件的行；
- 根据分单、分录行合并策略，对源单数据行进行分组；
- 根据分组后的源单数据行，生成目标单、目标单分录行；
- 逐行填写目标分录行、单据头字段值；
- 在目标单的关联子实体中，记录来源单据信息；
  - 后续保存目标单时，会根据关联子实体中的来源单据信息，记录单据关联关系及反写；
  - 如果没有在关联子实体中记录来源单据，则不记录单据关联关系，不能联查，也不能反写；
- 输出生成好的目标单数据包，完成单据转换
  - 特别说明：单据转换，只是生成目标单数据包，并没有保存入库；

如上单据转换过程，会触发单据转换插件事件，允许插件干预。

本节介绍单据转换插件各事件的触发时机及用途。



##  插件基类
### 插件接口及基类

系统预置了单据转换插件基类`AbstractConvertPlugIn`，实现了转换插件接口`IConvertPlugin`：

```java
package kd.bos.entity.botp.plugin;
public  AbstractConvertPlugIn  IConvertPlugIn {
```



单据转换插件接口是`IConvertPlugin`：

```java
package kd.bos.entity.botp.plugin; 
public interface IConvertPlugIn {
    
	/**
	 * 单据转换可选参数
	 * @return
	 */
	default OperateOption getOption() {
		return OperateOption.create();
	}
	default void setOption(OperateOption option) {}

	/**
	 * 开始执行转换规则之前，创建规则上挂的插件后，触发此事件
	 * 
	 * @param opType
	 * @param srcMainType
	 * @param tgtMainType
	 * @param rule
	 * @remark
	 * 由 CreateConvertPlugAction 动作触发
	 */
	void setContext(ConvertOpType opType, BillEntityType srcMainType, BillEntityType tgtMainType, ConvertRuleElement rule);
	
    // 代码顺序：按单据转换操作，各种执行场景，各场景中执行的活动进行排序；
    // 阅读时，按照顺序，从上到下寻找适合的事件

	// 各转换操作公共事件 *

	/**
	 * 初始化变量事件，此时还没有开始编译规则
	 * 
	 * @param e
	 * @remark
	 * 获取上下文信息，构建一些必须的变量
	 * 由 InitializeAction 动作触发
	 */
    default  void initVariable(InitVariableEventArgs e){}

    /**
     * 编译数据筛选条件前事件：可追加定制条件，是否忽略规则原生的条件
     * 
     * @param e 事件参数：包括本次下推的源单数据、是否忽略规则原生条件
     * @remark
     * 由 ConvertRuleCompiler 动作触发
     */
    default  void beforeBuildRowCondition(BeforeBuildRowConditionEventArgs e){}

    /**
     * 构建分单、行合并模式之前事件：调整分单、合并策略及依赖的字段
     * 
     * @param e
     * @remark 
     * 由 ConvertRuleCompiler 动作触发
     */
    default  void beforeBuildGroupMode(BeforeBuildGroupModeEventArgs e){}

    /**
     * 构建取数参数后事件：可在正式读取源单数据之前，添加额外的字段、过滤条件
     * 
     * @param e
     * @remark
     * 由 BuildQueryParameterAction 动作触发
     */
    default  void afterBuildQueryParemeter(AfterBuildQueryParemeterEventArgs e){}

    /**
     * 取源单数据前事件：可在正式读取源单数据之前，修改取数语句、取数条件
     * 
     * @param e
     * @remark
     * 由 LoadSourceDataAction 动作触发
     */
    default  void beforeGetSourceData(BeforeGetSourceDataEventArgs e){}

    /**
     * 取源单数据后事件：根据源单数据，获取其他定制的引用数据；也可以替换系统自动获取到的数据
     * 
     * @param e
     * @remark
     * 由 RunDataConditionAction 动作触发
     */
    default  void afterGetSourceData(AfterGetSourceDataEventArgs e){}

    /**
     * 初始化创建目标单据数据包前事件 （暂未触发）
     * 
     * @param e
     * @remark
     * 这个事件，只在选单时触发：
     * 选单时，需要基于现有的目标单数据包，进行追加处理；
     * 插件可以在此事件，获取到现有的目标单数据包，提前进行定制处理
     */
    default  void beforeCreateTarget(BeforeCreateTargetEventArgs e){}

    /**
     * 创建目标单据数据包后事件：把根据分单规则创建好的目标单，传递给插件
     * 
     * @param e
     * @remark
     * 由 CreateLinkEntityRowsAction 动作触发 
     */
    default  void afterCreateTarget(AfterCreateTargetEventArgs e){}

    /**
     * 目标字段赋值完毕后事件：插件可以在此基础上，继续填写目标字段值
     * 
     * @param e
     * @remark
     * 由 MappingFieldAction 动作触发
     */
    default void afterFieldMapping(AfterFieldMappingEventArgs e){}
    
    /**
     * 记录关联关系前事件：取消记录关联关系
     * 
     * @param e
     * @remark
     * 由 FillLinkInfoAction 动作触发
     */
    default  void beforeCreateLink(BeforeCreateLinkEventArgs e){}

    /**
     * 记录关联关系后事件：根据系统自动记录的关联关系，进行相关数据的同步携带，如携带其他子单据体数据
     * 
     * @param e
     * @remark
     * 由 FillLinkInfoAction 动作触发
     */
    default  void afterCreateLink(AfterCreateLinkEventArgs e){}
    
    /**
     * 单据转换后事件，最后执行：插件可以在这个事件中，对生成的目标单数据，进行最后的修改
     * 
     * @param e
     * @remark
     * 由 MergePushResultAction 动作触发
     */
    default  void afterConvert(AfterConvertEventArgs e){}
    
	// 选单前事件 *
    
    /**
     * 选单条件生成后，触发此事件：供插件追加选单条件
     * 
     * @param e
     */
    default void afterBuildDrawFilter(AfterBuildDrawFilterEventArgs e) {}
}
```



### 创建并注册插件

自定义单据转换插件，必须扩展插件基类`AbstractConvertPlugIn`，绑定到单据转换规则上：

![package](image\botp01.png)



- 附：自定义单据转换插件示例

```java
package kd.bos.plugin.sample.bill.billconvert.bizcase;

import kd.bos.entity.BillEntityType;
import kd.bos.entity.botp.ConvertOpType;
import kd.bos.entity.botp.ConvertRuleElement;
import kd.bos.entity.botp.plugin.AbstractConvertPlugIn;
import kd.bos.entity.botp.plugin.args.AfterBuildQueryParemeterEventArgs;
import kd.bos.entity.botp.plugin.args.AfterConvertEventArgs;
import kd.bos.entity.botp.plugin.args.AfterCreateLinkEventArgs;
import kd.bos.entity.botp.plugin.args.AfterCreateTargetEventArgs;
import kd.bos.entity.botp.plugin.args.AfterFieldMappingEventArgs;
import kd.bos.entity.botp.plugin.args.AfterGetSourceDataEventArgs;
import kd.bos.entity.botp.plugin.args.BeforeBuildGroupModeEventArgs;
import kd.bos.entity.botp.plugin.args.BeforeBuildRowConditionEventArgs;
import kd.bos.entity.botp.plugin.args.BeforeCreateLinkEventArgs;
import kd.bos.entity.botp.plugin.args.BeforeCreateTargetEventArgs;
import kd.bos.entity.botp.plugin.args.BeforeGetSourceDataEventArgs;
import kd.bos.entity.botp.plugin.args.InitVariableEventArgs;

/
 * 演示单据转换插件事件的触发时机
 * 
 * @author rd_JohnnyDing
 *
 */
public class BillConvertEventSample extends AbstractConvertPlugIn {

	/**
	 * 演示如何获取上下文信息
	 */
	private void getContext(){
		// 源单主实体
		BillEntityType srcMainType = this.getSrcMainType();
		// 目标单主实体
		BillEntityType tgtMainType = this.getTgtMainType();
		// 转换规则
		ConvertRuleElement rule = this.getRule();
		// 转换方式：下推、选单
		ConvertOpType opType = this.getOpType();
	}
	
	/**
	 * 初始化变量事件
	 * 
	 * @param e
	 * @remark
	 * 获取上下文信息，构建一些必须的变量
	 */
	@Override
	public void initVariable(InitVariableEventArgs e) {
		this.printEventInfo("initVariable", "");
	}
	
    /**
     * 构建取数参数后事件
     * 
     * @param e
     * @remark
     * 添加额外的字段、过滤条件
     */
	@Override
	public void afterBuildQueryParemeter(AfterBuildQueryParemeterEventArgs e) {
		this.printEventInfo("afterBuildQueryParemeter", "");
	}

    /**
     * 编译数据筛选条件前事件
     * 
     * @param e
     * @remark
     * 设置忽略规则原生的条件，改用插件定制条件，或者在规则条件基础上，追加定制条件
     * 
     */
	@Override
	public void beforeBuildRowCondition(BeforeBuildRowConditionEventArgs e) {
		this.printEventInfo("beforeBuildRowCondition", "");
	}

    /**
     * 取源单数据前事件
     * 
     * @param e
     * @remark
     * 修改取数语句、取数条件
     */
	@Override
	public void beforeGetSourceData(BeforeGetSourceDataEventArgs e) {
		this.printEventInfo("beforeGetSourceData", "");
	}

    /**
     * 取源单数据后事件
     * 
     * @param e
     * @remark
     * 根据源单数据，获取其他定制的引用数据；也可以替换系统自动获取到的数据
     */
	@Override
	public void afterGetSourceData(AfterGetSourceDataEventArgs e) {
		this.printEventInfo("afterGetSourceData", "");
	}


    /**
     * 构建分单、行合并模式之前事件
     * 
     * @param e
     * @remark 
     * 调整分单、合并策略及依赖的字段
     */
	@Override
	public void beforeBuildGroupMode(BeforeBuildGroupModeEventArgs e) {
		this.printEventInfo("beforeBuildGroupMode", "");
	}
	

    /**
     * 初始化创建目标单据数据包前事件
     * 
     * @param e
     * @remark
     * 这个事件，只在选单时触发：
     * 选单时，需要基于现有的目标单数据包，进行追加处理；
     * 插件可以在此事件，获取到现有的目标单数据包，提前进行定制处理
     */
	@Override
	public void beforeCreateTarget(BeforeCreateTargetEventArgs e) {
		this.printEventInfo("beforeCreateTarget", "");
	}

    /**
     * 创建目标单据数据包后事件
     * 
     * @param e
     * @remark
     * 这个事件，只在下推时触发，把根据分单规则创建好的目标单，传递给插件
     */
	@Override
	public void afterCreateTarget(AfterCreateTargetEventArgs e) {
		this.printEventInfo("afterCreateTarget", "");
	}

    /**
     * 目标字段赋值完毕后事件
     * 
     * @param e
     * @remark
     * 插件可以在此基础上，继续填写目标字段值
     */
	@Override
	public void afterFieldMapping(AfterFieldMappingEventArgs e) {
		this.printEventInfo("afterFieldMapping", "");
	}

    /**
     * 记录关联关系前事件
     * 
     * @param e
     * @remark
     * 取消记录关联关系
     */
	@Override
	public void beforeCreateLink(BeforeCreateLinkEventArgs e) {
		this.printEventInfo("beforeCreateLink", "");
	}

    /**
     * 记录关联关系后事件
     * 
     * @param e
     * @remark
     * 根据系统自动记录的关联关系，进行相关数据的同步携带，如携带其他子单据体数据
     */
	@Override
	public void afterCreateLink(AfterCreateLinkEventArgs e) {
		this.printEventInfo("afterCreateLink", "");
	}

    /**
     * 单据转换后事件，最后执行 
     * 
     * @param e
     * @remark
     * 插件可以在这个事件中，对生成的目标单数据，进行最后的修改
     */
	@Override
	public void afterConvert(AfterConvertEventArgs e) {
		this.printEventInfo("afterConvert", "");
	}

	private void printEventInfo(String eventName, String argString){
		String msg = String.format("%s : %s", eventName, argString);
		System.out.println(msg);
	}
}

```


## 插件事件

单据转换插件，提供如下插件事件：

| 事件 | 触发时机 |
| --- | --- |
| initVariable | 初始化变量事件 |
| afterBuildQueryParemeter | 构建取数参数后事件 |
| beforeBuildRowCondition | 编译数据筛选条件前事件 |
| beforeGetSourceData | 取源单数据前事件 |
| afterGetSourceData | 取源单数据后事件 |
| beforeBuildGroupMode | 构建分单、行合并模式之前事件 |
| beforeCreateTarget | 暂未触发 |
| afterCreateTarget | 创建目标单据数据包后事件 |
| afterFieldMapping | 目标字段赋值完毕后事件 |
| beforeCreateLink | 记录关联关系前事件 |
| afterCreateLink | 记录关联关系后事件 |
| afterConvert | 单据转换完毕事件，最后执行 |

### initVariable 事件

#### 事件触发时机

开始运行转换规则，创建好了转换规则上绑定的单据转换插件之后，即触发此事件。

插件可以在此事件中，对本地变量进行初始化。

此事件发生时，源单主实体、目标单主实体、转换规则都已经确定，可以基于这些上下文信息，初始化本地变量。

一些通用的单据转换业务插件，需要自动适应各种单据，这个事件就显得比较重要：

可以在转换规则执行前，让通用插件了解到当前的上下文，初始化一些变量，决定后续业务逻辑。

插件可以利用如下方法，获取到源单、目标单主实体、反写规则：

```java
	private void getContext(){
		// 源单主实体
		BillEntityType srcMainType = this.getSrcMainType();
		// 目标单主实体
		BillEntityType tgtMainType = this.getTgtMainType();
		// 转换规则
		ConvertRuleElement rule = this.getRule();
		// 转换方式：下推、选单
		ConvertOpType opType = this.getOpType();
	}

```



#### 代码模板

```java
package kd.bos.plugin.sample.bill.billconvert.template;

import kd.bos.entity.botp.plugin.AbstractConvertPlugIn;
import kd.bos.entity.botp.plugin.args.InitVariableEventArgs;

public class InitVariable extends AbstractConvertPlugIn {

	@Override
	public void initVariable(InitVariableEventArgs e) {
		// TODO 在此添加业务逻辑
	}
}

```



#### 示例

- 案例说明

1. 需要开发一个通用单据转换插件，把目标单单据体上的金额字段值，合计到单据头上；

2. 不同的单据上，单据体、单据头上的金额字段标识不同；

3. 暂时只支持采购费用发票、销售费用发票；



- 实现方案

1. 捕获 `initVariable`事件，确认源单、目标单，单据体、单据头各金额字段标识；

2. 在`afterCreateLink`事件，根据单据体金额字段，合计单据头金额字段，并换算本位币金额；



- 实例代码

  ```java
  package kd.bos.plugin.sample.bill.billconvert.bizcase;
  
  import java.math.BigDecimal;
  import java.math.MathContext;
  import java.math.RoundingMode;
  import java.util.Date;
  import kd.bos.dataentity.entity.DynamicObject;
  import kd.bos.dataentity.entity.DynamicObjectCollection;
  import kd.bos.dataentity.utils.StringUtils;
  import kd.bos.entity.ExtendedDataEntity;
  import kd.bos.entity.botp.plugin.AbstractConvertPlugIn;
  import kd.bos.entity.botp.plugin.args.AfterCreateLinkEventArgs;
  import kd.bos.entity.botp.plugin.args.InitVariableEventArgs;
  
  public class InitVariableSample extends AbstractConvertPlugIn {
  
  	/ 目标单主实体标识 */
  	private String targetEntityNumber;
  	/ 目标单单据体标识 */
  	private String key_entryentity;
  	
  	private boolean isOtherBill = false;
  	
  	/ 字段标识，单据头.价税合计_原币*/
  	private String key_AllAmountOri;
  	/ 字段标识，单据体.价税合计_原币*/
  	private String key_AllAmountOri_D;
  	/ 字段标识，单据头.价税合计_本位币*/
  	private String key_AllAmount;
  	
  	/ 字段标识，单据头.不含税金额_原币*/
  	private String key_AmountOri;
  	/ 字段标识，单据体.不含税金额_原币*/
  	private String key_AmountOri_D;
  	/ 字段标识，单据头.不含税金额_本位币*/
  	private String key_Amount;
  	
  	/ 字段标识，单据头.税额_原币*/
  	private String key_TaxAmountOri;
  	/ 字段标识，单据体.税额_原币*/
  	private String key_TaxAmountOri_D;
  	/ 字段标识，单据头.税额_本位币*/
  	private String key_TaxAmount;
  
  	/**
  	 * 初始化事件：根据目标单，确定价税合计金额等字段标识
  	 */
  	@Override
  	public void initVariable(InitVariableEventArgs e) {
  		this.targetEntityNumber = this.getTgtMainType().getName();
  		this.setAmountKey();
  	}
  
  	/**
  	 * 关联关系已经记录，并重算了反写控制字段值之后，触发此事件
  	 * 
  	 * @remark
  	 * 金额字段，是反写控制字段：
  	 * 反写控制字段，需要记录从每条源单行携带过来的值，然后自动合计到单据体字段上；
  	 * 用户手工修改单据体上的反写控制字段时，会根据原始携带量分配、反写到源单
  	 * 
  	 * 因此，如果要依赖于反写控制字段值进行运算，必须在afterCreateLink事件之后
  	 * 
  	 */
      @Override
  	public void afterCreateLink(AfterCreateLinkEventArgs e) {
      	
      	if (this.isOtherBill){
      		// 其他未知的单据，不合计金额
      		return;
      	}
      	
      	ExtendedDataEntity[] billDataEntitys = e.getTargetExtDataEntitySet().FindByEntityKey(this.targetEntityNumber);
      	// 逐单合计单据头金额字段值
      	for(ExtendedDataEntity billDataEntity : billDataEntitys){
      		this.calcAmount(billDataEntity);
      	}
  	}
  
  	/**
       * 根据目标单，设置含税价格等字段标识
       */
      private void setAmountKey() {
          
          if (StringUtils.equals(this.targetEntityNumber, "iv_purexpinv")){
          	// 采购费用发票
          	
          	this.key_entryentity = "entryentity";
          	
              //价税合计
          	this.key_AllAmountOri = "amountori";
          	this.key_AllAmountOri_D = "amountori";
          	this.key_AllAmount = "amount";
  
              //不含税金额
          	this.key_AmountOri = "headauxpriceori";
          	this.key_AmountOri_D = "headauxpriceori";
          	this.key_Amount = "headauxprice";
  
              //税额
          	this.key_TaxAmountOri = "deductibletaxori";
          	this.key_TaxAmountOri_D = "deductibletaxori";
          	this.key_TaxAmount = "deductibletax";
          }
          else if (StringUtils.equals(this.targetEntityNumber, "iv_saleexpinv")){
          	// 销售费用发票
  
          	this.key_entryentity = "entryentity";
          	
              //价税合计
          	this.key_AllAmountOri = "aftertotaltaxfor";
          	this.key_AllAmountOri_D = "allamountori";
          	this.key_AllAmount = "aftertotaltax";
  
              //不含税金额
          	this.key_AmountOri = "headauxpriceori";
          	this.key_AmountOri_D = "amountori";
          	this.key_Amount = "headauxprice";
  
              //税额
          	this.key_TaxAmountOri = "taxamount";
          	this.key_TaxAmountOri_D = "detailtaxamountori";
          	this.key_TaxAmount = "taxamountori";
          }
          else {
          	// 其他单据：不在此插件中计算价税合计
          	isOtherBill = true;
          }
      }
      
      /**
       * 计算价税合计
       * 
       * @param billDataEntity
       */
      private void calcAmount(ExtendedDataEntity billDataEntity){
      	
          long currencyID = (long) billDataEntity.getValue("currencyid_id");				// 原币
          long mainCurrencyID =(long) billDataEntity.getValue("mainbookstdcurrid_id");	// 本币
          long exchangeTypeID = (long) billDataEntity.getValue("exchangetype_id");		// 换算类型
          Date date = (Date) billDataEntity.getValue("date");		// 业务日期
  
          // 取当日汇率
          BigDecimal rate = new BigDecimal("0");
          if (currencyID > 0 && mainCurrencyID > 0) {
              rate = this.getExchangeBusRate(currencyID, mainCurrencyID, exchangeTypeID, date);
          }
  
          // 把汇率回填到单据上
          billDataEntity.setValue("exchangerate", rate);
          
          // 开始合计金额：逐行循环，汇总到变量上
          BigDecimal taxAmountOriH = new BigDecimal("0");   // 单据头.税额
          BigDecimal noTaxAmountOriH = new BigDecimal("0");   // 单据头.不含税金额
          BigDecimal totalTaxAmountOriH = new BigDecimal("0");   //单据头.价税合计
  
          DynamicObjectCollection entryRows = (DynamicObjectCollection)billDataEntity.getValue(this.key_entryentity);
          for (DynamicObject entryRow : entryRows){
          	taxAmountOriH = taxAmountOriH.add(entryRow.getBigDecimal(key_TaxAmountOri_D)); 	// 原币 单据体.税额
              noTaxAmountOriH = noTaxAmountOriH.add(entryRow.getBigDecimal(key_AmountOri_D));	// 原币 单据体.不含税金额
              totalTaxAmountOriH = totalTaxAmountOriH.add(entryRow.getBigDecimal(key_AllAmountOri_D));	// 原币 单据体.价税合计
          }
          
          // 填写单据头.原币各金额
          billDataEntity.setValue(key_TaxAmountOri, taxAmountOriH);
          billDataEntity.setValue(key_AmountOri, noTaxAmountOriH);
          billDataEntity.setValue(key_AllAmountOri, totalTaxAmountOriH);
  
          // 本位币各金额 ：原币金额 * 汇率 (四舍五入，10位小数)
          MathContext mc = new MathContext(10, RoundingMode.HALF_UP);
          billDataEntity.setValue(key_TaxAmount, taxAmountOriH.multiply(rate, mc));
          billDataEntity.setValue(key_Amount, noTaxAmountOriH.multiply(rate, mc));
          billDataEntity.setValue(key_AllAmount, totalTaxAmountOriH.multiply(rate, mc));
      }
  
      /**
       * 取当日汇率
       * @return
       */
      private BigDecimal getExchangeBusRate(long currencyId1, long currencyId2, long exchangeType, Date date){
      	// 略过取汇率的逻辑，直接返回 1
      	return new BigDecimal("1");
      }
  }
  
  ```

  




 ###  afterBuildQueryParemeter 事件

#### 事件触发时机

系统根据转换规则上的字段映射关系，确认好了需要加载的源单字段之后，触发此事件，并传入需转换的源单行过滤条件`( FID in [1,2,3,4] )`。

插件可以在此事件中，增加需要加载的源单字段，调整源单行取数条件。

#### 代码模板

```java
package kd.bos.plugin.sample.bill.billconvert.template;

import kd.bos.entity.botp.plugin.AbstractConvertPlugIn;
import kd.bos.entity.botp.plugin.args.AfterBuildQueryParemeterEventArgs;

public class AfterBuildQueryParemeter extends AbstractConvertPlugIn {

	@Override
	public void afterBuildQueryParemeter(AfterBuildQueryParemeterEventArgs e) {
		// TODO 在此添加业务逻辑
	}
}

```



#### 事件参数

- `public class AfterBuildQueryParemeterEventArgs extends ConvertPluginEventArgs`
  - `public Map  <String, String> getSrcFldAlias()`：预计会加载的源单字段及其别名；
  - `public void addSrcField(String fullPropName)` ：
    - 添加插件需要用到的源单字段；
    - 传入字段标识，如`textfield`；
    - 如果要取源单基础资料字段的引用属性，要传入基础资料字段标识及其引用属性，如`basedatafield.name`；
    - 不需要在前面带单据体标识；
  - `public List <QFilter> getQFilters()`：系统根据传入的源单内码，生成的源单取数条件，插件可以调整此集合中的条件对象；



#### 示例

- 案例说明

1. 源单有单据编号、业务日期、金额字段

2. 需要把这三个字段值，拼成一个字符串，填写到目标单内容字段上；

3. 业务日期格式化为`yyyy-MM-dd`，金额带币别

- 实现方案

1. 捕获 `afterBuildQueryParemeter` 事件，要求加载源单单据编号、业务日期、金额、币别字段

2. 捕获 `afterFieldMapping` 事件，取源单字段值，格式化后填写在目标单上

- 实例代码

  ```java
  package kd.bos.plugin.sample.bill.billconvert.bizcase;
  
  import java.math.BigDecimal;
  import java.text.SimpleDateFormat;
  import java.util.Date;
  import java.util.List;
  
  import kd.bos.dataentity.entity.DynamicObject;
  import kd.bos.entity.ExtendedDataEntity;
  import kd.bos.entity.botp.plugin.AbstractConvertPlugIn;
  import kd.bos.entity.botp.plugin.args.AfterBuildQueryParemeterEventArgs;
  import kd.bos.entity.botp.plugin.args.AfterFieldMappingEventArgs;
  import kd.bos.entity.botp.runtime.ConvertConst;
  
  public class AfterBuildQueryParemeterSample extends AbstractConvertPlugIn {
  
  	private final static String KEY_BILLNO = "billno";
  	private final static String KEY_DATE = "date";
  	private final static String KEY_AMOUNT = "amount";
  	private final static String KEY_CURRENCYNAME = "currency.name";
  	private final static String KEY_CONTENT = "content";
  	
  	/**
  	 * 在开始读取源单数据前，触发此事件
  	 * @remark
  	 * 在此事件中，要求加载插件要用到的源单字段
  	 */
  	@Override
  	public void afterBuildQueryParemeter(AfterBuildQueryParemeterEventArgs e) {
  		
  		e.addSrcField(KEY_BILLNO);		// 单据编号
  		e.addSrcField(KEY_DATE);		// 业务日期
  		e.addSrcField(KEY_AMOUNT);		// 金额
  		e.addSrcField(KEY_CURRENCYNAME);	// 币别.名称 currency.name
  	}
  
  	/**
  	 * 目标单字段值，携带完毕后，触发此事件
  	 * @remark
  	 * 在此事件中，自行取源单字段值，格式化后填写到目标单
  	 */
  	@SuppressWarnings("unchecked")
  	@Override
  	public void afterFieldMapping(AfterFieldMappingEventArgs e) {
  		
  		// 取目标单，单据头数据包 （可能会生成多张单，是个数组）
  		String targetEntityNumber = this.getTgtMainType().getName();
  		ExtendedDataEntity[] billDataEntitys = e.getTargetExtDataEntitySet().FindByEntityKey(targetEntityNumber);
  		
  		SimpleDateFormat timesdf = new SimpleDateFormat("yyyy-MM-dd");
  		
  		// 逐单处理
  		for(ExtendedDataEntity billDataEntity : billDataEntitys){
  			
  			// 取当前目标单，对应的源单行
  			List<DynamicObject> srcRows = (List<DynamicObject>)billDataEntity.getValue(ConvertConst.ConvExtDataKey_SourceRows);
  			// 取源单第一行上的字段值，忽略其他行
  			DynamicObject srcRow = srcRows.get(0);
  			
  			// 开始格式化字段值
  			StringBuilder sBuilder = new StringBuilder();
  			sBuilder.append(this.getSrcMainType().getDisplayName().toString());
  			
  			// 单据编号字段值
  			String billno = (String)e.getFldProperties().get(KEY_BILLNO).getValue(srcRow);
  			sBuilder.append(billno).append("; ");
  			
  			// 日期
  			Object date = e.getFldProperties().get(KEY_DATE).getValue(srcRow);
  			if (date != null){
  				sBuilder.append("日期: ").append(timesdf.format((Date)date)).append("; ");
  			}
  			
  			// 金额
  			BigDecimal amount = (BigDecimal)e.getFldProperties().get(KEY_AMOUNT).getValue(srcRow);
  			sBuilder.append("金额: ").append(amount.toString());
  			
  			// 币别
  			String currency = (String)e.getFldProperties().get(KEY_CURRENCYNAME).getValue(srcRow);
  			sBuilder.append(currency);
  			
  			// 给目标单字段赋值
  			billDataEntity.setValue(KEY_CONTENT, sBuilder.toString());
  		}
  	}
  }
  
  ```

  



###  beforeBuildRowCondition 事件

#### 事件触发时机

编译转换规则 - 数据范围，配置的源单数据筛选条件之前，触发此事件。

插件可以在此事件，忽略转换规则上配置的条件，改用插件定制条件，或者追加插件定制条件；

#### 代码模板

```java
package kd.bos.plugin.sample.bill.billconvert.template;

import kd.bos.entity.botp.plugin.AbstractConvertPlugIn;
import kd.bos.entity.botp.plugin.args.BeforeBuildRowConditionEventArgs;

public class BeforeBuildRowCondition extends AbstractConvertPlugIn {

	@Override
	public void beforeBuildRowCondition(BeforeBuildRowConditionEventArgs e) {
		// TODO 在此添加业务逻辑
	}
}

```



#### 事件参数

- `public class  BeforeBuildRowConditionEventArgs extends ConvertPluginEventArgs`
  - `public void  setIgnoreRuleFilterPolicy(boolean  ignoreRuleFilterPolicy)`：完全忽略转换规则上的数据范围；
  - `public List<QFilter> getCustQFilters()`：插件定制条件，用于数据库取数；
  - `public void setCustFilterExpression(String custFilterExpression)`：插件定制条件表达式，用于内存运算；条件含义需要与`CustQFilters`一致，系统会分别用于不同的时机点；
  - `public void setCustFilterDesc(String custFilterDesc)`：插件定制条件描述，当源单数据行，不符合此条件时，系统会把这段条件描述，提示给用户，告诉用户数据行不允许下推的原因；



#### 示例

- 案例说明

1. 单据下推，需要根据系统选项，动态增加数据筛选条件：

   a. 如果勾选了&quot;锁定的订单不允许下推&quot;选项，则不能下推锁定状态为已锁定(B)的单据

   b. 如果没有勾选此选项，则不做此限制

- 实现方案

1. 捕捉 `beforeBuildRowCondition` 事件，读取选项值

2. 如果勾选了选项，则追加数据行筛选条件，只允许下推未锁定的单据

- 实例代码

```java
package kd.bos.plugin.sample.bill.billconvert.bizcase;

import kd.bos.entity.botp.plugin.AbstractConvertPlugIn;
import kd.bos.entity.botp.plugin.args.BeforeBuildRowConditionEventArgs;
import kd.bos.orm.query.QCP;
import kd.bos.orm.query.QFilter;

public class BeforeBuildRowConditionSample extends AbstractConvertPlugIn {

	@Override
	public void beforeBuildRowCondition(BeforeBuildRowConditionEventArgs e) {
		
		if (!cannotPushLockBill()){
			e.setCustFilterDesc("不允许下推已锁定的单据");		// 给出不允许下推的原因
			
			// 设置条件表达式，用于脚本执行 （必选）
			e.setCustFilterExpression(" lockstatus = 'A' ");
			
			// 同时设置具有相同含义的QFilter条件，用于选单数据查询 （必选）
			QFilter qFilter = new QFilter("lockstatus", QCP.equals, "A"); 
			e.getCustQFilters().add(qFilter);
		}
	}
	
	/**
	 * 读取业务应用的系统参数值
	 * @return
	 */
	private boolean cannotPushLockBill(){
		// 实际业务系统并没有这个选项，本演示代码，直接返回false，演示添加条件
		return false;
		//return (boolean)SystemParamterServiceHelper.getParameter(0, 0, "sal",    "cannotpushlockbill");
	}
}

```





### beforeGetSourceData 事件

#### 事件触发时机

系统读取源单数据前，触发此事件。

插件可以对取数`SELECT`子句、取数条件，做最后的修改。

#### 代码模板

```java
package kd.bos.plugin.sample.bill.billconvert.template;

import kd.bos.entity.botp.plugin.AbstractConvertPlugIn;
import kd.bos.entity.botp.plugin.args.BeforeGetSourceDataEventArgs;

public class BeforeGetSourceData extends AbstractConvertPlugIn {

	@Override
	public void beforeGetSourceData(BeforeGetSourceDataEventArgs e) {
		// TODO 在此添加业务逻辑
	}
}

```



#### 事件参数

- `public String getSelectSQL()`：取数SELECT子句，包含了全部源单字段及别名；
- `public List<QFilter> getQFilters()`：源单行内码筛选条件；



#### 示例

不建议在这个事件调整取数`SELECT`子句、取数条件；

类似需求，放在`afterBuildQueryParemeter`事件比较合适。

示例略。





### afterGetSourceData 事件

#### 事件触发时机

源单行数据读取完毕，并按照转换规则上的数据范围，对源单数据行进行筛选之后，触发此事件。

插件可以根据通过条件的源单行数据，获取其他定制的引用数据；

也可以直接替换掉系统自动获取到的源单行数据，改用插件自定读取的源单行数据；

#### 代码模板

```java
package kd.bos.plugin.sample.bill.billconvert.template;

import kd.bos.entity.botp.plugin.AbstractConvertPlugIn;
import kd.bos.entity.botp.plugin.args.AfterGetSourceDataEventArgs;

public class AfterGetSourceData extends AbstractConvertPlugIn {

	@Override
	public void afterGetSourceData(AfterGetSourceDataEventArgs e) {
		// TODO 在此添加业务逻辑
	}
}

```



#### 事件参数

-  `AfterGetSourceDataEventArgs` 
  - `public Map<String, DynamicProperty> getFldProperties()`：源单字段与字段别名映射字典，可以通过字段标识，取到字段属性对象，然后使用属性对象到源单数据行中取字段值；
  - `public List<DynamicObject> getSourceRows()`：通过了数据筛选条件的全部源单行；



#### 示例

暂无示例。



### beforeBuildGroupMode 事件

#### 事件触发时机

系统按照单据转换规则 – 分单策略，对读取到的源单行，进行分组前，触发此事件。

插件可以在此事件，调整分单依赖的字段，影响后续的分单。

单据转换规则的分单策略，把源单行进行分组，不同的组生成不同的目标单：

- 一对一：一张源单，生成一张目标单，即按照源单单据内码分组；
- 多对一：本次选择的全部源单，下推到一张目标单中，即按照常量值分组，全部单，都会分到一个组；
- 按规则分单：本次选择的全部源单行，按照分单字段值进行分组；

单据转换规则支持的分录行合并策略，把源单行进行分组，不同的组合并为一条目标单分录行：

- 一对一：一条源单分录行，生成一条目标单分录行，即按照源单分录行内码分组；
- 多对一：本次选择的全部源单行，合并为一条目标单分录行，即按常量分组，全部源单行，都回分到一个组；
- 按规则合并：本次选择的全部源单行，按照字段值进行分组；相同的字段值分为一组，合并为一条分录行；

正常情况下，可以配置转换规则 – 分单策略，控制分单、合并。

插件可以根据实际业务数据，在此事件中，动态调整分单、合并依赖的字段，从而影响后续的分单。

#### 代码模板

```java
package kd.bos.plugin.sample.bill.billconvert.template;

import kd.bos.entity.botp.plugin.AbstractConvertPlugIn;
import kd.bos.entity.botp.plugin.args.BeforeBuildGroupModeEventArgs;

public class BeforeBuildGroupMode extends AbstractConvertPlugIn {

	@Override
	public void beforeBuildGroupMode(BeforeBuildGroupModeEventArgs e) {
		// TODO 在此添加业务逻辑
	}
}

```



#### 事件参数

- `BeforeBuildGroupModeEventArgs` 
  - `public String getHeadGroupKey()`：现有的分单依据字段；
  - `public void setHeadGroupKey(String headGroupKey)` ：设置新的分单依据字段
  - `public String getEntryGroupKey()`：现有的单据体行合并依据字段；
  - `public void setEntryGroupKey(String entryGroupKey)`：设置单据体行合并依据字段；
  - `public String getSubEntryGroupKey()`：子单据体行合并字段；
  - `public void setSubEntryGroupKey(String subEntryGroupKey)`：设置子单据体行合并字段



#### 示例

暂无示例。



### beforeCreateTarget  事件

暂未触发。

### afterCreateTarget 事件

#### 事件触发时机

根据分单策略，创建好目标单据、单据体数据行之后，触发此事件；

此事件发生时，目标单各行数据包字段仅填写了默认值。后续代码会按照字段映射关系，携带源单字段值，覆盖默认值。

插件可以在此事件，对目标单字段设置默认值，或者根据源单字段值，动态生成目标单字段值。

#### 代码模板

```java
package kd.bos.plugin.sample.bill.billconvert.template;

import kd.bos.entity.botp.plugin.AbstractConvertPlugIn;
import kd.bos.entity.botp.plugin.args.AfterCreateTargetEventArgs;

public class AfterCreateTarget extends AbstractConvertPlugIn {

	@Override
	public void afterCreateTarget(AfterCreateTargetEventArgs e) {
		// TODO 在此添加业务逻辑
	}
}

```



#### 事件参数

- public class AfterCreateTargetEventArgs extends ConvertPluginEventArgs
  - public ExtendedDataEntitySet getTargetExtDataEntitySet()：生成的目标单扩展数据包；
  - public Map<String, DynamicProperty> getFldProperties() ：源单字段与源单行数据包属性对象映射字典，需要据此到源单行中取需要的字段值；



#### 示例

暂缺示例。



### afterFieldMapping 事件

#### 事件触发时机

按照字段映射配置，把源单字段值，填写到了目标单字段上之后，触发此事件。

插件可以在此事件中，对目标单字段值，进行修订、计算、汇总等，或者根据生成的目标单字段值，进行拆单、拆行。

本事件与`afterCreateTarget`事件的差别：

1. 本事件在`afterCreateTarget`事件之后触发；

2. `afterCreateTarget`事件发生时，目标单字段还没有赋值，适合赋默认值；

3. 本事件发生时，目标单字段已经根据字段映射关系，赋值完毕，适合字段值计算；

#### 代码模板

```java
package kd.bos.plugin.sample.bill.billconvert.template;

import kd.bos.entity.botp.plugin.AbstractConvertPlugIn;
import kd.bos.entity.botp.plugin.args.AfterFieldMappingEventArgs;

public class AfterFieldMapping extends AbstractConvertPlugIn {

	@Override
	public void afterFieldMapping(AfterFieldMappingEventArgs e) {
		// TODO 在此添加业务逻辑
	}
}

```



#### 事件参数

- `public class AfterFieldMappingEventArgs extends ConvertPluginEventArgs`
  - `public ExtendedDataEntitySet getTargetExtDataEntitySet()`：生成的目标单扩展数据包；
  - `public Map<String, DynamicProperty> getFldProperties()` ：源单字段与源单行数据包属性对象映射字典，需要据此到源单行中取需要的字段值；



#### 示例

参阅 `afterBuildQueryParemeter`事件示例：

在`afterFieldMapping`事件中，组合、格式化多个源单字段值，填写到目标单字段上。



### beforeCreateLink 事件

#### 事件触发时机

目标单字段值填写完毕，开始在目标单关联子实体中，记录源单信息之前，触发此事件。

插件可以在此事件，撤销记录源单信息。

#### 代码模板

```java
package kd.bos.plugin.sample.bill.billconvert.template;

import kd.bos.entity.botp.plugin.AbstractConvertPlugIn;
import kd.bos.entity.botp.plugin.args.BeforeCreateLinkEventArgs;

public class BeforeCreateLink extends AbstractConvertPlugIn {

	@Override
	public void beforeCreateLink(BeforeCreateLinkEventArgs e) {
		// TODO 在此添加业务逻辑
	}
}

```





#### 事件参数

- `public class BeforeCreateLinkEventArgs extends ConvertPluginEventArgs`
  - `public void setCancel(boolean cancel)`：取消记录关联关系；
  - `public ExtendedDataEntitySet getTargetExtDataEntitySet()`：生成的目标单扩展数据包；
  - `public Map<String, DynamicProperty> getFldProperties()` ：源单字段与源单行数据包属性对象映射字典，需要据此到源单行中取需要的字段值；



#### 示例

- 案例说明：

1. 根据动态条件，决定是否记录与源单的关联关系

- 实现方案：

1. 捕获`beforeCreateLink`事件

2. 判断条件，如果条件满足，则取消记录关联关系

- 实例代码：

  ```java
  package kd.bos.plugin.sample.bill.billconvert.bizcase;
  
  import kd.bos.entity.botp.plugin.AbstractConvertPlugIn;
  import kd.bos.entity.botp.plugin.args.BeforeCreateLinkEventArgs;
  
  public class BeforeCreateLinkSample extends AbstractConvertPlugIn {
  
  	@Override
  	public void beforeCreateLink(BeforeCreateLinkEventArgs e) {
  		e.setCancel(this.isCancelLink());
  	}
  	
  	private boolean isCancelLink(){
  		return true;
  	}
  }
  
  ```

  



### afterCreateLink 事件

#### 事件触发时机

系统在目标单关联子表，记录好源单信息、重算了反写控制字段值之后，触发此事件。

插件可以在此事件，根据目标单记录的源单信息，携带其他数据到目标单。

#### 代码模板

```java
package kd.bos.plugin.sample.bill.billconvert.template;

import kd.bos.entity.botp.plugin.AbstractConvertPlugIn;
import kd.bos.entity.botp.plugin.args.AfterCreateLinkEventArgs;

public class AfterCreateLink extends AbstractConvertPlugIn {

	@Override
	public void afterCreateLink(AfterCreateLinkEventArgs e) {
		// TODO 在此添加业务逻辑
	}
}

```



#### 事件参数

- `public class AfterCreateLinkEventArgs extends ConvertPluginEventArgs`
  - `public ExtendedDataEntitySet getTargetExtDataEntitySet()`：生成的目标单扩展数据包；
  - `public Map<String, DynamicProperty> getFldProperties()` ：源单字段与源单行数据包属性对象映射字典，需要据此到源单行中取需要的字段值；



#### 示例

参阅 `initVariable`事件示例，在`afterCreateLink`事件，根据来源单据取值计算单据头金额。



### afterConvert 事件

#### 事件触发时机

目标单据生成完毕，触发此事件。

这个事件，是最后触发的，至此，全部业务逻辑已经执行完毕。

插件可以在这个事件，对生成的目标单数据，进行最后的调整。

#### 代码模板

```java
package kd.bos.plugin.sample.bill.billconvert.template;

import kd.bos.entity.botp.plugin.AbstractConvertPlugIn;
import kd.bos.entity.botp.plugin.args.AfterConvertEventArgs;

public class AfterConvert extends AbstractConvertPlugIn {

	@Override
	public void afterConvert(AfterConvertEventArgs e) {
		// TODO 在此添加业务逻辑
	}
}

```



#### 事件参数

- `public class AfterConvertEventArgs extends ConvertPluginEventArgs`
  - `public ExtendedDataEntitySet getTargetExtDataEntitySet()`：生成的目标单扩展数据包；
  - `public Map<String, DynamicProperty> getFldProperties()` ：源单字段与源单行数据包属性对象映射字典，需要据此到源单行中取需要的字段值；

#### 示例

- 案例说明

1. 采购单据，转固定资产卡片时，需每个物品生成一张卡片，即按数量分单

2. 当前转换规则的分单策略，无法配置出此需求，只能插件开发

- 实现方案

1. 捕获`afterConvert`事件，复制新单：根据资产数量字段值，确认新单的复制次数；

- 实例代码

  ```java
  package kd.bos.plugin.sample.bill.billconvert.bizcase;
  
  import java.util.ArrayList;
  import java.util.List;
  
  import kd.bos.dataentity.entity.DynamicObject;
  import kd.bos.dataentity.utils.OrmUtils;
  import kd.bos.entity.ExtendedDataEntity;
  import kd.bos.entity.botp.plugin.AbstractConvertPlugIn;
  import kd.bos.entity.botp.plugin.args.AfterConvertEventArgs;
  
  public class AfterConvertSample extends AbstractConvertPlugIn {
  
  	private final static String FAREALCARD_ENTITYNAME = "fa_card_real";
  	
  	@Override
  	public void afterConvert(AfterConvertEventArgs e) {
  		
  		// 获取已生成的资产卡片
  		ExtendedDataEntity[] billDataEntitys = e.getTargetExtDataEntitySet().FindByEntityKey(FAREALCARD_ENTITYNAME);
  		
  		// 构造 ExtendedDataEntity 时需要的索引值
  		int dataIndex = billDataEntitys.length;
  		
  		List<ExtendedDataEntity> copyDataEntitys = new ArrayList<>();
  		for(ExtendedDataEntity billDataEntity : billDataEntitys){
  			
  			// 原始的资产数量
  			int qty = (int) billDataEntity.getValue("assetamount");
  			
  			// 将资产数量改为1
  			billDataEntity.setValue("assetamount", 1);
  			
  			// 来源分录拆分序号 从1开始
  			int splitSeq = 1;
  			billDataEntity.setValue("sourceentrysplitseq", splitSeq++);
  			
  			// 复制 （原始的资产数量 - 1）个卡片对象
  			for(int i = 1; i < qty; i++){
  				DynamicObject copyObj = (DynamicObject) OrmUtils.clone(billDataEntity.getDataEntity(), false, true);
  				copyObj.set("sourceentrysplitseq", splitSeq++);
  				
  				copyDataEntitys.add(new ExtendedDataEntity(copyObj, dataIndex++, 0));
  			}
  		}
  		
  		// 将复制出的单据，放入 targetExtDataEntitySet ，最终就会生成那么多的卡片
  		e.getTargetExtDataEntitySet().AddExtendedDataEntities(FAREALCARD_ENTITYNAME, copyDataEntitys);
  	}
  }
  
  ```

