2
votes

We want to accommodate our web subscription service into Acumatica, which means we sell a service as a subscription product that has starting date and expiration date, and we want to be able to enter the sale by adding sales order and then adding/changing an extra "contract" associated to that product to handle the subscription expiration/renewal issues.

Our idea is to somehow customize the sales order process to run some kind of check automatically every time when a sales order is completed - if a subscription product is in that order, we want a process to be triggered automatically to add/update a contract based on the order information.

Could it be done through customization?

Just want to mention, I have been working with Web Service API to integrate our e-commerce with Acumatica and I know I could implement this by polling the order table and then using web service API to add contract, however, it looks to me it would be better to do this inside Acumatica through some kind of customization if it is doable.

Does anybody know if this customization could be done and how to do it if it does?

Thanks.

Edited:

Having looked responses from @Gabriel and @Hybridzz, I have tried a piece of code as below:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Avalara.AvaTax.Adapter;
using Avalara.AvaTax.Adapter.TaxService;
using PX.CCProcessingBase;
using PX.Common;
using PX.Data;
using PX.Objects.AP;
using PX.Objects.AR;
using PX.Objects.CA;
using PX.Objects.CM;
using PX.Objects.CR;
using PX.Objects.CS;
using PX.Objects.EP;
using PX.Objects.GL;
using PX.Objects.IN;
using PX.Objects.PO;
using PX.Objects.TX;
using AvaMessage = Avalara.AvaTax.Adapter.Message;
using POLine = PX.Objects.PO.POLine;
using POOrder = PX.Objects.PO.POOrder;
using PX.Objects;
using PX.Objects.SO;
using PX.Objects.CT;

namespace PX.Objects.SO
{

  public class SOOrderEntry_Extension:PXGraphExtension<SOOrderEntry>
  {
    public delegate void PersistDelegate();
    [PXOverride]
    public void Persist(PersistDelegate baseMethod)
    {
        using (PXTransactionScope ts = new PXTransactionScope())
        {

           // Create, setup and activate contracts
           ContractMaint contractMaint = PXGraph.CreateInstance<ContractMaint>();
           CTBillEngine engine = PXGraph.CreateInstance<CTBillEngine>();
           //var tranExt = PXCache<ARTran>.GetExtension<ARTranExt>(tran);
           string contractCD = "1234567";
           DateTime startDate = new DateTime(2015,1,1);
           Contract contract = SetupActivateContract(contractMaint, contractCD, startDate , 13128,14330, engine);
        }
        baseMethod();
    }

private Contract SetupActivateContract(ContractMaint contractMaint, string contractCD, DateTime? invoiceDate, int? customerID, 
    int? customerLocationID, CTBillEngine engine)
{
    contractMaint.Clear();

    // Initialize new contract
    Contract contract = (Contract)contractMaint.Contracts.Cache.CreateInstance();
    contract.ContractCD = contractCD;
    contract = contractMaint.Contracts.Insert(contract);

    // Lookup contract template ID
    Contract template = PXSelect<Contract,
                            Where<Contract.isTemplate, Equal<boolTrue>, And<Contract.contractCD, Equal<Required<Contract.contractCD>>>>>
                        .Select(Base, "MMS");
    if (template == null) throw new PXException("The MMS contract template was not found.");

    // Set required fields
    contract.TemplateID = template.ContractID;
    contract.CustomerID = customerID;
    contract = contractMaint.Contracts.Update(contract);
    contract.LocationID = customerLocationID;
    contract.StartDate = invoiceDate;
    contract.ActivationDate = invoiceDate;
    ContractMaint.SetExpireDate(contract);
    contract = contractMaint.Contracts.Update(contract);

    // Save generated contract
    contractMaint.Save.Press();
    // Setup and activate the contract
    engine.SetupAndActivate(contract.ContractID, contract.ActivationDate);

     return contract;
   }
 }
}

The code was validated and published without any problem, however, when I tried to add a sales order, I didn't see any contract being added into database as I expected. I did add some "throw exception" statements to make sure this piece of code was actually called during the sales order process, but I just don't understand why the contract wasn't added.

Please note this is the first time I tried to do customization, although I have some experiences in web service API, there could be something basic that I wasn't aware of.

Any help would be appreciated.

2
It is indeed possible, and the upcoming T300 training include this specific example. I wrote the example and will retrieve it to post here!Gabriel
Thanks, @Gabriel! look forward to your example.Gladiator

2 Answers

4
votes

This topic is covered in the (yet to be published) customization training. The training is centered around a fictitious mobile phone company called "YogiFon". When release an invoice, system will check whether invoice contains an item with inventory code "SIMCARD", and setup the contract automatically as part of the release process. As part of this customization, two custom fields were added to the invoice lines, to have user input the phone number and SIM Card ID. These fields are stored with the contract attributes.

There are two graph extensions needed, one for the ARReleaseProcess graph, and another one for the SOInvoiceEntry graph. I wrote the original example, but credits goes to Ruslan Devyatko for reviewing it.

ARReleaseProcess extension:

public class ARReleaseProcess_Extension : PXGraphExtension<ARReleaseProcess>
{
    public bool SetupContract = false;

    public delegate void PersistDelegate();
    [PXOverride]
    public void Persist(PersistDelegate baseMethod)
    {
        // use ARDocument.Current
        ARRegister invoice = (ARRegister)Base.Caches[typeof(ARRegister)].Current;
        List<Contract> setupContracts = new List<Contract>();

        if (SetupContract)
        {
            // Create, setup and activate contracts
            ContractMaint contractMaint = PXGraph.CreateInstance<ContractMaint>();
            CTBillEngine engine = PXGraph.CreateInstance<CTBillEngine>();

            int seq = 1;

            //reuse ARTran_TranType_RefNbr from ARReleaseProcess
            foreach (ARTran tran in
                PXSelect<ARTran,
                    Where<ARTran.tranType, Equal<Required<ARInvoice.docType>>,
                        And<ARTran.refNbr, Equal<Required<ARInvoice.refNbr>>,
                        And<ARTranExt.usrSIMCardID, IsNotNull,
                        And<ARTranExt.usrContractID, IsNull>>>>,
                    OrderBy<Asc<ARTran.tranType, Asc<ARTran.refNbr, Asc<ARTran.lineNbr>>>>>.
                Select(Base, invoice.DocType, invoice.RefNbr))
            {
                // Create, setup and activate contract for a particular SOInvoice line
                var tranExt = PXCache<ARTran>.GetExtension<ARTranExt>(tran);
                string contractCD = String.Format("{0}{1:00}", invoice.RefNbr, seq);
                Contract contract = SetupActivateContract(contractMaint, contractCD, invoice.DocDate, invoice.CustomerID, 
                    invoice.CustomerLocationID, tranExt.UsrSIMCardID, tranExt.UsrPhoneNumber, engine);
                setupContracts.Add(contract);

                // Associate generated contract with the SOInvoice line
                tranExt.UsrContractID = contract.ContractID;
                Base.ARTran_TranType_RefNbr.Cache.Update(tran);

                seq++;
            }
        }

        baseMethod();
    }

    private Contract SetupActivateContract(ContractMaint contractMaint, string contractCD, DateTime? invoiceDate, int? customerID, 
        int? customerLocationID, string simCardID, string phoneNumber, CTBillEngine engine)
    {
        contractMaint.Clear();

        // Initialize new contract
        Contract contract = (Contract)contractMaint.Contracts.Cache.CreateInstance();
        contract.ContractCD = contractCD;
        contract = contractMaint.Contracts.Insert(contract);

        // Lookup contract template ID
        Contract template = PXSelect<Contract,
                                Where<Contract.isTemplate, Equal<boolTrue>, And<Contract.contractCD, Equal<Required<Contract.contractCD>>>>>
                            .Select(Base, "SIMCARD");
        if (template == null) throw new PXException("The SIMCARD contract template was not found.");

        // Set required fields
        contract.TemplateID = template.ContractID;
        contract.CustomerID = customerID;
        contract = contractMaint.Contracts.Update(contract);
        contract.LocationID = customerLocationID;
        contract.StartDate = invoiceDate;
        contract.ActivationDate = invoiceDate;
        ContractMaint.SetExpireDate(contract);
        contract = contractMaint.Contracts.Update(contract);

        // Store SIM/Phone Number into attributes
        foreach (CSAnswers attribute in contractMaint.Answers.Select())
        {
            switch (attribute.AttributeID)
            {
                case "SIMCARDID":
                    attribute.Value = simCardID;
                    contractMaint.Answers.Update(attribute);
                    break;
                case "PHONENUM":
                    attribute.Value = phoneNumber;
                    contractMaint.Answers.Update(attribute);
                    break;
            }
        }
        // Save generated contract
        contractMaint.Save.Press();
        // Setup and activate the contract
        engine.SetupAndActivate(contract.ContractID, contract.ActivationDate);

        return contract;
    }
}

SOInvoiceEntry extension:

public class SOInvoiceEntry_Extension : PXGraphExtension<SOInvoiceEntry>
{
    #region Event Handlers
    protected void ARTran_RowSelected(PXCache cache, PXRowSelectedEventArgs e, PXRowSelected InvokeBaseHandler)
    {
        if (InvokeBaseHandler != null)
            InvokeBaseHandler(cache, e);
        var row = (ARTran)e.Row;

        if (row == null) return;

        // The SIM Card ID and the Phone Number fields are only editable when the SIMCARD item is used
        // In real life you would have a flag in InventoryItem to indicate that, rather than hardcoding based on InventoryCD
        InventoryItem item = (InventoryItem)PXSelectorAttribute.Select<ARTran.inventoryID>(Base.Transactions.Cache, row);
        bool enableFields = item != null && item.InventoryCD.StartsWith("SIMCARD");
        PXUIFieldAttribute.SetEnabled<ARTranExt.usrSIMCardID>(cache, row, enableFields);
        PXUIFieldAttribute.SetEnabled<ARTranExt.usrPhoneNumber>(cache, row, enableFields);
    }
    #endregion

    public PXAction<ARInvoice> release;
    [PXUIField(DisplayName = "Release", Visible = false)]
    [PXButton()]
    public IEnumerable Release(PXAdapter adapter)
    {
        PXGraph.InstanceCreated.AddHandler<ARReleaseProcess>((graph) =>
        {
            // Create, setup and activate contracts while releasing SOInvoice
            graph.GetExtension<ARReleaseProcess_Extension>().SetupContract = true;
        });
        return Base.release.Press(adapter);
    }
}
0
votes

You can override the Persist of the salesorder graph SOOrderEntry

[PXOverride]
public void Persist(Action persit)
{
    using (PXTransactionScope ts = new PXTransactionScope())
    {
       persit(); // this will call base graph Persist();
       //If no error the document save is completed, but still wrapped in a transaction and you can do your logic below this
    }
}