最近遇到一个直接交运过账的问题,应该是AX2009系统的bug。
背景:
如果需要把采购的物料直接由供应商交付给客户,需要用到直接交运功能,AX2009的实现方式是采购订单收货的地址直接写客户的地址,在采购订单过账装箱单时自动过账销售订单的装箱单。
问题重现:
环境:
操作系统:Windows 2003
AX2009: SP1+RU7 5.0.1500.4570
数据:使用AX2009的DEMO数据库的CEU公司
1.创建一个销售订单

2.创建销售订单行
选择1000和5008两个物料

3.设置销售订单为直接交运

4.创建直接交运的采购订单,功能->创建直接交运
5.自动产生两个采购订单,分别对应销售订单的一行

6.通过应付账款->期间->过账装箱单,同时过账两个采购订单

7.点击 确定,问题出现如下:

原因分析:
整个过程采用的都是AX2009的标准功能,从整个操作看也没有什么不妥之处,从业务上看,创建销售订单,直接交运,两个供应商同时发货给了客户,于是在系统里选择这两个采购订单过账装箱单,这个在业务上也是合情合理的,但是AX2009为什么给出了这么一个奇怪的报错,超交?让人有点丈二和尚。
1.查看采购订单和销售订单行的过账情况:
采购订单:

销售订单:

从上面两个图看,第一个采购订单000385及其对应的销售订单行已经顺利过账,事务已经提交,问题出在过账第二个采购订单时。从系统的报错看,过账第二个采购订单的时候,它在尝试过账1000这个物料,但是第二个采购订单关联的物料是5008,这个采购订单跟1000物料没有一毛钱的关系。
从这里猜测看应该是在过账第二个采购订单关联的销售订单时错误地选择了销售订单行,把本不应该过账的1000再次过账了,其实只过账5008这个行就可以了。
代码分析
AX2009的代码称这种直接交运的过账为internalPosting,因为这个过账是在同一个公司下,对于不同公司间交易的过账称之为externalPosting。
过账采购订单对应的销售订单的代码在PurchFormLetter_PackingSlip的intercompanyPost,如果采购订单是直接交运的,就会调用InterCompanyPostSales过账相应的销售订单.
接下去看类InterCompanyPost的formletterCollect方法,整个方法执行后的最终结果是找到需要过账的销售订单,查找的逻辑是根据VendPackingSlipTrans的
InventRefTransId找到相应的销售订单行,进而得到销售订单。至于VendPackingSlipTrans 的InventRefTransId是从哪里来的,就要去看PurchFormLetter_PackingSlip的writeJournlLine方法了,该方法中有initFormPurchLine的调用,也就是说这个InventRefTransId是从PurchLine的InventRefTransId来的,而PuchLine的InventrefTransId是创建直接交运的时候产生的,整个值也就是对应的销售订单行的InventTransId.
从这段代码看,只是找到了需要过账的销售订单并将其放到Map中,但并未限定只过账本次过账的采购订单对应的行。
接下去看,是否还有地方可以限定,interCompanyPost最终会调用SalesFormLetter的update方法,这个方法是用来过账销售订单的简单的API,该方法会构造出适当的SalesParmUpdate,SalesParmTable,SalesParmLine等记录,用来过账销售订单。
那么产生这些记录的时候会不会有限定只过账本次采购订单对应的销售订单行的语句呢?限定的语句在SalesFormLetter的updateQueryBuild方法中:
if (interCompanyMap)
{
mapSalesRecordEnumerator = interCompanyMap.getEnumerator();
while (mapSalesRecordEnumerator.moveNext())
{
localSalesTable = mapSalesRecordEnumerator.currentValue();
chooseLines.query().dataSourceTable(tablenum(SalesTable)).addRange(fieldnum(SalesTable, SalesId)).value(queryValue(localSalesTable.SalesId));
noSelected++;
}
this.callerFormDataSource(null);
if (noSelected > 1)
this.defaultGiroType(NoYes::Yes);
else
this.defaultGiroType(NoYes::No);
}
可以看出这段方法就是把interCompanyPost的formletterCollect得到的SalesId在这里做了限定,也就是让SalesFormLetter只过帐采购订单对应的销售订单。
但是依然没看到过滤销售订单行,让只跟本次过账的采购订单关联的销售订单行过账。
这样错过了第二次机会。
这样就只能依靠在正式过账前的最后一次过滤来限定了,这个过滤发生在创建SalesParmLine时,因为要判断本次销售订单行要过账的数量,如果能在这个时点过滤也是可以的。
创建SalesParmLine的方法是在SalesFormLetter的CreateParmLine方法
if (interCompanyParmId
&& !this.interCompanyParmSelectFromJournal())
{
localSalesParmLine.Closed = this.interCompanyParmLineClosed(_salesLineOrig);
[localSalesParmLine.DeliverNow, localSalesParmLine.RemainBefore , localSalesParmLine.RemainAfter] = this.qtySales (_salesLineOrig, this.interCompanyParmLineQty(_salesLineOrig));
[localSalesParmLine.InventNow , localSalesParmLine.RemainBeforeInvent, localSalesParmLine.RemainAfterInvent] = this.qtyInvent (_salesLineOrig, this.interCompanyParmLineQty(_salesLineOrig, true));
salesLine.SalesDeliverNow = localSalesParmLine.DeliverNow;
salesLine.setInventDeliverNow();
if (localSalesParmLine.Closed)
{
localSalesParmLine.RemainAfter = 0;
localSalesParmLine.setRemainAfterInvent();
}
}
其中决定本次要过账数量的方法是interCompanyParmLineQty,过账装箱单数量的方法在SalesFormLetter_PackingSlip
select forceplaceholders InventQty, Qty from vendPackingSlipTransLocal
where vendPackingSlipTransLocal.InventTransId == salesLineLocal.InventRefTransId
exists join vendPackingSlipJourLocal
where vendPackingSlipJourLocal.PackingSlipId == vendPackingSlipTransLocal.PackingSlipId
&& vendPackingSlipJourLocal.DeliveryDate == vendPackingSlipTransLocal.DeliveryDate
&& vendPackingSlipJourLocal.PurchId == vendPackingSlipTransLocal.PurchId
&& vendPackingSlipJourLocal.InternalPackingSlipId == vendPackingSlipTransLocal.InternalPackingSlipId
&& vendPackingSlipJourLocal.ParmId == interCompanyParmId;
这个语句是查询vendPackingSlipTrans表里的记录,条件是inventTransId等于salesLine的InventRefTransId并且ParmId等于过账采购订单时产生的ParmId。
这个语句可以过滤只过账当前采购订单对应的销售订单行吗?
不能。
因为是同时过账两个采购订单000385和000386,按照AX2009的过账逻辑和数据结构,过账时用的是同一个parmId,这样不论是1000还是5008,在过账第二个采购订单时,上面的语句都会得到过账的数量,这样1000就重复过账了,于是提示超交了。
这样三个地方可以过滤只过账当前过账的采购订单对应的销售订单行的地方都没过滤,于是问题产生了。
解决方法
分析原因后就比较容易解决了,在上述三个地方的任意一个地方进行过滤就可以了。IntercompanyPost类修改比较麻烦,在CreateParmLine的修改属于事后补救,所以感觉在SalesFormLetter的updateQueryBuild这个地方过滤比较好。
出差费用管理模块主要分为两个部分 支出和预付,一般公司称之为报销和请款,也就是出差之类费用的报销和出差前向公司预支一部分现金。AX的出差费用管理模块基本能满足一般公司的请款和报销需求。
一般业务上需要满足如下功能:
1.提供界面让用户可以输入请款和报销
2.提供审批功能
涉及到公司钱的问题,一般需要审批
3.请款和报销需要在总帐上有所体现,也就是生成相应的日记帐和凭证
4.请款和报销需要核销,正如预收和应收需要核销一样。
第1点和第2点介绍跟其他模块没啥大的区别,这里就不赘述了,唯一的区别可能就是出差费用管理模块的输入只能在EP上做。
对于第3点,与其它模块一样,生成的日记帐的借贷科目需要提前设好,不可能要求用户在录入单据的时候自己去选择费用科目,用户更习惯选择的是费用类别之类,至于对应哪个科目是财务要关心的事情,于是很自然借方的科目就有地方设置了,嗯,需要在费用类别那地方挂一个费用科目
AX的操作路径:
出差费用管理->设置->支出条目->支出类别

接下来就是贷方了,贷方也就是这个费用是谁来承担的,一般情况下我们员工报销应该是员工承担了这部分费用,所以一般贷记员工的应付就可以了。
AX费用管理模块如果要贷记员工,需要在员工窗体关联一个供应商帐户,每个员工对应一个供应商帐户,虽然不是很喜欢这种处理方式,因为相当于所有的员工要对应一个供应商帐户,怎么看怎么别扭,但是为了使用标准的应付模块来给员工付款,也想不到更好的实现方式,如果用财务维度做辅助核算,每个公司对维度的使用情况又不同,很难做到标准化,于是这种笨拙的方式也是无奈之举。
出差费用管理->设置->员工详细信息

当然除了贷记员工外,还可以由公司或者第三方供应商帮忙承担了费用,这样的话就需要贷记公司的银行账户或者第三方供应商账户,到底贷记员工还是公司或者第三方供应商,这个也应该有个地方定义。
操作路径:出差费用管理->设置->支出条目->付款方式

付款方式里 支出所有者 这个字段只是个描述字段不起决定作用,重要的是对方科目类型和对方科目。
员工:如果选择了员工,则对方科目不能选择,因为系统会直接贷记到在员工窗体设置的供应商。
分类帐:选择了分类帐,需要指定一个过账科目,这样就会贷记到一个固定的会计科目。
银行:如果是公司支付了费用,则指定一个银行科目,生成的凭证会直接贷方会记到指定的银行科目。
所以通常情况下,对方科目类型选择员工就可以了。
在支出类别处关联一个默认的付款方式,这样只要用户选择一个费用类别,要生成凭证的借方和贷方科目就都有了。
另外,在安装完EP后,默认的预付功能不能用,说找不到页面之类,这是因为安装程序没有正确创建预付和预付审批对应的子站点导致的,在AOT上重新部署一下就可以了。

在前面介绍的一篇文章中介绍了用代码启动SSIS包的方法,http://www.cnblogs.com/Farseer1215/archive/2010/11/08/1871464.html,其中第二种方法必须在安装了SSIS组件的服务器上运行.
如果这样处理的实用性不是很强,不可能在每一台客户机上都装一个SSIS组件,所以最好的办法是在安装了SSIS组件的服务器上安装一个服务,其他客户端调用即可,于是可以考虑用WCF写一个小程序来运行SSIS包,其他的客户端直接调用服务。
1.创建WCF服务
在VS2010中选择创建WCF服务库,这时VS会用WCF库的模板创建一个项目,配置文件,接口和类实现都已经创建好,唯一要做的是事情是实现自己的逻辑。
要想启动SSIS需要用到ManagedDTS这个类库,添加对Microsoft.SqlServer.ManagedDTS这个DLL 的引用。
在接口定义中定义相应的操作和数据契约,在类中实现这些接口,定义了如下两个方法:
public SSISPackageResult ExecSSISPackage(string _packageName)
{
SSISPackageResult result = new SSISPackageResult();
try
{
Application application = new Application();
Package package = application.LoadFromSqlServer(_packageName, ".", null, null,null);
result.Execresult = package.Execute();
if (result.Execresult == DTSExecResult.Failure || result.Execresult == DTSExecResult.Canceled)
result.ExecInfo = string.Format("SSIS包{0}运行失败,请查看包运行日志!",_packageName);
else
result.ExecInfo = string.Format("SSIS包{0}运行成功",_packageName);
}
catch (Exception ex)
{
result.Execresult = DTSExecResult.Failure;
result.ExecInfo = string.Format("SSIS包{0}运行失败,具体异常信息为:"+ex.Message.ToString());
}
return result;
}
public SSISPackageResult ExecSSISPackageWithParam(string _packageName, string _variableName, string _variableValue)
{
SSISPackageResult result = new SSISPackageResult();
try
{
Application application = new Application();
Package package = application.LoadFromSqlServer(_packageName, ".", null, null,null);
package.Variables[_variableName].Value = _variableValue;
result.Execresult = package.Execute();
if (result.Execresult == DTSExecResult.Failure || result.Execresult == DTSExecResult.Canceled)
result.ExecInfo = string.Format("SSIS包{0}运行失败,请查看包运行日志!",_packageName);
else
result.ExecInfo = string.Format("SSIS包{0}运行成功",_packageName);
}
catch (Exception ex)
{
result.Execresult = DTSExecResult.Failure;
result.ExecInfo = string.Format("SSIS包{0}运行失败,具体异常信息为:"+ex.Message.ToString());
}
return result;
}
2.创建承载WCF服务的Windows服务
WCF服务的宿主可以是IIS,Windows Services,Windows Form甚至控制台命令程序,考虑到并不是所有服务器都会安装IIS,SRS2008也不再依赖于IIS,Windows Form和控制台命令程序控制起来不方便,所以采用Windows Services做为WCF服务的宿主程序。
在解决方案中加入一个新的项目WS_SSIS,项目模板采用Windows服务,添加对项目WCF_SSIS和System.ServiceModel的引用,在OnStart方法中添加启动WCF服务的代码:
ServiceHost serviceHost = new ServiceHost(typeof(SSISService));
serviceHost.Open();
因为System.Configuration不支持库的配置文件为服务增加一个配置文件,把WCF服务的配置文件内容拷贝过来。
为了安装服务,增加如下类,需要添加对程序集System.Configuration.Install的引用:
[RunInstaller(true)]
public class SSISServicesInstaller:Installer
{
private ServiceProcessInstaller process;
private ServiceInstaller service;
public SSISServicesInstaller()
{
process = new ServiceProcessInstaller();
process.Account = ServiceAccount.LocalSystem;
service = new ServiceInstaller();
service.ServiceName = "SSIS Service";
service.Description = "该服务用来执行SSIS包完成相应的数据整合任务";
Installers.Add(process);
Installers.Add(service);
}
}
这样在用InstallUtil.exe安装服务时会自动添加相应的服务,在Win7或者Windows2008上安装服务时要注意使用管理员运行InstallUtil,否则会出错。
运行后会在服务中添加SSISService:

安装时让SSIS服务木运行在本地服务帐户下,可根据实际情况修改服务的运行帐户,让其可以正确访问SSIS,然后启动服务。
3.创建WCF客户端调用WCF服务完成操作
可以用多种方法来实现在AX中调用WCF服务,可以先用C#写个DLL,使用添加服务生成代理类,然后用代理类调用WCF服务,然后在AX中引用该DLL。AX2009也支持直接添加服务引用,自动生成代理类供调用WCF服务使用。
AOT->References->添加服务引用

填写正确的信息:
其中WSDL URL就是WCF向外提供服务的URL地址,具体的详细信息参见WCF的相关知识。
.NET 代码命名空间在AX生成的代理类的命名空间,这个可以按照自己的习惯随便取名。
点击确定:

这样正确生成了调用WCF服务需要的客户端程序。
4.调用服务
写一个类调用WCF服务
static server void main(Args _args)
{
InterOpPermission interop = new InterOpPermission(InteropKind::ClrInterop);
SSISClient.ISSISServiceClient ssisServiceClient;
SSISClient.SSISPackageResult packageResult;
str bb;
;
interop.assert();
ssisServiceClient = new SSISClient.ISSISServiceClient();
packageResult = ssisServiceClient.ExecSSISPackage("InventTable");
info(packageResult.get_ExecInfo());
CodeAccessPermission::revertAssert();
}
运行结果如下:

这是个很有趣的更新,虽然不是必需的,但确实会给开发带来一定的便利,统一代码的书写方式。
在ERP中很多数据是时间相关的,比如贸易协定,汇率,利息等,都有一个有效期的概念。
一般我们的处理方式是增加两个字段FromDate和ToDate,用来表示这个当前记录的有效期间,然后在ValidateWrite或者insert方法中通过代码防止多条记录间日期重叠和间隔的问题。
AX2012在系统层面增加了对这个功能的支持,在表的属性中增加了ValidTimeStateFieldType,有些类似于现有的CreatedBy之类的属性,选择了之后会自动创建ValidFrom和ValidTo两个字段,结合索引的使用,可以自动对日期的重叠和间隔做处理。
虽然增加了一些平台本身的复杂度,不过可以统一代码,对这类问题有个统一的处理方式,也算是个不错的更新。