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