Acumatica移除FSxARTran后,发票生成无法同步预约自定义字段值
解决Acumatica预约开票时自定义字段未继承预约层级设置的问题
问题背景
Acumatica新版本移除了FSxARTran投影,导致原有的服务订单与AR交易关联逻辑失效。我们自定义了UsrASGPrintCheck字段,存在于以下层级:
- Inventory Items(库存层级:设置发票打印默认值)
- Service Order行项(可覆盖库存默认)
- Appointment行项(可再次覆盖服务订单设置)
- ARTran行项(最终发票行取值)
当前功能异常点:点击Appointment的「RUN BILLING」按钮时,系统调用InvoiceAppointment()方法将预约明细同步至ARTran,再启动ARInvoiceEntry图,但生成的ARTran记录仅使用库存默认值,完全忽略预约层级的设置。
无法通过常规事件处理解决的原因:传递到ARInvoiceEntry时refNbr为空,无法关联到对应的预约FSSODET行;原依赖的FSxARTran投影已被移除,失去了关联查询的桥梁。
尝试的重写代码未生效:曾尝试重写InvoiceAppointment()并添加ARInvoiceEntry的RowPersisting处理器,但该处理器从未执行,无法关联服务订单主数据。
原InvoiceAppointment()源码
public PXAction<FSAppointment> invoiceAppointment; [PXButton] [PXUIField(DisplayName = "Run Billing", MapEnableRights = PXCacheRights.Select, MapViewRights = PXCacheRights.Select)] public IEnumerable InvoiceAppointment(PXAdapter adapter) { List<FSAppointment> list = adapter.Get<FSAppointment>().ToList(); List<AppointmentToPost> rows = new List<AppointmentToPost>(); if (!adapter.MassProcess) { SaveWithRecalculateExternalTaxesSync(); } if (ServiceOrderTypeSelected.Current != null && ServiceOrderRelated.Current != null && ServiceOrderTypeSelected.Current.PostTo == ID.Batch_PostTo.SO) { ValidateContact(ServiceOrderRelated.Current); } foreach (FSAppointment fsAppointmentRow in list) { // Acuminator disable once PX1008 LongOperationDelegateSynchronousExecution [compatibility with legacy code] PXLongOperation.StartOperation( this, delegate () { SetServiceOrderStatusFromAppointment(ServiceOrderRelated.Current, fsAppointmentRow, ActionButton.InvoiceAppointment); CreateInvoiceByAppointmentPost graphCreateInvoiceByAppointmentPost = PXGraph.CreateInstance<CreateInvoiceByAppointmentPost>(); graphCreateInvoiceByAppointmentPost.Filter.Current.PostTo = ServiceOrderTypeSelected.Current.PostTo == ID.SrvOrdType_PostTo.ACCOUNTS_RECEIVABLE_MODULE ? ID.Batch_PostTo.AR_AP : ServiceOrderTypeSelected.Current.PostTo; graphCreateInvoiceByAppointmentPost.Filter.Current.IgnoreBillingCycles = true; graphCreateInvoiceByAppointmentPost.Filter.Current.BranchID = fsAppointmentRow.BranchID; graphCreateInvoiceByAppointmentPost.Filter.Current.LoadData = true; if (fsAppointmentRow.ActualDateTimeEnd > Accessinfo.BusinessDate) { graphCreateInvoiceByAppointmentPost.Filter.Current.UpToDate = fsAppointmentRow.ActualDateTimeEnd; graphCreateInvoiceByAppointmentPost.Filter.Current.InvoiceDate = fsAppointmentRow.ActualDateTimeEnd; } graphCreateInvoiceByAppointmentPost.Filter.Insert(graphCreateInvoiceByAppointmentPost.Filter.Current); AppointmentToPost appointmentToPostRow = graphCreateInvoiceByAppointmentPost.PostLines.Current = graphCreateInvoiceByAppointmentPost.PostLines.Search<AppointmentToPost.refNbr>(fsAppointmentRow.RefNbr, fsAppointmentRow.SrvOrdType); if (appointmentToPostRow == null) { throw new PXSetPropertyException(TX.Error.DocumentCannotBeInvoiced, fsAppointmentRow.SrvOrdType, fsAppointmentRow.RefNbr); } rows = new List<AppointmentToPost> { appointmentToPostRow }; Guid currentProcessID = graphCreateInvoiceByAppointmentPost.CreateInvoices(graphCreateInvoiceByAppointmentPost, rows, graphCreateInvoiceByAppointmentPost.Filter.Current, adapter.QuickProcessFlow, false); if (graphCreateInvoiceByAppointmentPost.Filter.Current.PostTo == ID.SrvOrdType_PostTo.SALES_ORDER_MODULE || graphCreateInvoiceByAppointmentPost.Filter.Current.PostTo == ID.SrvOrdType_PostTo.SALES_ORDER_INVOICE) { foreach (PXResult<FSPostBatch> result in SharedFunctions.GetPostBachByProcessID(this, currentProcessID)) { FSPostBatch fSPostBatchRow = (FSPostBatch)result; graphCreateInvoiceByAppointmentPost.ApplyPrepayments(fSPostBatchRow); } } AppointmentEntry apptGraph = PXGraph.CreateInstance<AppointmentEntry>(); apptGraph.AppointmentRecords.Current = apptGraph.AppointmentRecords.Search<FSAppointment.refNbr> (fsAppointmentRow.RefNbr, fsAppointmentRow.SrvOrdType); if (!adapter.MassProcess || this.IsMobile == true) { using (new PXTimeStampScope(null)) { apptGraph.AppointmentPostedIn.Current = apptGraph.AppointmentPostedIn.SelectWindowed(0, 1); apptGraph.openPostingDocument(); } } }); } return list; }
尝试的重写代码(未生效)
public PXAction<FSAppointment> invoiceAppointment; [PXButton] [PXUIField(DisplayName = "Run Billing", MapEnableRights = PXCacheRights.Select, MapViewRights = PXCacheRights.Select)] public IEnumerable InvoiceAppointment(PXAdapter adapter) { PXGraph.InstanceCreated.AddHandler<ARInvoiceEntry>((g) => { g.RowPersisting.AddHandler<ARInvoice>((cache, e) => { var invoiceGraph = cache.Graph as ARInvoiceEntry; var transactions = invoiceGraph.Transactions.Select()?.FirstTableItems; ARInvoice invoice = (ARInvoice)e.Row; foreach (ARTran tran in transactions) { // Can use SetValueExt just like regular event handlers cache.SetValueExt<ARInvoiceExt.usrPrintFlag>(tran, false); } }); }); var result = Base.InvoiceAppointment(adapter); }
解决方案
核心问题分析
原重写代码失效的原因:InvoiceAppointment()内的开票逻辑是在PXLongOperation中执行的,这会启动新线程,而PXGraph.InstanceCreated仅对当前线程的图实例生效,导致注入的处理器无法被新线程中的ARInvoiceEntry调用。
方案一:重写openPostingDocument方法(推荐)
在打开ARInvoiceEntry时,获取当前预约的关联信息,直接更新ARTran的自定义字段:
public class AppointmentEntryExt : PXGraphExtension<AppointmentEntry> { public delegate void OpenPostingDocumentDelegate(); [PXOverride] public void openPostingDocument(OpenPostingDocumentDelegate baseMethod) { var postedDoc = Base.AppointmentPostedIn.Current; if (postedDoc != null && postedDoc.DocType == "INV" && !string.IsNullOrEmpty(postedDoc.RefNbr)) { // 获取当前预约的所有行项 var currentAppt = Base.AppointmentRecords.Current; var apptLines = PXSelect<FSAppointmentDet, Where<FSAppointmentDet.srvOrdType, Equal<Required<FSAppointmentDet.srvOrdType>>, And<FSAppointmentDet.refNbr, Equal<Required<FSAppointmentDet.refNbr>>>>>.Select(Base, currentAppt.SrvOrdType, currentAppt.RefNbr); // 注入ARInvoiceEntry的行选择事件处理器 PXGraph.InstanceCreated.AddHandler<ARInvoiceEntry>(arGraph => { arGraph.RowSelected.AddHandler<ARInvoice>((cache, e) => { ARInvoice invoice = e.Row as ARInvoice; if (invoice == null || invoice.RefNbr != postedDoc.RefNbr) return; foreach (ARTran tran in arGraph.Transactions.Select()) { // 通过ARTran关联的服务订单行号匹配预约行项 var tranExt = tran.GetExtension<ARTranExt>(); if (!string.IsNullOrEmpty(tranExt.UsrSOOrderType) && tranExt.UsrSOLineNbr.HasValue) { var apptDet = apptLines.FirstOrDefault(d => d.SrvOrdType == tranExt.UsrSOOrderType && d.LineNbr == tranExt.UsrSOLineNbr); if (apptDet != null) { var apptDetExt = apptDet.GetExtension<FSAppointmentDetExt>(); arGraph.Transactions.Cache.SetValueExt<ARTranExt.UsrASGPrintCheck>(tran, apptDetExt.UsrASGPrintCheck); } } } }); }); } baseMethod(); } // 重写InvoiceAppointment,确保基础流程正常执行 public PXAction<FSAppointment> invoiceAppointment; [PXButton] [PXUIField(DisplayName = "Run Billing", MapEnableRights = PXCacheRights.Select, MapViewRights = PXCacheRights.Select)] public IEnumerable InvoiceAppointment(PXAdapter adapter) { return Base.InvoiceAppointment(adapter); } }
方案二:扩展CreateInvoiceByAppointmentPost(在生成ARTran时直接赋值)
在创建ARTran的源头同步自定义字段,避免后续修正:
public class CreateInvoiceByAppointmentPostExt : PXGraphExtension<CreateInvoiceByAppointmentPost> { public delegate Guid CreateInvoicesDelegate(CreateInvoiceByAppointmentPost graph, List<AppointmentToPost> rows, FSPostFilter filter, bool quickProcessFlow, bool isMassProcess); [PXOverride] public Guid CreateInvoices(CreateInvoiceByAppointmentPost graph, List<AppointmentToPost> rows, FSPostFilter filter, bool quickProcessFlow, bool isMassProcess, CreateInvoicesDelegate baseMethod) { // 监听ARTran的插入事件,直接从预约行项取值 graph.RowInserted.AddHandler<ARTran>((cache, e) => { ARTran tran = e.Row as ARTran; if (tran == null) return; // 通过AppointmentToPost关联到预约行项 var apptToPost = rows.FirstOrDefault(r => r.SrvOrdType == tran.TranType && r.RefNbr == tran.RefNbr && r.LineNbr == tran.LineNbr); if (apptToPost != null) { var apptDet = PXSelect<FSAppointmentDet, Where<FSAppointmentDet.srvOrdType, Equal<Required<FSAppointmentDet.srvOrdType>>, And<FSAppointmentDet.refNbr, Equal<Required<FSAppointmentDet.refNbr>>, And<FSAppointmentDet.lineNbr, Equal<Required<FSAppointmentDet.lineNbr>>>>>.Select(graph, apptToPost.SrvOrdType, apptToPost.RefNbr, apptToPost.LineNbr); if (apptDet != null) { var apptDetExt = apptDet.GetExtension<FSAppointmentDetExt>(); cache.SetValueExt<ARTranExt.UsrASGPrintCheck>(tran, apptDetExt.UsrASGPrintCheck); } } }); return baseMethod(graph, rows, filter, quickProcessFlow, isMassProcess); } }
注意事项
- 关联字段验证:确保ARTran与FSAppointmentDet的关联字段(如
SrvOrdType、LineNbr)在数据同步时正确传递,可通过ARTran的扩展字段存储服务订单的类型和行号。 - 线程上下文:
PXLongOperation会启动独立线程,所有事件处理器需在该线程内注入才能生效。 - 数据一致性:方案二直接在生成ARTran时赋值,能避免后续打开发票时的二次修正,数据一致性更优;方案一更适合需要在发票界面实时调整的场景。
内容的提问来源于stack exchange,提问作者MarkJoel60
相关产品推荐
相关产品推荐

