You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
    }
}

注意事项

  1. 关联字段验证:确保ARTran与FSAppointmentDet的关联字段(如SrvOrdType、LineNbr)在数据同步时正确传递,可通过ARTran的扩展字段存储服务订单的类型和行号。
  2. 线程上下文:PXLongOperation会启动独立线程,所有事件处理器需在该线程内注入才能生效。
  3. 数据一致性:方案二直接在生成ARTran时赋值,能避免后续打开发票时的二次修正,数据一致性更优;方案一更适合需要在发票界面实时调整的场景。

内容的提问来源于stack exchange,提问作者MarkJoel60

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 18:58:10