Search code examples
c#acumatica

FSxARTran has been removed and I need it when creating Invoice from Appointment


A couple of revisions ago, Acumatica Deprecated and removed the FSxARTran projection.

I thought I had worked around all of the issues this created, but one came back to bite me recently.

The use case is that we have a custom field called UsrASGPrintCheck which is a field we added to Inventory Items, Service Order line items, Appointment Line items, and ARTran Line Items.

The idea is that the customer can specify at the inventory level if it should default to being printed on an invoice. It can be overridden at the Service Order level, and again at the appointment level.

I have that working part fine.

The issue is when thy go to print the invoice.

In the Appointment, when the “RUN BILLING” button is hit, it runs a method called InvoiceAppointment() which transfers the appointment detail to the ARTran database, and then runs the ARInvoiceEntry graph.

But it is creating the records there with the default value from inventory and is ignoring the Appointment setting.

I am not able to do it in an event, because when it is sent to ARInvoiceEntry it has in the refNbr.

Without a refNbr, I cannot get the appointment’s FSSODET line. I need to look up the associated appointment, but I don’t see that anywhere in the ARInvoiceEntry graph’s cache.

We used to use FSxARTran here as a cross reference, which would allow us to get the FSService Order Information that linked to the FSARtran, but that is gone now.

Does anyone have any idea how I can override the InvoiceAppointment() method, grabbing the appointment detail, and setting my flag before it brings the ARInvoiceEntry graph up to the user?

If it helps this is the InvoiceAppointment() method in the source code that gets called when the button is clicked.

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

Someone suggested overriding the method like this:

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

But the Handler that is added this way is never executed, so I have no place to place my code to check the Service Order master.

Any suggestions as to where I go from here?


Solution

  • I couldn’t figure out how to implement this solution (which was frustrating because, I think there is a way.)

    I went back to the use case which is:

    When a “Run Billing” action is selected from an appointment, it creates an Invoice, which is a master record of ARInvoice, with detail records copied from the AppointmentDet to ARTran records. There is a custom field on the AppointmentDet record called UsrPrintFlag, which the user may have set. I need to make sure the value of the AppointmentDet record’s UsrPrintFlag is copied to the ARTran’s usr field of the same name.

    The challenge was that when the AppointmentDet records are first copied over, the Invoice does not have a reference number, and the ARTRan records do not have most of the fields. The Appointment is not part of the ARInvoiceEntry graph,and there is no cache or link is available to find the Apppintment that created the ARInvoice. Therefore, overriding the RowInserted method is useless. (previously there was a projection called FSxARTran which carried the Appointment information, but it was deprecated and deleted several releases ago.)

    My solution is to put the logic in

    Events.FieldUpdated<ARTran, ARTran.inventoryID> e
    

    I reasoned that if they are creating a sparse ARTRan record, eventually the underlying method would have to copy the InventoryID over, since it would be required in the Invoice.

    It isn’t nice and clean, but it gives me a handle on things.

    Fortunately, the CustomerID and LineNbr are fields carried over into the sparse ARTRan records created by Insert. I also know that the “RUN BILLING” option does not appear to the user until the Appointment is closed. When the InventoryID is given to me, I am able to look up all “closed” appointments for the customer, and start working through them one by one (starting at the newest appointment which is probably the one we want.)

    I then work though the AppointmentDet lines, matching the Inventory ID and the LineNbr. If those match, I consider it a hit, and I use that AppointmentDet values to set the User defined fields as needed.

    I can come up with some scenarios where this theoretically might fail, but those don’t seem like reasonable possibilities in real life.

    In case anyone else runs into this, here is some sample code:

     protected virtual void _(Events.FieldUpdated<ARTran, ARTran.inventoryID> e)
            {
                if (e is null) return;
                if (!IsPrintFeatureEnabledForFS()) return;  // if not active, just leave
    
                var tran = e.Row as ARTran;
                // Best I can determine, if an Invoice is created for a SALES ORDER
                // the SOOrderType in ARTran is "SO" -- if it is a SERVICE ORDER
                // SOOrderType will == NULL 
                if (tran.SOOrderType != null)
                    return;
                ARTranExt rowExt = e.Row.GetExtension<ARTranExt>();
                if (rowExt == null) return;
                if (e.OldValue is null)
                {
                    if (IsPrintFeatureEnabledForFS())
                    {
    // Add Check to see what ScreenID created this record. We only care about FS300200
                        if (tran.CreatedByScreenID == "FS300200")
                        {
                            var ApptTransaction = FindMatchingAppointmentDetail(tran.CustomerID, tran.LineNbr, tran.InventoryID);
                            if (ApptTransaction != null)
                            {
                                FSAppointmentDetExt apDetExt = ApptTransaction.GetExtension<FSAppointmentDetExt>();
                                rowExt.UsrASGPrintCheck = apDetExt?.UsrASGPrintCheck;
                            }
                        }
    
                    }
                  }
               }
    

    And the magic function to find the appointment:

     private FSAppointmentDet FindMatchingAppointmentDetail(int? CustomerID, int? LineNbr, int? InventoryID)
            {
    
                // We want to see the Apppointment closed since that is where they "Run Billing" From
                var appointments = SelectFrom<FSAppointment>
                    .Where<FSAppointment.customerID.IsEqual<@P.AsInt>
                        .And<FSAppointment.status.IsEqual<@P.AsString>>>
                    .OrderBy<Desc<FSAppointment.actualDateTimeEnd>>
                    .View.Select(Base, CustomerID, "Z");  // "Z" for closed status, Billing would be 'B' 
    
    
                foreach (PXResult<FSAppointment> result in appointments)
                {
                    FSAppointment fsAppointment = result;
                    PXTrace.WriteInformation($"Checking Appointment: {fsAppointment.AppointmentID} (Date: {fsAppointment.ActualDateTimeEnd})");
    
                    // Get all appointment details for this appointment
    
                    var appointmentDetails = PXSelect<FSAppointmentDet,
                          Where<FSAppointmentDet.appointmentID, Equal<Required<FSAppointment.appointmentID>>,
                          And<FSAppointmentDet.lineNbr, Equal<Required<FSAppointmentDet.lineNbr>>>>>
                          .Select(Base, fsAppointment.AppointmentID, LineNbr);
    
    
                    foreach (FSAppointmentDet fsAppointmentDet in appointmentDetails)
                    {
                        // Check if LineNbr matches and InventoryID does, we have a match. This could possibly
                        // give us a false positive, but it seems HIGHLY unlikely.
    
                        if (fsAppointmentDet.InventoryID == InventoryID)
                        {
                            PXTrace.WriteInformation($"Match found! AppointmentID: {fsAppointment.AppointmentID}, LineNbr: {fsAppointmentDet.LineNbr}");
                            return fsAppointmentDet; // Return the first match (newest first)
                        }
                    }
                }
                PXTrace.WriteWarning("No matching appointment detail found.");
                return null; // No match found
            }
    

    This is working, at least for now.