I added two custom fields.One on sales order discount detail tab and one on invoices discount detail tab.And when I prepare the invoice from sales order screen,I want the data in my custom field on the sales order form to automatically insert into the custom field on invoices screen.I am still new to acumatica customization.So please give me some idea on this matter.
To the "being new" and "some idea on this matter" portion:
Being new to Acumatica, I'd suggest you start with the training material they provide. In this case, focus on T220 as it explains how to do what you are asking. To appease the moderators on Stack Overflow, you should try to include more detail in your questions. Code samples are encouraged, as well as screen shots when applicable.
The Sales Order screen is SO301000 using the graph SOOrderEntry. The Invoice screen is SO303000 using the graph SOInvoiceEntry. This is important to know because while the action for PrepareInvoice is in SOOrderEntry, it will call InvoiceOrder in SOOrderEntry which then initializes the graph SOInvoiceEntry (the invoice screen) and calls SOInvoiceEntry's InvoiceOrder method.
From SOOrderEntry.CreateInvoice:
SOInvoiceEntry ie = PXGraph.CreateInstance<SOInvoiceEntry>();
...
ie.InvoiceOrder((DateTime)ie.Accessinfo.BusinessDate, res, details,
customer.Current, created, quickProcessFlow, !isMassProcess);
You want to be mindful of where the fields you update are truly managed. What I mean by that is that business logic requires event handlers to fire when certain fields change. These event handlers interact with each other in that one event handler may cause a change that triggers another event handler. If the events are not occuring in the same graph as where you update the field, then you risk the important business logic not firing and cause data integrity issues.
In short, you are calling PrepareInvoice from SOOrderEntry, but you want to manipulate a field on the Invoice. Therefore, it is most appropriate to put your business logic for those invoice fields into SOInvoiceEntry. Of course, the custom fields on the SO would be maintained in SOOrderEntry. The real trick is going to come in getting your fields updated in SOInvoiceEntry from the method being used in SOOrderEntry.
If you need business logic on your custom invoice field(s), do that in SOInvoiceEntry because that is where the invoice is created once you dig into the code. To set the values, you can still put your code in SOOrderEntry, but prefix it with the name of the graph instance for SOInvoiceEntry. In standard code, Acumatica called that graph instance ie. Therefore, you would use syntax like ie.Document.Current to refer to the current record of the document view in SOInvoiceEntry.
If you follow all that, it's time to muddy the waters a little more. SOInvoiceEntry actually inherits from ARInvoiceEntry. That means that when placing code for the invoice, you need to decide if it applies to all AR Invoices or only the ones coming from the SO's PrepareInvoice method. If you need your changes to apply to all invoices, you need to put your business logic of the AR invoice into ARInvoiceEntry.
To the coding part of the question:
You did not specify where the fields are that you want to copy from or to. I will assume on the SOLine and ARTran (invoice line). In this case, a lot more digging takes down into CreateTranFromShipLine in SOInvoiceEntry. That methed creates the invoice line and returns it (as ARTran). If we override THAT method, we can find our DAC extension on the ARTran record and on the SOLine record and copy the value in the override.
using PX.Data;
using PX.Objects.AR;
namespace PX.Objects.SO
{
public class SOInvoiceEntry_Extension : PXGraphExtension<SOInvoiceEntry>
{
public static bool IsActive()
{
// Insert your logic for returning whether or not this shoudl be active
return true;
}
public delegate ARTran CreateTranFromShipLineDelegate(ARInvoice newdoc, SOOrderType ordertype, string operation, SOLine orderline, ref SOShipLine shipline);
[PXOverride]
public virtual ARTran CreateTranFromShipLine(ARInvoice newdoc, SOOrderType ordertype, string operation, SOLine orderline, ref SOShipLine shipline, CreateTranFromShipLineDelegate del)
{
ARTran tran = del.Invoke(newdoc, ordertype, operation, orderline, ref shipline);
ARTranExt arTranExt = tran.GetExtension<ARTranExt>();
SOLineExt soLineExt = orderline.GetExtension<SOLineExt>();
Base.Caches[typeof(ARTran)].SetValueExt<ARTranExt.usrMyCustomField>(arTranExt, soLineExt.UsrMyCustomField);
return tran;
}
}
}
ARTran is a DAC, but it references the cache which is the entire record of the database. By accessing the custom field as a DAC extension, you should be updating the cacheed record. The method does not update the view, nor does it appear to save the record, so I don't think (could be wrong) that you need to actually update the cache. If you do need to update the cache, simply add the following line after the SetValueExt line:
Base.Caches[typeof(ARTran)].Update(arTranExt);
Related
I would like to change the price of a product based on the customer's selection. For example, I'm trying to build a small PDP widget to make customers able to choose the number of candles on a cake or write text on cakes and update the price accordingly. The docs only cover how to change the price by overwriting the cart's collector/processor but I don't want to use this method because of other plugins potentially overwriting the same service. So, is there are any other methods of changing the price of the products by subscribing to an event?
There are a few things you will need to consider in this one.
Firstly, you will need to save the user input data somewhere (amount of candles, text).
Possibly a separate database table that has a OneToMany relationship on cart line items. See this article. Und ya, this is also the part where you will hook into the onLineItemAdd event & save your user input to that table. You may as well also subscribe to the onLineItemUpdate for the same saving logic. You could do the same when removing the item from the cart, although that may not be necessary if you use database's "CASCADE on delete" when implementing your DB table. Meaning once the line item gets removed by the customer (deleted in the DB), your database entry gets deleted as well.
Afterwards, you can then use the extensions or otherwise called associations to pull this data on the cart page & the order pages. You can be a little more fancy here, if you look at all the frontend router calls, you will notice that Shopware sometimes passes "Criteria" class you can hook into.
public static function getSubscribedEvents(): array
{
return [
OrderRouteRequestEvent::class => 'alterCriteria',
DocumentOrderCriteriaEvent::class => 'alterCriteria',
];
}
public function alterCriteria(Event $event): void
{
if (method_exists($event, 'getCriteria')) {
$event->getCriteria()->addAssociation('lineItems.myExtension'); // check syntax, could be lineItem.
}
}
Now you can extend the twig templates to show your candles or text in the order page, cart page, document (invoice) pages.
Secondly, you will have to handle the price. This part will be easier now that you have data saved & being automatically pulled via criteria subscribers. If it's not possible to hook into those events all the time, you will still have an option to manually load your data.
I do not recommend modifying the price itself, but maybe you could look into adding a surcharge instead. Maybe this article will be helpful to understand the price flow. You could also see if there are any other plugins out there that implement surcharge logic to see it in action.
I have a customization where when the user hits the 'Create Shipment' button on the Sales Order screen, I need to - on the Shipments screen - set the Description to the Customer (AcctCD) value.
I only want this to happen on the initial creation of the shipment. I've tried the RowInserted event for the SOShipment DAC, but the problem is there is nothing in the e.Row - all the values are null. I don't want to use the RowPersisted event, because that will fire every time a save is executed and I only want this to happen on the first creation of the SOShipment record.
So - my question is, if I want to use the actual 'CreateShipment' process that exists in the Sales Order graph / BLC - which method do I override (there are several - CreateShipment, CreateShipmentIssue, etc.), and how would I go about using that overridden method to set the Description field to the Customer CD?
Thanks...
You can do this overriding the method CreateShipment on SOShipmentEntry graph.
Also, to avoid overriding all the function you can actually override the internal function public virtual bool SetShipmentFieldsFromOrder where the order values are populated to the shipment header... there you can set the description to the AcctCD value from the Customer.
Another way to do this avoiding overriding methods is declare the OrderNbr Field Updated event handler on SOShipmentEntry Graph and ask if the record is being created by the Sales Order screen. (Using the property Accessinfo.ScreenID) If the ScreenID from access info is the number from Sales Order (SO301000) you can then assume the record is being created from the Create Shipment button and then set the description as you want.
My custom graph will create a new customer. The new customer will not use a default customer class. Each time my method saves the Customer record, the base event CustomerClass_FieldVerifying happens. That event expects a user input, in order to accept & update the billing settings. In my case, always the new customer should accept the billing setting updates. I believe that I can override the base event, and simply step over:
if (BAccount.Ask(Messages.Warning, Messages.CustomerClassChangeWarning, MessageButtons.YesNo) == WebDialogResult.No)
I'm curious if there is a better approach.
You should be able to set the answer in some way following this post:
How to pass value to confirmation popup from custom screen
Sample:
BAccount.Answer = WebDialogResult.Yes;
On the Activities tab of the Contacts screen, I am pulling activity data from an outside source and creating corresponding Acumatica activities. This activity creation is a periodically schedule process, so the activities are not created real-time. Therefore the CreatedDateTime field for the external activities does not reflect the actual time that the activities happened.
However, I need these external activities to be sorted according to when they actually occurred (not according to when they were created in Acuamtica) and to be displayed in proper sequence relative to existing Acuamtica activities. But the Activities view for the Contacts graph is sorted by CreatedDateTime, so these activities are being displayed according to Acuamtica creation time not their actual time.
I can think of two possible solutions, but I cannot see a way to accomplish either one.
1. Set the CRActivity.CreatedDateTime field to be the actual activity time.
If we could set the Created Date Time to be the actual time of the activity, the default sort order for the Activities tab would correctly display the external activities in sequence of when they actually occurred. However, this field is a default system field, and I do not see a good way to force it to be anything other than the time the Acumatica object is persisted. Is there any way to specify a value for the CreatedDateTime field?
2. Modify the default sort order of the Activities tab.
We can set the StartDate of the new activities to be the actual time that they occurred. So if we could order the Activities grid by Start Date rather than by Created Date Time, that would also achieve the desired affect. If the Activities view were a straightforward PXSelect, we could simply create a new PXSelect statement with the order by we want in our graph extension and use that as the data member for the grid. However, the Activities view is not simple and the OrderBy is buried inside the GenerateOriginalCommand method of the CRActivityListBase class. Is there anyway to override this OrderBy in way that takes advantage of all the functionality of CRActivityListBase?
Is there an acceptable way to use either of these approaches, or is there an alternative approach that would display the activities sorted by actual time?
You can replace Sorting utilizing OrderByNew as below.
public class ContactMaintPXExt : PXGraphExtension<ContactMaint>
{
public override void Initialize()
{
Base.Activities.OrderByNew<OrderBy<Desc<CRActivity.startDate>>>();
}
}
I've just been watching Greg Youngs talk on Event Sourcing, but i'm confused as to where business logic fits in. A simple example:
1) Shopping Cart Created
2) Item Added
3) Item Added
4) Promotional Code - 20% Off
Is the Promotional Code calculated against the shopping carts items and the result of that stored as an event. I understand "PromotionalCodeAddedEvent" would maybe make sense, but where does the math happen? I'm thinking:
public void AddPromotionalCode(PromotionalCode code)
{
//perform calculation against shopping cart items.
//if valid
ApplyChanges(cmd);
}
Then the result doesn't end up anywhere and the Read Model would have to perform the calculations.
I'm not fully understanding the concept, any help would be great.
Something as seemingly simple as a promotional code can actually be a fairly complex use case. Primarily, this is due to the fact that a promotional code is logic that (usually) is maintained by one or more business users, whereas it also belongs inside of the domain, itself. It is non-traditional, in that sense. There are multiple ways to handle this, and what I will outline would just be my personal approach.
For the sake of argument, assume that you have a simple series of known promotional codes, such as:
X% Off Purchase, with or without a Minimum Purchase
$X Off Purchase, with or without a Minimum Purchase
We can make some assumptions, as well:
A Promotional Code has a Start Date
A Promotional Code has an End Date
The application of a promotional code can be tricky. Consider the two scenarios that we have defined. "$X Off Purchase" is comparatively simple, because it is a fixed amount. "X% Off Purchase", however, is more complex. If we only had the fixed amount, we could apply the discount to the cart as soon as any thresholds are met. With a percentage-based discount, if the user were to add two items, add a promotional code, and then add another item, the promotion would have already been "applied".
Because of this, I would personally only "attach" a promotional code to a cart. Why? The reason is that at the point of checkout, I can likely assume that a cart is going to be used to generate an order. Until that time, the cart's contents are fluid. A user's action against the cart will change the total value of the cart, as well as the total discount, assuming a non-fixed discount amount. It can also render either discount as being invalid, should a user remove one or more items from a cart and the total value of the cart fall below the threshold to apply the discount.
So, I would actually have multiple commands which would would be involved. Essentially any commands that impact the value of the cart could change the discount amount. To that end, I would be looking for the discount amount to be recalculated for the commands which:
Add an item to the cart
Remove an item from the cart
Change the quantity of items in the cart
Add a promotional code to the cart
Change the promotional code attached to the cart
Since these all are operations against a cart, I would be calculating the discount within the promotional code, itself, with the participation of the data contained in the cart. It feels like a promotional code is going to be an aggregate, going down this path. So, I would have the command handlers invoke a domain service that can provide my cart with the information that it requires. That domain service is going to load the promotional code, and I am going to be able to pass in the line items within that cart, so that the promotional code will tell me what the calculated discount would be. Then, I am going to generate the event, which contains the new value of the cart, along with the adjusted value (the discount). Going down this path, the logic for calculating a discount based upon line items within a cart is the responsibility of the promotional code.
You could put this responsibility in the cart, instead. Personally, though, I feel as if encapsulation of domain logic within the promotional code, itself, makes more sense. I had mentioned that it is likely that you will generate an order from a cart. By having the promotional code as an aggregate, and it containing the logic to apply the discount based upon the line items, we have a single truth, in how we calculate a discount for line items - whether that is in terms of a cart or in terms of an order.
You can, for example, raise a second event such as PromotionalCodeApplied which contains the computation results.
The Read Model then just have to use the pre-computed result.
I understand "PromotionalCodeAddedEvent" would maybe make sense, but where does the math happen?
It should happen in commands that perform modifications to the shopping cart. Every such command will call some method like RecalculateTotals() where all business logic will be hosted.
Consider the following pseudocode:
public void AddPromotionalCode(PromotionalCode code)
{
var #event = new PromotionalCodeAdded(code);
var amount = RecalculateTotalAmount(extraEvent: #event);
#event.TotalAmount = amount;
_eventStore.Publish(#event);
}
decimal RecalculateTotalAmount(IEvent extraEvent)
{
var relatedEventTypes = new[] { typeof(PromotionalCodeAdded), typeof(ShoppingCartCreated), typeof(ItemAdded) };
var events = _eventStore.ReadEventsOfTypes(relatedEventTypes);
var events = events.Concat(new[] { extraEvent });
//calculation logic goes here based on all related events
}
This is where I like to return events from command methods. As Alexander Langer mentioned, you'd apply the "math" and return the relevant event(s):
public PromotionalCodeApplied AddPromotionalCode(PromotionalCode code)
{
//perform calculation against shopping cart items.
var promotionalCodeApplied = new PromotionalCodeApplied(code.VoucherNumber, discountAmount, DateTime.Now);
On(promotionalCodeApplied);
return promotionalCodeApplied;
}
public void On(PromotionalCodeApplied promotionalCodeApplied)
{
_voucherNumber = promotionalCodeApplied.VoucherNumber;
_discountAmount = promotionalCodeApplied.DiscountAmount;
_discountDate = promotionalCodeApplied.DateAppllied;
}
Now your read model has access to the relevant values.