So, I have some strange requirements for a project (shops within shops within shops and tiered pricing and "oh my God, shoot me"), but through the magic of Merchello, I've met most of them :)
I have, however, run into issues when manually generating orders - I have trouble actually fulfilling them, so the stock of a product doesn't change. I can capture payments (Cash/Invoice), but I can't fulfill the order.
In essence, all that is needed is a button to press in the sale overview, when they've shipped the goods, to mark it and to change the product stock.
Capture Funds works just fine, but I'm missing a fulfillment button. It'd also be neat, if the shipping address would show, but it will always be the same as the billing address, so it's not really important.
I've taken inspiration from one of Rusty's examples on how to manually generate an order, but attempted to make it suit my needs:
//Get current customer, or if not found, create new
var customer = customerService.GetByLoginName(member.Username)
?? customerService.CreateCustomerWithKey(member.Username, model.company, "", model.email);
//To be used for both billing and shipping
var billingAddress = new Address()
{
Name = model.company,
Address1 = model.address,
Locality = model.country,
PostalCode = model.postal
};
//Attempt at storing a shipping/billing address. Not really a success.
var customerBasket = customer.Basket();
customerBasket.SalePreparation().SaveShipToAddress(billingAddress);
customerBasket.SalePreparation().SaveBillToAddress(billingAddress);
customerBasket.PackageBasket(billingAddress);
//If the customer doesn't have any registered addresses
if (customer.Addresses.Count() <= 0)
{
customer.CreateCustomerAddress(billingAddress, model.company, AddressType.Billing);
customer.CreateCustomerAddress(billingAddress, model.company, AddressType.Shipping);
}
//Adding adjusted products to the customer's basket. Adjusted meaning: Prices altered in accordance with the tiered pricing
foreach(var item in basketList)
{
customerBasket.AddItem(item.name, item.sku, item.qty, item.price);
}
//Finalize order
var paymentMethod = customer.Basket().SalePreparation().GetPaymentGatewayMethods().FirstOrDefault();
customer.Basket().SalePreparation().AuthorizePayment(paymentMethod);
I've tried to modify it back and forth for a while, to accomodate tax pricing and shipping, though these are a secondary concerns. My main issue is that I'm quite unsure how I would go about "enabling" fulfillment of an order in such a scenario.
First, I'm not sure why the shipping address is not showing up. In your checkout, how are you saving the shipping information?
It think once that gets sorted the Fulfill button will get sorted.
You can also for the Order creation so that it can be fulfilled by overriding the setting in the merchello.config file:
<!--
Overrides the Payment Method's IPaymentResponse ApproveOrderCreation indicating an order should always be created no matter
if the payment has been collected or not.
-->
<setting alias="AlwaysApproveOrderCreation" value="false" />
Well, I'm not actually storing much information about the shipping itself - figured I only needed the address, since the end user will eventually decide on how to ship the goods on a case-by-case basis, so that boils down to:
Or at least, that was the idea behind it - it is a all a little patch work-esque at this point (co-workers have been lost and found, and then lost again), so I am... Mending and making features, to the best of my abilities :D
I suppose I could create a shipping method that is sort of "empty" somehow, and attach it to the order? Would that be feasible?
Yes, you should quote a shipment with a Zero amount and save the shipment rate quote. All of that information gets serialized with the invoice (that's how we can do anonymous checkouts when the customer does not have a permanent record in the db for example).
The SaveShipToAddress is merely a step to temporarily store the address so it can be used at a later stage in the SalePreparation. It gets disposed with the SalePreparation data once the sale is complete.
I'm terribly sorry, I feel like an idiot, but I have trouble wrapping my brain around how shipping and rates are stored and connected to an order, apparently.
I have tried following the example and generating rates, if none are found, but the quotes always came back empty, regardless.
I'm currently at this attempt:
//Preparing shipment
var warehouse = warehouseService.GetDefaultWarehouse();
var shipmentStatus = shipmentService.GetShipmentStatusByKey(Constants.DefaultKeys.ShipmentStatus.Ready);
var shipment = shipmentService.CreateShipment(shipmentStatus, warehouse.AsAddress(), billingAddress, customerBasket.Items);
//Digging up a ShipCountry
var catalog = warehouseService.GetAllWarehouseCatalogs().FirstOrDefault();
var shipCountry = gatewayproviderService.GetShipCountry(catalog.Key, "DK");
var key = Constants.ProviderKeys.Shipping.FixedRateShippingProviderKey;
var method = gatewayproviderService.GetShipMethodsByShipCountryKey(key, shipCountry.Key).FirstOrDefault();
var shipmentRateQuote = shipment.ShipmentRateQuoteByShipMethod(method.Key);
//Clear old ShipmentRateQuotes and save new ones to customer's basket
customerBasket.SalePreparation().ClearShipmentRateQuotes();
customerBasket.SalePreparation().SaveShipmentRateQuote(shipmentRateQuote);
But I can't get shipmentRateQuote to populate properly - I feel like I'm misunderstanding something. I've dug up the right shipping method, but it doesn't seem to be connected to the shipment itself, and so the basket doesn't have anything to save.
That looks like it should be working (is it throwing any errors?)
My guess is your looking in the wrong place for the saved values.
As this is just a quote and not an actual shipment. These are NOT saved in the merchShipment table, rather information is actually serialized in the ExtendedDataCollection of the shipment line item that represents the quote for that shipment. That way, if something changes at no fault of the customer (out of stock, need to ship a back order, split into multiple shipments) the information can be retrieved without having to bother the customer and/or change the sale price and can be completely handled by the warehouse staff.
if (invoice.ShippingLineItems().Any())
{
var shipmentLineiTem = invoice.ShippingLineItems().FirstOrDefault();
var shipment = shippingLineItem.ExtendedData.GetShipment<InvoiceLineItem>();
}
Because shopmentRateQuote ends up being null, which I admittedly don't quite understand, as when I inspect "method", it does return a shipping method I created in the backend, with some dummy rates, so ShipmentRateQuoteByShipMethod() should find some?
I've kept poking at it, and gotten somewhat past it, through a lot of fiddling.
//Preparing shipment
var warehouse = warehouseService.GetDefaultWarehouse();
var shipmentStatus = shipmentService.GetShipmentStatusByKey(Constants.DefaultKeys.ShipmentStatus.Quoted);
var shipment = shipmentService.CreateShipment(shipmentStatus, warehouse.AsAddress(), billingAddress, customerBasket.Items);
//Get rates and quotes
var methods = rateTableProvider.ShipMethods.ToArray(); //Gets "GLS", "Post Danmark (Vary by Weigth)", "Ground (Vary by Weight)"
var method = rateTableProvider.ShipMethods.Where(x => x.ShipCountryKey == shipCountry.Key).FirstOrDefault(); //Gets "Post Danmark (Vary by Weight)"
shipment.ShipMethodKey = method.Key;
var quote = rateTableProvider.QuoteShipMethodForShipment(shipment, rateTableProvider.GetShippingGatewayMethod(method.Key, shipCountry.Key));
////Create invoice
var paymentMethod = customer.Basket().SalePreparation().GetPaymentGatewayMethods().FirstOrDefault();
var invoice = invoiceService.CreateInvoice(Constants.DefaultKeys.InvoiceStatus.Unpaid);
//Add items to invoice
List<InvoiceLineItem> invoiceItems = new List<InvoiceLineItem>();
foreach (var item in customerBasket.Items)
{
invoiceItems.Add(new InvoiceLineItem(LineItemType.Product, item.Name, item.Sku, item.Quantity, item.Price, item.ExtendedData));
}
//Add keys to invoice
((Invoice)invoice).CustomerKey = customer.Key;
invoice.VersionKey = Guid.NewGuid();
//Set billing address
invoice.SetBillingAddress(billingAddress);
//Update invoice items and total
foreach (var item in invoiceItems)
{
invoice.AddItem(item);
invoice.Total += item.TotalPrice;
}
//Add shipping quotes to invoice
invoice.Items.Add(quote.AsLineItemOf<InvoiceLineItem>());
//Save invoice
invoiceService.Save(invoice);
var shipmentLineItem = invoice.ShippingLineItems().FirstOrDefault();
invoice.AuthorizePayment(paymentMethod);
var order = invoice.PrepareOrder();
orderService.Save(order);
An order now goes through, and a shipping address is now present on the order overview. I still have no fulfillment button, however :(
Also, for some reason, the Sales Listing now doesn't update when funds are captured - it's always listed as "Unpaid", even though everything seems to go off without a hitch, when I press "Capture Funds". The Capture Funds buttons sticks around as well, but I don't recall if that's abnormal, really.
Okay, so immediately after posting, I realized a face-palming-worthy error: the product's extended data, didn't set it for "shippable", which lead to the missing fulfillment button (I have the lovely little button back now).
The bug with Capture Funds is still there though, and pressing Fulfill just results in an error, unfortunately:
After a bit of poking and prodding, the error seems to arise in /App_Code/Merchello/js/merchello.controller.js, in "openFulfillShipmentDialog()". For some reason,
appears to fail - the object itself looks empty, and it never gets into the "then" function. Now, I am a regular orangulartang, in that I don't really know angular, but I do know how to alert() stuff for derpy testing, and "request" does seem to get populated with a shipMethodKey, an invoiceKey, and a lineItemKey, so I don't quite understand why it fails, when the request is fully populated.
So, this seems to be the closest I can get at the moment:
The Capture Payment button works without bugging, and "Fulfill" doesn't throw an error. It doesn't do anything either (at all), but there doesn't seem to be any errors, whatsoever. It seems likely that this is related to the missing Shipping Address.
I have tried adding a shipping address thusly:
//Digging up a ShipCountry
var warehouse = warehouseService.GetDefaultWarehouse();
var catalog = warehouse.WarehouseCatalogs.FirstOrDefault();
var shipCountry = gatewayproviderService.GetShipCountry(catalog.Key, warehouse.CountryCode);
//Preparing shipment
var shipmentStatus = shipmentService.GetShipmentStatusByKey(Constants.DefaultKeys.ShipmentStatus.Quoted);
var shipment = shipmentService.CreateShipment(shipmentStatus, warehouse.AsAddress(), billingAddress, customerBasket.Items);
var key = Constants.ProviderKeys.Shipping.FixedRateShippingProviderKey;
var rateTableProvider = (FixedRateShippingGatewayProvider)MerchelloContext.Current.Gateways.Shipping.GetProviderByKey(key);
rateTableProvider.ExtendedData.SetValue(Constants.ExtendedDataKeys.CurrencyCode, "DKK");
// If no shipping methods are found, add a new one
if (!rateTableProvider.ShipMethods.Any())
{
// creates the rate table for ship rate quotes
var gwShipMethod = (FixedRateShippingGatewayMethod)rateTableProvider.CreateShipMethod(FixedRateShippingGatewayMethod.QuoteType.VaryByWeight, shipCountry, "Post Danmark (Vægtbaseret)");
gwShipMethod.RateTable.AddRow(0, 10000, 0);
rateTableProvider.SaveShippingGatewayMethod(gwShipMethod);
}
IShipMethod shipmethod = rateTableProvider.ShipMethods.Where(x => x.ShipCountryKey == shipCountry.Key).FirstOrDefault();
IShipmentRateQuote quote = rateTableProvider.QuoteShipMethodForShipment(shipment, rateTableProvider.GetShippingGatewayMethod(shipmethod.Key, shipCountry.Key));
InvoiceLineItem quoteItem = quote.AsLineItemOf<InvoiceLineItem>();
quoteItem.ExtendedData.AddShipment(shipment);
quoteItem.ExtendedData.SetValue(Constants.ExtendedDataKeys.CurrencyCode, "DKK");
quoteItem.ExtendedData.AddAddress(billingAddress, AddressType.Billing);
quoteItem.ExtendedData.AddAddress(billingAddress, AddressType.Shipping);
customerBasket.Items.Add(quoteItem);
//Finalize order
var paymentMethod = customerBasket.SalePreparation().GetPaymentGatewayMethods().FirstOrDefault();
CustomerBasket.SalePreparation().AuthorizePayment(paymentMethod);
However, whenever I do
customerBasket.Items.Add(quoteItem);
the backend bugs out again, not showing the entire order and loading endlessly, with an error:
@Marc - the shipping address needs to have at very least a country code so that the ship method can be determined.
In your error message, I see it is failing to find the currency symbol, which would indicate that the Currency Code is not present in the extended data in the invoice line items.
In a standard checkout, there is a validation task that runs the following code:
var unTagged = value.Items.Where(x => !x.ExtendedData.ContainsKey(Constants.ExtendedDataKeys.CurrencyCode)).ToArray();
if (unTagged.Any())
{
var defaultCurrency =
SalePreparation.MerchelloContext.Services.StoreSettingService.GetByKey(
Constants.StoreSettingKeys.CurrencyCodeKey);
foreach (var item in unTagged)
{
item.ExtendedData.SetValue(Constants.ExtendedDataKeys.CurrencyCode, defaultCurrency.Value);
}
}
var allCurrencyCodes =
value.Items.Select(x => x.ExtendedData.GetValue(Constants.ExtendedDataKeys.CurrencyCode)).Distinct();
return 1 == allCurrencyCodes.Count()
? Attempt<IInvoice>.Succeed(value)
: Attempt<IInvoice>.Fail(new InvalidDataException("Invoice is being created with line items costed in different currencies."));
This ensures that each line has a currency code set AND that all currency codes within the invoice are consistent. Merchello does not allow for multiple currencies in a single invoice - apply taxes to something like that would be an absolute nightmare =).
I have kind of started over on the project entirely, so that it will all be my own code and logic, in stead of doing patchwerk fixing. If I can get it done on time, and if I'm allowed to by the end of this, I'm considering uploading it somewhere, just so examples of crazy stuff exists.
I hear tiered pricing is on its way in Merchello, but in this case, it's achieved by adding an Archetype to the product nodes and updating the prices accordingly as they're added to the basket - serializing and deserializing a Json object to pass along a dictionary of tiers and prices. I have a sneaking suspicion that this might have been where this project broke - juggling "real" and "artificially constructed" products and prices, making them lose ExtendedData at one point or another.
@Marc - Your project sounds really interesting and you've had to handle quite a few difficult requirements.
I think you're correct that it is the shipping line item and, assuming your quoteItem is the shipping line item, it looks like you are adding it correctly.
I agree that the currency codes are set a bit weird. I just have not had a chance to build out a concept to pretty up that aspect of the API and have the problem that I don't want to break any shipping provider someone has coded as a plugin.
I'm off for the weekend, but would like to look at your scenario more closely - would you have time to setup a Skype call next week?
Yeah, that'd be cool - after starting over on the project, I think I've gotten a more solid structure up and running than I had before, but completing orders is still quite tricky with my limited experience. I reckon I can take a day off work during the week to fiddle with it at home and take a call for a while. I have to hand over the project on Thursday, but I should be able to manage either Tuesday or Wednesday. But if that doesn't work for you, it'd still be interesting to get it finished afterwards, so Friday or after is also great.
So after a Skype session with Rusty, the problem turned out to be some missing extended data in my products, which was lost when they were added to the basket, due to the tiered pricing. This is fixed now, and I have made a post about it here - Tiered Pricing.
Fulfilling Orders
G'day, hello, ni hao
So, I have some strange requirements for a project (shops within shops within shops and tiered pricing and "oh my God, shoot me"), but through the magic of Merchello, I've met most of them :)
I have, however, run into issues when manually generating orders - I have trouble actually fulfilling them, so the stock of a product doesn't change. I can capture payments (Cash/Invoice), but I can't fulfill the order.
In essence, all that is needed is a button to press in the sale overview, when they've shipped the goods, to mark it and to change the product stock.
Capture Funds works just fine, but I'm missing a fulfillment button. It'd also be neat, if the shipping address would show, but it will always be the same as the billing address, so it's not really important.
I've taken inspiration from one of Rusty's examples on how to manually generate an order, but attempted to make it suit my needs:
I've tried to modify it back and forth for a while, to accomodate tax pricing and shipping, though these are a secondary concerns. My main issue is that I'm quite unsure how I would go about "enabling" fulfillment of an order in such a scenario.
Any assistance would be very much appreciated <3
Wow - that's a new one =)
First, I'm not sure why the shipping address is not showing up. In your checkout, how are you saving the shipping information?
It think once that gets sorted the Fulfill button will get sorted.
You can also for the Order creation so that it can be fulfilled by overriding the setting in the merchello.config file:
Well, I'm not actually storing much information about the shipping itself - figured I only needed the address, since the end user will eventually decide on how to ship the goods on a case-by-case basis, so that boils down to:
Or at least, that was the idea behind it - it is a all a little patch work-esque at this point (co-workers have been lost and found, and then lost again), so I am... Mending and making features, to the best of my abilities :D
I suppose I could create a shipping method that is sort of "empty" somehow, and attach it to the order? Would that be feasible?
Yes, you should quote a shipment with a Zero amount and save the shipment rate quote. All of that information gets serialized with the invoice (that's how we can do anonymous checkouts when the customer does not have a permanent record in the db for example).
The SaveShipToAddress is merely a step to temporarily store the address so it can be used at a later stage in the SalePreparation. It gets disposed with the SalePreparation data once the sale is complete.
I'm terribly sorry, I feel like an idiot, but I have trouble wrapping my brain around how shipping and rates are stored and connected to an order, apparently.
I have tried following the example and generating rates, if none are found, but the quotes always came back empty, regardless.
I'm currently at this attempt:
But I can't get shipmentRateQuote to populate properly - I feel like I'm misunderstanding something. I've dug up the right shipping method, but it doesn't seem to be connected to the shipment itself, and so the basket doesn't have anything to save.
That looks like it should be working (is it throwing any errors?)
My guess is your looking in the wrong place for the saved values.
As this is just a quote and not an actual shipment. These are NOT saved in the merchShipment table, rather information is actually serialized in the ExtendedDataCollection of the shipment line item that represents the quote for that shipment. That way, if something changes at no fault of the customer (out of stock, need to ship a back order, split into multiple shipments) the information can be retrieved without having to bother the customer and/or change the sale price and can be completely handled by the warehouse staff.
It does return a null exception error on
Because shopmentRateQuote ends up being null, which I admittedly don't quite understand, as when I inspect "method", it does return a shipping method I created in the backend, with some dummy rates, so ShipmentRateQuoteByShipMethod() should find some?
I've kept poking at it, and gotten somewhat past it, through a lot of fiddling.
An order now goes through, and a shipping address is now present on the order overview. I still have no fulfillment button, however :(
Also, for some reason, the Sales Listing now doesn't update when funds are captured - it's always listed as "Unpaid", even though everything seems to go off without a hitch, when I press "Capture Funds". The Capture Funds buttons sticks around as well, but I don't recall if that's abnormal, really.
Okay, so immediately after posting, I realized a face-palming-worthy error: the product's extended data, didn't set it for "shippable", which lead to the missing fulfillment button (I have the lovely little button back now).
The bug with Capture Funds is still there though, and pressing Fulfill just results in an error, unfortunately:
After a bit of poking and prodding, the error seems to arise in /App_Code/Merchello/js/merchello.controller.js, in "openFulfillShipmentDialog()". For some reason,
appears to fail - the object itself looks empty, and it never gets into the "then" function. Now, I am a regular orangulartang, in that I don't really know angular, but I do know how to alert() stuff for derpy testing, and "request" does seem to get populated with a shipMethodKey, an invoiceKey, and a lineItemKey, so I don't quite understand why it fails, when the request is fully populated.
So, this seems to be the closest I can get at the moment: The Capture Payment button works without bugging, and "Fulfill" doesn't throw an error. It doesn't do anything either (at all), but there doesn't seem to be any errors, whatsoever. It seems likely that this is related to the missing Shipping Address.
I have tried adding a shipping address thusly:
However, whenever I do
the backend bugs out again, not showing the entire order and loading endlessly, with an error:
@Marc - the shipping address needs to have at very least a country code so that the ship method can be determined.
In your error message, I see it is failing to find the currency symbol, which would indicate that the Currency Code is not present in the extended data in the invoice line items.
In a standard checkout, there is a validation task that runs the following code:
This ensures that each line has a currency code set AND that all currency codes within the invoice are consistent. Merchello does not allow for multiple currencies in a single invoice - apply taxes to something like that would be an absolute nightmare =).
Ah, I thought as much, which is why I tried just throwing CurrencyCodes at it wherever I could :P I did try
To confirm that every product I add (this excludes shipping atm though) has a CurrencyCode - I've added one to the quote (AsLineItemOf
It seems a little weird though, because it doesn't break until I add my "quoteItem", and that has a mindlessly hard coded
I have kind of started over on the project entirely, so that it will all be my own code and logic, in stead of doing patchwerk fixing. If I can get it done on time, and if I'm allowed to by the end of this, I'm considering uploading it somewhere, just so examples of crazy stuff exists.
I hear tiered pricing is on its way in Merchello, but in this case, it's achieved by adding an Archetype to the product nodes and updating the prices accordingly as they're added to the basket - serializing and deserializing a Json object to pass along a dictionary of tiers and prices. I have a sneaking suspicion that this might have been where this project broke - juggling "real" and "artificially constructed" products and prices, making them lose ExtendedData at one point or another.
@Marc - Your project sounds really interesting and you've had to handle quite a few difficult requirements.
I think you're correct that it is the shipping line item and, assuming your quoteItem is the shipping line item, it looks like you are adding it correctly.
I agree that the currency codes are set a bit weird. I just have not had a chance to build out a concept to pretty up that aspect of the API and have the problem that I don't want to break any shipping provider someone has coded as a plugin.
I'm off for the weekend, but would like to look at your scenario more closely - would you have time to setup a Skype call next week?
Yeah, that'd be cool - after starting over on the project, I think I've gotten a more solid structure up and running than I had before, but completing orders is still quite tricky with my limited experience. I reckon I can take a day off work during the week to fiddle with it at home and take a call for a while. I have to hand over the project on Thursday, but I should be able to manage either Tuesday or Wednesday. But if that doesn't work for you, it'd still be interesting to get it finished afterwards, so Friday or after is also great.
I'm around all week. Ping me on twitter and we can schedule something from there.
So after a Skype session with Rusty, the problem turned out to be some missing extended data in my products, which was lost when they were added to the basket, due to the tiered pricing. This is fixed now, and I have made a post about it here - Tiered Pricing.
Thanks again to Rusty for helping.
is working on a reply...