I need to create new Customers by mostly copying a select customer and modifying a few fields relevant to a custom Process.
Outside of the custom Process as an initial attempt to see if this is even possible to copy a Customer I have the following:
public class CustomerMaint_Extension : PXGraphExtension<CustomerMaint>
{
public PXAction<Customer> copyTest;
[PXProcessButton]
[PXUIField(DisplayName = "Copy Test")]
protected virtual void CopyTest()
{
var customer = Base.BAccount.Current;
var graph = PXGraph.CreateInstance<CustomerMaint>();
var cache = graph.BAccount.Cache;
// Set field Defaults using CustomerMaint.CopyAccounts method
graph.CopyAccounts(cache, customer);
// Create new copy of current Customer
var copyCustomer = (Customer)cache.CreateCopy(customer);
// Modify key values
copyCustomer.AcctCD = "COPY " + customer.AcctCD;
copyCustomer.BAccountID = null;
// Prevent "Customer Class Changed -- update Defaults?" dialog
cache.SetStatus(copyCustomer, PXEntryStatus.Inserted);
// Insert into cache
// *** Exception occurs here ***
copyCustomer = (Customer)cache.Insert(copyCustomer);
// Modify additional fields as necessary by custom process
// ...
// Persist to database
graph.Save.Press();
}
}
The issue I'm currently encountering with this code as it currently is, is in the cache.Insert(copyCustomer) throws an exception:
Error: An error occurred during processing of the field CustomerClassID: Value cannot be null.
Parameter name: key
I've tracked this down to be coming from the CustomerClassDefaultInserting function of the CustomerMaint graph at the point of SalesPerson.Insert(sperson). It appears this function is attempting to create the CustSalesPeople record for the Default Salesperson of the assigned Customer Class.
Is this even on the right path to copy a Customer or is there a better way? Or how to address the exception when Inserting the new customer?
The issue with copying the customer is more than just copying one object. There are many objects that need to be copied for the customer.
I copied your code and executed it. I commented out CopyAccounts as I would think it would be called on the cloned object, not on the existing customer.
The next error I received was Error: An error occurred during processing of the field Default Location value 8068 Error: Default Location '8068' cannot be found in the system.
This was due to cloning the other foreign key fields from the Customer record. DefLocationID is the default location for the customer. It is trying to set those keys as well, and cannot due to the restriction put on the Selector, requiring the location to be the same BAccountID as the record. DefAddressID, DefContactID, and other key fields will act the same way.
So to complete this, you would need to review all foreign keys on the Customer/BAccount that may be set, and then copy those objects and set them to the proper DAC's as well. Some information may not need to be copied, so I would just null those key fields and copy what is required.
Hey Nickolas Hook Like KRichardson pointed out because of all the PK/FK constraints you can't just copy the data you will have to do everything individually.
the other approach is to pull the data for the parent.
Related
I created a custom table with additional settings for customers. Next I added a field to the customer core table in which I'd like to store the id choice per customer. I extended with EntityExtensionInterface the customerDefinition :
public function extendFields(FieldCollection $collection): void
{
$collection->add(
(new OneToOneAssociationField(
'customerSetting',
'customer_setting',
'id',
WdtCustomerSettingsDefinition::class,
true
))->addFlags(new Inherited())
);
}
public function getDefinitionClass(): string
{
return CustomerDefinition::class;
}
When I manually manipulate the customer table, with an id from my custom table in the added field, I can retrieve and use the settings from my custom table in the storefront.
For the backend I created a single select to the entity custom_table,
<sw-entity-single-select entity="wdt_customer_settings" v-model="customer.extensions.customerSetting.id" >
</sw-entity-single-select>
and with the manually 'injected' id from the custom table, this choice indicates indeed 'selected' However, after changing to another choice and saving results in an error: Customer could not be saved.
What am I missing?
You should look always to the ajax responses. There is the explict error which is occured. Do you added some boilerplate code to check that your extensions is always available? Otherwise it would cause issues on new entities
In POOrderEntry as a POLine is created or deleted, I need to push a reference back to a custom DAC that originates the PO Line. For instance, if the PO Line is deleted, my custom DAC has the reference removed in Events.RowDeleted via:
using (PXTransactionScope ts = new PXTransactionScope())
{
Base.Caches[typeof(MyDAC)].SetValueExt<MyDAC.pOType>(row, null);
Base.Caches[typeof(MyDAC)].SetValueExt<MyDAC.pONbr>(row, null);
Base.Caches[typeof(MyDAC)].SetValueExt<MyDAC.pOLineNbr>(row, null);
Base.Caches[typeof(MyDAC)].Update(row);
Base.Caches[typeof(MyDAC)].Persist(PXDBOperation.Update);
ts.Complete(Base);
}
I have tried to allow the normal Persist to save the values, but it doesn't unless I call Persist (last line of my example above). The result is an error via Acuminator of "Changes cannot be saved to the database from the event handler". As I look at this, I wonder if it should be in an Long Operation instead of a Transaction Scope, but the error from Acuminator tells me I'm doing this wrong. What is the proper way to achieve my update back to "MyDAC" for each PO Line?
I have also tried initializing a graph instance for MyDAC's graph, but I get a warning about creating a PXGraph in an event handler so I can't "legally" call the graph where MyDAC is maintained.
My code compiles and functions as desired, but the error from Acuminator tells me there must be a more proper way to accomplish this.
You can add a view to the graph extension.
Then in the row deleted you will use your view.Update(row) to update your custom dac.
During the base graph persist your records will commit as long as there are no other errors found in other events.
The way you have it now commits your changes with a chance the row that was being deleted is never deleted.
Also with this change there is no need to use PXTransactionScope.
An example might look something like this...
public class POOrderEntryExtension : PXGraphExtension<POOrderEntry>
{
public PXSelect<MyDac> MyView;
protected virtual void _(Events.RowDeleted<POLine> e)
{
//get your row to update from e.Row
var myRow = PXSelect...
myRow.pOType = null;
myRow.pONbr = null;
myRow.pOLineNbr = null;
MyView.Update(myRow);
}
}
I have a modification that facilitates an internal business process utilizing PO and SO. The screen provides the ability to purchase an MRO spare part outside of the normal replenishment process. The item may or may not be maintained in inventory, so it may be ordered for replenishment or as an SO to be processed as Mark for PO.
In creating the SO, I am able to store the SO reference information to the DAC for my customization. When creating the PO directly, I also am able to capture the PO reference information. However, when creating the PO from the SO using the standard Acumatica menu action for Create Purchase Order, I have been unable to capture the right event to enable storing the PO reference being assigned in SOLineSplit3 to my custom DAC. (Worth noting that I also need to be able to override the default curyunitcost value on the PO line using the value stored on my custom DAC as these purchases do not carry a fixed price per buy. This is done by tracing the SOLineSplit back to my custom DAC and overriding POLine_CuryUnitCost_FieldDefaulting.)
The action invoked on the Sales Order Entry screen (Action - Create Purchase Order) calls the method CreatePOOrders in POCreate.cs which in turn creates an instance of the POOrderEntry graph to create the actual purchase order.
Eventually, the following code is reached, which appears to attach the PO reference information to SOLineSplit3 as soline:
soline.POType = line.OrderType;
soline.PONbr = line.OrderNbr;
soline.POLineNbr = line.LineNbr;
soline.RefNoteID = docgraph.Document.Current.NoteID;
docgraph.UpdateSOLine(soline, docgraph.Document.Current.VendorID, true);
docgraph.FixedDemand.Cache.SetStatus(soline, PXEntryStatus.Updated);
I am not yet familiar with Cache.SetStatus, but the pop-up description seems to indicate that this is using the FixedDemand select in POOrderEntry to find and set (or insert) the SOLineSplit3 record. The call to UpdateSOLine above it is a new internal method that was not in my previous version of POCrete.cs, as this entire method seems to have had some significant rework recently. In trying to capture events on SOLineSplit3 in both POCreate and POOrderEntry, it appears that Cache.SetStatus does not raise any events that I can capture... or I am just completely lost on what event to capture/override.
Immediately following this section, the following appears to update a Replenishment record and save the entire POOrderEntry graph.
if (docgraph.Transactions.Cache.IsInsertedUpdatedDeleted)
{
using (PXTransactionScope scope = new PXTransactionScope())
{
docgraph.Save.Press();
if (demand.PlanType == INPlanConstants.Plan90)
{
docgraph.Replenihment.Current = docgraph.Replenihment.Search<INReplenishmentOrder.noteID>(demand.RefNoteID);
if (docgraph.Replenihment.Current != null)
{
INReplenishmentLine rLine =
PXCache<INReplenishmentLine>.CreateCopy(docgraph.ReplenishmentLines.Insert(new INReplenishmentLine()));
rLine.InventoryID = line.InventoryID;
...
rLine.PlanID = demand.PlanID;
docgraph.ReplenishmentLines.Update(rLine);
docgraph.Caches[typeof(INItemPlan)].Delete(demand);
docgraph.Save.Press();
}
}
scope.Complete();
}
...
}
Basically, I need to insert my code right between the assignment of the PO information to "soline" and the docgraph.Save.Press(); without copying dozens of lines of code to modify this method. I have managed cloning the base method and inserting my code successfully, but I'd prefer to use an event handler and eliminate modifying the standard code. But the question... What event in which graph will let me grab the PO information and follow the breadcumbs back through SOLineSplit to my custom DAC?
Acumatica Build 18.212.0033
Extend POCreate graph because it is the one instanciating the POOrderEntry you are interested in.
Setup a hook on any POOrderEntry graph created by POCreate and subscribe your events on the intercepted graph. I tested this solution, with a Sales Order that has allocations lines in allocation window it will catch the SOLineSplit3 events:
public class POCreate_Extension : PXGraphExtension<POCreate>
{
public override void Initialize()
{
PXGraph.InstanceCreated.AddHandler<POOrderEntry>((graph) =>
{
graph.RowInserting.AddHandler<POOrder>((sender, e) =>
{
PXTrace.WriteInformation("POOrderEntry_POOrder_RowInserting");
});
graph.RowInserting.AddHandler<POOrderEntry.SOLineSplit3>((sender, e) =>
{
PXTrace.WriteInformation("POOrderEntry_SOLineSplit3_RowInserting");
});
graph.RowUpdating.AddHandler<POOrderEntry.SOLineSplit3>((sender, e) =>
{
PXTrace.WriteInformation("POOrderEntry_SOLineSplit3_RowUpdating");
});
graph.RowPersisting.AddHandler<POOrderEntry.SOLineSplit3>((sender, e) =>
{
PXTrace.WriteInformation("POOrderEntry_SOLineSplit3_RowPersisting");
});
});
}
}
I've noticed that whenever an AR Invoice gets saved, a record gets created in the Note table with the new invoice's note ID. Can you tell me how that is being accomplished? I'd like to get one of my screens to do the same thing. I guess there must be some kind of attribute on the either the DAC or the graph but I can't find it. I have the PXNote attribute on the NoteID column in my DAC but it does not cause a Note record to be automatically created.
Thanks for your help.
To have Note record automatically created when a new parent record gets saved, one should invoke the static PXNoteAttribute.GetNoteID<Field>(PXCache cache, object data) method when the parent record is inserted in the cache.
For example, to have Note record automatically created when a new Stock Item gets saved, you should subscribe to RowInserted handler for the InventoryItem DAC and call PXNoteAttribute.GetNoteID<Field>(...):
public class InventoryItemMaintExt : PXGraphExtension<InventoryItemMaint>
{
public void InventoryItem_RowInserted(PXCache sender, PXRowInsertedEventArgs e)
{
var noteCache = Base.Caches[typeof(Note)];
var oldDirty = noteCache.IsDirty;
PXNoteAttribute.GetNoteID<InventoryItem.noteID>(sender, e.Row);
noteCache.IsDirty = oldDirty;
}
}
The code snippet above can be incorporated into almost any custom BLC with a couple simple changes to replace InventoryItem with a custom DAC.
Is there a relation between Retrieve and Update in a Dynamic CRM Plugin?
For example if I am retrieving only one field:
Entity e = (Entity)service.Retrieve("EntityLogicalName", EntityGuid,
new ColumnSet(new string[] {"entityid"}));
Can I update another field in the Entity e that has NOT been retrieved?
For example:
e.Attributes["AnotherEntityField1] = "test1";
e.Attributes["AnotherEntityField2] = "test2";
service.update(e);
By NOT including all fields that to be updated in the Retrieve, may this cause some hidden issues?
Assuming, as it appears, that you are just retrieving the entity's primary key, entityid, you won't need to do the retrieve.
Entity e = new Entity("EntityLogicalName") { Id = EntityGuid };
e.Attributes.Add("AnotherEntityField1", "test1");
e.Attributes.Add("AnotherEntityField2", "test2");
service.Update(e);
If you are doing a retrieve to confirm the record exists you need to try/catch or use a retrieve multiple since a Retrieve will throw an exception if the record does not exist.
What you are trying to do is perfectly acceptable and won't cause any problems. Since you obtained the Entity instance via a Retrieve operation the required LogicalName and Id will be set correctly for an update.
Your code would need to read as below for adding new attributes not retrieved initially otherwise you'll get a KeyNotFoundException as the Entity type is just a wrapper over Dictionary<string,string>.
e.Attributes.Add("AnotherEntityField2","test2");
When you're trying to update an entity you don't need that an field exists in the attributes colletion, but to avoid the exception "The given key is not presented in the dictionary" is a good practise to check first if the Attributes Colletion contains the field you want to updated. If yes just update it otherwise you have to add it to the Attributes Colletion of the entity.
if(e.Attributes.Contains("AnotherEntityField1"))
{
e.Attributes["AnotherEntityField1"] = "test1";
}
else
{
e.Attributes.Add("AnotherEntityField1", "test1");
}
// now update operation