How can I set a custom field on Acumatica Production Detail Operation when the Production Order is first created? - acumatica

I have a custom field on an Acumatica Production Detail Operation (UsrEligibleForRoboticFulfillment) that I have created an Action to set based on criteria on the component items in the Materials tab. (code below)
I would like to call this Action to set the field as soon as the Production Order is created, but the split nature of the Production Order is such that there are no events on the Production Detail raised that I can attach to and call the Action. I've tried Row Inserted as well as Persist delegate on the Production Detail graph.
I CAN attach to either the AMProdItem Row Inserted or Persist Delegate on the Production Maint graph, but at this point in time the Operations and Materials have not yet been created.
What's the best way to update this field when a new Production Order is created?
Action code:
public PXAction<AMProdItem> UpdateEligibleForRoboticFulfillment;
[PXButton(CommitChanges = true)]
[PXUIField(DisplayName = "Update Robotic Eligibility", MapEnableRights = PXCacheRights.Select, MapViewRights = PXCacheRights.Select)]
protected void updateEligibleForRoboticFulfillment()
{
AMProdItem prodDetail = Base.ProdItemRecords.Current;
AMProdOper prodOper = Base.ProdOperRecords.Current;
InventoryItem finishedProduct = PXSelect<InventoryItem,
Where<InventoryItem.inventoryID, Equal<Current<AMProdItem.inventoryID>>>>.Select(Base).FirstOrDefault();
//Only production orders are eligible for robotic fulfillment, not disassemblies
if (prodDetail.OrderType == "MO")
{
bool wasRoboticsEligible = (prodOper.GetExtension<AMProdOperExt>().UsrEligibleForRoboticFulfillment ?? false);
//Get the current branchID
int branchID = (int)Base.Accessinfo.BranchID;
//Get the default site/warehouse for this branch.
INSite site = INTranHelper.GetDefaultSiteForItemBranch(branchID);
//Get the flag indicating whether this site is active for robotics
bool activeRobotics = site.GetExtension<INSiteExt>().UsrActiveRobotics ?? false;
//Get the flags for manual process and component robotics compatible
bool requiresManualProcess = finishedProduct.GetExtension<InventoryItemExt>().UsrManualFinishRequired ?? false;
//Gotta be prepared for the possibility that more than one component is used
//Check for any components that are NOT robotics eligible that have qty required and haven't already been fully allocated
PXResultset<AMProdMatl> components = PXSelectJoin<AMProdMatl,
InnerJoin<InventoryItem, On<InventoryItem.inventoryID, Equal<AMProdMatl.inventoryID>,
And<InventoryItemExt.usrRoboticsCompatible, Equal<False>,
And<AMProdMatl.orderType, Equal<Current<AMProdOper.orderType>>,
And<AMProdMatl.prodOrdID, Equal<Current<AMProdOper.prodOrdID>>,
And<AMProdMatl.operationID, Equal<Current<AMProdOper.operationID>>,
And<AMProdMatl.qtyActual, Less<AMProdMatl.totalQtyRequired>,
And<AMProdMatl.qtyReq, Greater<decimal0>>>>>>>>>>
.Select(Base);
bool roboticsEligible = !requiresManualProcess && activeRobotics;
//If any component is not eligible, the whole operation is not eligible
if (components.Count > 0)
{
roboticsEligible = false;
}
//If the robotics eligible flag should have changed, change it
if (wasRoboticsEligible != roboticsEligible)
{
prodOper.GetExtension<AMProdOperExt>().UsrEligibleForRoboticFulfillment = roboticsEligible;
Base.ProdOperRecords.Update(prodOper);
}
}
}

Had to open a ticket with Acumatica; got a working solution! I had to enclose the persist delegate method in a transaction scope.
Override Persist() method of graph
Call base method first so that Operations and Materials on the Production Order Detail gets created
Enclosed in transaction scope
Something like this:
public delegate void PersistDelegate();
[PXOverride]
public void Persist(PersistDelegate baseMethod)
{
if (/**/)
{
using (var ts = new PXTransactionScope())
{
//Call base method to persist
baseMethod();
/*Custom Logic here*/
ts.Complete();
}
}
else
baseMethod();
}

Related

While deleting the customer, how can i set null value for the custom field for the assigned contacts in Acumatica

I have tried multiple ways, but getting Another process error in the default version of Acumatica 19.106.0020
On top of it i have a customized code on both customer and contact screen, my requirement to clear the value of the custom field that is created in contact table when customer is deleting from the screen AR303000 i need to set null value of the custom field for the deleted contact from the customer.
i have tried by setting value on Customer_RowDeleting event but continuously getting Another process error, below is the screenshot error
Below is the code that i was tried
protected virtual void Customer_RowDeleting(PXCache sender, PXRowDeletingEventArgs e, PXRowDeleting BaseEvent)
{
BaseEvent?.Invoke(sender, e);
Customer rows = e.Row as Customer;
if (rows == null)
return;
if (Base.BAccount.Cache.GetStatus(Base.BAccount.Current) == PXEntryStatus.Deleted)
{
foreach (Contact BACT in PXSelectReadonly<Contact,
Where<Contact.bAccountID, Equal<Required<Contact.bAccountID>>,
And<Contact.contactType, NotEqual<ContactTypesAttribute.bAccountProperty>>>>.Select(Base, rows.BAccountID))
{
ContactMaint congraph = PXGraph.CreateInstance<ContactMaint>();
Contact CTData = PXSelectReadonly<Contact,
Where<Contact.contactID, Equal<Required<Contact.contactID>>>>.Select(Base, BACT.ContactID);
if (CTData != null)
{
congraph.Contact.Current = CTData;
if (congraph.Contact.Current != null)
{
congraph.Contact.SetValueExt<ContactExt.usrKWBAccountId>(congraph.Contact.Current, null);
congraph.Contact.Update(congraph.Contact.Current);
congraph.Save.Press();
}
}
}
}
}
Thanks in advance.
Hi Chris, please find the attached image here
I don't recommend to create graphs during RowDeleting event. If you have Acuminator installed, you will see a warning about creating graphs in event handlers.
Instead, call your custom code during the Persist method. Persist method is called during Delete operation. After the Base persist is finished, your custom code can perform it's work. Try something like this
public class CustomerMaint_Extension : PXGraphExtension<CustomerMaint>
{
public delegate void PersistDelegate();
[PXOverride]
public void Persist(PersistDelegate baseMethod)
{
Customer currentCustomer = Base.CurrentCustomer.Current; //get the customer record before it's deleted, i.e. Customer.bAccountID
baseMethod(); //let the base delete process happen first
if (Base.CurrentCustomer.Cache.GetStatus(currentCustomer) == PXEntryStatus.Deleted)
{
using (PXTransactionScope ts = new PXTransactionScope())
{
//here is where you add your code to delete other records
ts.Complete(); //be sure to complete the transaction scope
}
}
}
}
Also you might want to unpublish other customization packages, and see if the error continues without those packages. That is one way to determine the source of the error...by process of elimination.

How can I restrict Site ID (Whse) selection to only the current branch?

While it is easy in thought to just limit records to INSite.branchID = AccessInfo.branchID, the need is a bit more complex. I thought that I found a simple solution when looking at the DAC for INTran to find:
#region SiteID
public abstract class siteID : PX.Data.BQL.BqlInt.Field<siteID> { }
protected Int32? _SiteID;
[IN.SiteAvail(typeof(INTran.inventoryID), typeof(INTran.subItemID))]
[PXDefault(typeof(INRegister.siteID))]
[PXForeignReference(typeof(FK.Site))]
[InterBranchRestrictor(typeof(Where<SameOrganizationBranch<INSite.branchID, Current<INRegister.branchID>>>))]
public virtual Int32? SiteID
{
get
{
return this._SiteID;
}
set
{
this._SiteID = value;
}
}
#endregion
which has an intriguing attribute for InterBranchRestrictor. After a little digging, I found this attribute actually is used rather widely in Acumatica, but it appears to be limited to only Report Graphs and enabling the feature for Inter-Branch Transactions. Easy enough, I enabled the feature and tried an Inventory Issue again. No luck. I still could select a site id for a different branch.
So far, I only have limited control by creating a graph extension on INIssueEntry to set and validate the site ID. But what I really want is to limit the selector to only site id's of the current branch.
protected void _(Events.FieldDefaulting<INTran.siteID> e)
{
PXResultset<INSite> Results = PXSelect<INSite, Where<INSite.branchID, Equal<Current<AccessInfo.branchID>>>>.Select(Base);
if (Results.Count == 1)
{
foreach (PXResult<INSite> result in Results)
{
INSite site = result;
e.NewValue = site.SiteID;
e.Cancel = true;
}
}
}
protected void _(Events.FieldVerifying<INTran.siteID> e)
{
int? siteID = (int?)e.NewValue;
INTran row = (INTran)e.Row;
INSite site = PXSelect<INSite, Where<INSite.siteID, Equal<Required<INSite.siteID>>,
And<INSite.branchID, Equal<Current<AccessInfo.branchID>>>>>.Select(Base, siteID);
if(siteID != null && site?.SiteID == null)
{
PXUIFieldAttribute.SetError<INTran.siteID>(e.Cache, row, "Invalid Warehouse for Branch");
}
}
I really want to leverage what it seems like
[InterBranchRestrictor(typeof(Where<SameOrganizationBranch<INSite.branchID, Current<INRegister.branchID>>>))]
does, but clearly I just don't understand what it does. Alternatively, if I could add a where clause to
[IN.SiteAvail(typeof(INTran.inventoryID), typeof(INTran.subItemID))]
then I could restrict the list to the current branch that way, but I'm struggling to make that work as well. (Problems around implementing the PXForeignReference attribute in the extension needed to override the field definition.)
How can I restrict (in a manner that can be replicated efficiently throughout Acumatica) branch specific records to only site ID's of the current branch?
The InterBranchRestrictorAttribute is checking the graph to be working from a Report or the Inter-Branch Transactions feature to be turned on in the IsReportOrInterBranchFeatureEnabled method, so you need to remove this from your implementation.
You can write your own PXResrictorAttribute in the way shown in the example below:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]
public class CustomRestrictorAttribute: PXRestrictorAttribute
{
public CustomRestrictorAttribute(Type where) : base(CustomRestrictorAttribute.EmptyWhere, "Restrictor Message.", Array.Empty<Type>())
{
this._interBranchWhere = where;
}
protected override BqlCommand WhereAnd(PXCache sender, PXSelectorAttribute selattr, Type Where)
{
return base.WhereAnd(sender, selattr, this._interBranchWhere);
}
private static readonly Type EmptyWhere = typeof(Where<True, Equal<True>>);
protected Type _interBranchWhere;
}
And apply it to the DAC field like below:
[SiteAvail(typeof(SOLine.inventoryID), typeof(SOLine.subItemID))]
[PXParent(typeof(Select<SOOrderSite, Where<SOOrderSite.orderType, Equal<Current<SOLine.orderType>>, And<SOOrderSite.orderNbr, Equal<Current<SOLine.orderNbr>>, And<SOOrderSite.siteID, Equal<Current2<SOLine.siteID>>>>>>), LeaveChildren = true, ParentCreate = true)]
[PXDefault(PersistingCheck = PXPersistingCheck.Nothing)]
[PXUIRequired(typeof(IIf<Where<SOLine.lineType, NotEqual<SOLineType.miscCharge>>, True, False>))]
[InterBranchRestrictor(typeof(Where2<SameOrganizationBranch<INSite.branchID, Current<SOOrder.branchID>>,
Or<Current<SOOrder.behavior>, Equal<SOBehavior.qT>>>))]
[CustomInterBranchRestrictor(typeof(Where<INSite.branchID,Equal<Current<SOOrder.branchID>>>))]
protected virtual void SOLine_SiteID_CacheAttached(PXCache cache)
{
}

Cancel a sales order

How can a particular sales order be cancelled via code? Perhaps I can call the ProcessOrders graph, loop through the select orders, and execute the Cancel Order method. Unfortunately I see no such method. The drop-down action is driven by the automation menu. I do not find a cancel order action in the standard sales order entry graph. So what is the best way to accomplish the goal?
Via code, I can manually set the cancelled flag and status. This seems to work, but I'm not sure that is recommended. Seems like I'm skipping something, and the automation menu should be the way.
I'm copying the answer from this Acumatica blog post since it does exactly what you need, call the 'Cancel Order' automation step from code: Running Automation Step from Code
To call automation step you have to:
Define a new custom PXView that will return record that we want to
process
Create an adapter, that will provide data for button handler. Adapter
will get data from custom PXview.
Create a separate instance of graph, that will handle action.
Code:
public class SOOrderEntry_Extension:PXGraphExtension<SOOrderEntry>
{
//Lets define additional button than will call automation button.
public PXAction<SOOrder> ButtonExample;
[PXButton()]
[PXUIField(DisplayName = "Button Example")]
public virtual IEnumerable buttonExample(PXAdapter adapter)
{
SOOrder order = Base.Document.Current;
//creating a graph that will process Internal command
SOOrderEntry graph = PXGraph.CreateInstance<SOOrderEntry>();
graph.Document.Current = graph.Document.Search<SOOrder.orderNbr>(order.OrderNbr, order.OrderType);
//Searching for correct button from that is defined in Automatin steps.
//All sub menues are adden under action button, so we can get them and iterate.
foreach (var action in (graph.action.GetState(null) as PXButtonState).Menus)
{
if (action.Command == "Cancel Order")
{
//Constructing dummy view that will always return only one record.
adapter = new PXAdapter(new DummyView(graph, graph.Document.View.BqlSelect, new List<object> { order }));
//defining a button command
adapter.Menu = action.Command;
//running button
return graph.action.Press(adapter);
}
}
return adapter.Get();
}
//Defining a dummy view that is inherited from PXView
internal class DummyView : PXView
{
//Storing list of records
List<object> _Records;
internal DummyView(PXGraph graph, BqlCommand command, List<object> records)
: base(graph, true, command)
{
_Records = records;
}
//Everytime when system calls select for the view, retun saved records.
public override List<object> Select(object[] currents, object[] parameters, object[] searches, string[] sortcolumns, bool[] descendings, PXFilterRow[] filters, ref int startRow, int maximumRows, ref int totalRows)
{
return _Records;
}
}
}
I would think it's preferable to call the Automation Step instead of manually setting the Cancelled field because if a user changes the Automation Step you would pick up the changes by calling it.
Note that when possible you should always use existing Graph when you need to manually handle data because this will trigger validations.
If you were to manually change the Cancelled field using SOOrderEntry instead of calling the automation steps, the following validation in SOOrderEntry would still apply:
protected virtual void SOOrder_Cancelled_FieldVerifying(PXCache sender, PXFieldVerifyingEventArgs e)
{
SOOrder row = (SOOrder) e.Row;
PXResultset<CCProcTran> trans = PXSelect<CCProcTran, Where<CCProcTran.origRefNbr, Equal<Current<SOOrder.orderNbr>>,
And<CCProcTran.origDocType, Equal<Current<SOOrder.orderType>>,
And<CCProcTran.refNbr, IsNull,
And<CCProcTran.docType, IsNull>>>>>
.Select(this);
CCProcTranHelper.UpdateCCPaymentState(row, trans);
if (row != null && (row.IsCCAuthorized == true || row.IsCCCaptured == true))
{
bool authIsValid = true;
if (row.IsCCAuthorized == true)
{
if (row.CCAuthTranNbr != null)
{
CCProcTran authTran = PXSelect<CCProcTran, Where<CCProcTran.tranNbr, Equal<Required<CCProcTran.tranNbr>>>>.Select(this, row.CCAuthTranNbr);
if (String.IsNullOrEmpty(authTran.DocType) == false && String.IsNullOrEmpty(authTran.RefNbr) == false)
{
authIsValid = false;
}
}
else
{
CCProcTran authTran = this.ccAuthTrans.Select(); //Double-checking for valid auth tran
if (authTran == null)
authIsValid = false;
}
if (authIsValid && row.CCAuthExpirationDate.HasValue)
{
authIsValid = row.CCAuthExpirationDate.Value > PXTimeZoneInfo.Now;
}
}
if (authIsValid)
{
sender.RaiseExceptionHandling<SOOrder.cCPaymentStateDescr>(row, row.CCPaymentStateDescr, new PXSetPropertyException(Messages.CannotCancelCCProcessed, PXErrorLevel.Error));
}
}
}

Modifying the GL Batch generated when a Bills and Adjustments document is released

The goal is taking the Journal Transaction generated from the AP Bill page and adding 2 additional rows in GLTran.
1st Attempt
First, I extended the Release action from the Journal Transactions graph to include the 2 new lines:
public class JournalEntryExt : PXGraphExtension<JournalEntry>
{
public delegate IEnumerable ReleaseDelegate(PXAdapter adapter);
[PXOverride]
public IEnumerable Release(PXAdapter adapter, ReleaseDelegate baseMethod)
{
baseMethod(adapter);
//new code
GLTran tranRow = new GLTran();
tranRow = this.Base.GLTranModuleBatNbr.Insert(tranRow);
tranRow.AccountID = 2713;
tranRow.SubID = 467;
tranRow.CuryDebitAmt = 100;
this.Base.GLTranModuleBatNbr.Update(tranRow);
tranRow = new GLTran();
tranRow = this.Base.GLTranModuleBatNbr.Insert(tranRow);
tranRow.AccountID = 1514;
tranRow.SubID = 467;
tranRow.CuryCreditAmt = 100;
this.Base.GLTranModuleBatNbr.Update(tranRow);
this.Base.Actions.PressSave();
return adapter.Get();
}
Result: Creating and releasing the batch, entered the 2 new lines correctly.
After this, I thought that releasing the AP Bill would also trigger this extended logic from the GL Page. However, that didn't occur - The release of the Bill doesn't seem to re-use the Release logic defined in the GL page.
2nd Attempt
Then, I went back to the GL page and included the logic in the RowPersisted event, so that the 2 new lines would get created right after saving the document:
public class JournalEntryExt : PXGraphExtension<JournalEntry>
{
protected virtual void Batch_RowPersisted(PXCache sender, PXRowPersistedEventArgs e)
{
if (e.Row == null)
{
return;
}
Batch batchRow = (Batch)e.Row;
if (batchRow != null
&& e.Operation == PXDBOperation.Insert
&& e.TranStatus == PXTranStatus.Completed)
{
////new code
GLTran tranRow = new GLTran();
tranRow = this.Base.GLTranModuleBatNbr.Insert(tranRow);
tranRow.AccountID = 2713;
tranRow.SubID = 467;
tranRow.CuryDebitAmt = 102;
this.Base.GLTranModuleBatNbr.Update(tranRow);
tranRow = new GLTran();
tranRow = this.Base.GLTranModuleBatNbr.Insert(tranRow);
tranRow.AccountID = 1514;
tranRow.SubID = 467;
tranRow.CuryCreditAmt = 102;
this.Base.GLTranModuleBatNbr.Update(tranRow);
}
}
Result: Creating and saving the Batch correctly entered the 2 new lines.
After this, I thought that releasing the AP Bill would trigger this extended event, given that a Journal Entry graph should get created and used from the Bill page, but in this case, also releasing the AP Bill, did not add the 2 new lines in the generated Batch.
3rd Attempt
Then I thought I could extend the Bill's Release action and take control of the generated Journal Entry with the Search<> method. However, in this case, the extended logic seems to be executed within a transaction as the Document.Current.BatchNbr was still NULL:
4th Attempt
Finally, I tried to extend the Persist() method of APReleaseProcess similarly to how it's done in the guide T300, however none of the methods are listed (version 17.207.0029):
Any other ideas as to how to enter these GL Lines?
Thanks!
Hopefully, it didn't take you forever to go through these 4 attempts... I've got to say though, the number of efforts and details in your question is quite impressive and definitely very appreciated!
A 2-step customization will be required to insert 2 additional GL Transactions in the Batch generated for an AP Bill:
to insert additional GL Transactions, you need to override Persist method within the JournalEntry BLC extension and invoke the logic to insert additional GLTrans only if the custom ModifyBatchFromAP boolean flag value equals True:
using PX.Data;
using System;
namespace PX.Objects.GL
{
public class JournalEntry_Extension : PXGraphExtension<JournalEntry>
{
private bool modifyBatchFromAP = false;
public bool ModifyBatchFromAP
{
get
{
return modifyBatchFromAP;
}
set
{
modifyBatchFromAP = value;
}
}
[PXOverride]
public void Persist(Action del)
{
if (ModifyBatchFromAP)
{
var glTran = Base.GLTranModuleBatNbr.Insert();
Base.GLTranModuleBatNbr.SetValueExt<GLTran.accountID>(glTran, "20000");
glTran = Base.GLTranModuleBatNbr.Update(glTran);
Base.GLTranModuleBatNbr.SetValueExt<GLTran.subID>(glTran, "000000");
glTran.CuryDebitAmt = 100;
glTran.TranDesc = "Additional Debit Transaction for AP Doc";
Base.GLTranModuleBatNbr.Update(glTran);
glTran = Base.GLTranModuleBatNbr.Insert();
Base.GLTranModuleBatNbr.SetValueExt<GLTran.accountID>(glTran, "20200");
glTran = Base.GLTranModuleBatNbr.Update(glTran);
Base.GLTranModuleBatNbr.SetValueExt<GLTran.subID>(glTran, "000000");
glTran.CuryCreditAmt = 100;
glTran.TranDesc = "Additional Credit Transaction for AP Doc";
Base.GLTranModuleBatNbr.Update(glTran);
}
del();
}
}
}
after that in the overridden Release action within the APInvoiceEntry_Extension, you will subscribe to the InstanceCreated event for the JournalEntry BLC type to set ModifyBatchFromAP flag value to True allowing your logic from step 1 to execute for the Batch generated only for an AP document:
using PX.Data;
using PX.Objects.GL;
using System.Collections;
namespace PX.Objects.AP
{
public class APInvoiceEntry_Extension : PXGraphExtension<APInvoiceEntry>
{
public delegate IEnumerable ReleaseDelegate(PXAdapter adapter);
[PXOverride]
public IEnumerable Release(PXAdapter adapter, ReleaseDelegate baseMethod)
{
PXGraph.InstanceCreated.AddHandler<JournalEntry>((JournalEntry graph) =>
{
graph.GetExtension<JournalEntry_Extension>().ModifyBatchFromAP = true;
});
return baseMethod(adapter);
}
}
}
P.S. it is not currently possible to use the Select Method to Override dialog with the APReleaseProcess class due to PXHiddenAttribute applied to it. Let me forward this to our Engineering Team to bring their attention on that matter.

How to customize the sales order process to trigger an automatic "adding contract" process when sales order is successfully completed

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

Resources