I need to display (tier) prices based on the qty increments of a product. E.g. a simple product, with a regular price of 50¢, no taxes and qty increments of 20 should be displayed on product views with "$10 per 20".
Without using taxes this should be quite easy. But there seems to be no "default" helper or model to do this with taxes enabled and different calulation algorithms (e.g. Mage_Tax_Model_Calculation::CALC_UNIT_BASE); expect for quotes in Mage_Tax_Model_Sales_Total_Quote_Tax and Mage_Tax_Model_Sales_Total_Quote_Subtotal.
Did I miss something here, or do I have to write the business logic to calculate the price on my own? And how I would best encapsulate it?
I solved my problem for now very simple. For this I used the <rewrite> element in a custom module to expand the helper Mage_Tax_Helper_Data with an additional instance method:
class My_Module_Helper_Tax_Data extends Mage_Tax_Helper_Data {
/**
* Get product price based on stock quantity increments
*
* #param Mage_Catalog_Model_Product $product
* #param float $price inputed product price
* #param null|int $qtyIncrements inputed stock quantity increments
* #param null|bool $includingTax return price include tax flag
* #param null|Mage_Customer_Model_Address $shippingAddress
* #param null|Mage_Customer_Model_Address $billingAddress
* #param null|int $customerTaxClass customer tax class
* #param mixed $store
* #param bool $priceIncludesTax flag what price parameter contain tax
* #return float
*/
public function getQtyIncrementsPrice($product, $price, $qtyIncrements = 1,
$includingTax = null, $shippingAddress = null, $billingAddress = null,
$customerTaxClass = null, $store = null, $priceIncludesTax = null) {
$store = Mage::app()->getStore($store);
$qtyIncrements *= 1;
if ($this->_config->getAlgorithm($store)
== Mage_Tax_Model_Calculation::CALC_UNIT_BASE) {
$price = $this->getPrice(
$product, $price, $includingTax, $shippingAddress,
$billingAddress, $customerTaxClass, $store, $priceIncludesTax
);
$price *= $qtyIncrements;
$price = $store->roundPrice($price);
} else {
$price *= $qtyIncrements;
$price = $this->getPrice(
$product, $price, $includingTax, $shippingAddress,
$billingAddress, $customerTaxClass, $store, $priceIncludesTax
);
}
return $price;
}
}
It can be later used in a custom rewrite of methods such as Mage_Catalog_Block_Product_Price::getTierPrices().
Related
I have a fully mysterious issue with Prestashop 1.7.7.
In database, in ps_product table, all quantities are set to 0. However, in BO, I can see actual quantities.
EDIT: I found the table ps_stock_availabe. Quantities are here. I added this code in my controller:
$champion->loadStockData(); // This is a Product instance.
$chevalier->loadStockData(); // This is a Product instance.
$porter->loadStockData(); // This is a Product instance.
$potions->loadStockData(); // This is a Product instance.
$pepites->loadStockData(); // This is a Product instance.
With no luck.
How could I get stock informations in my product instance in controller please ?
Thanks for your time.
I found the way to come across my issue.
$champion = new Product(27);
$chevalier = new Product(28);
$porter = new Product(24);
$potions = new Product(25);
$pepites = new Product(26);
$champion->quantity = Product::getQuantity($champion->id); // Get the actual quantity
$chevalier->quantity = Product::getQuantity($chevalier->id);
$porter->quantity = Product::getQuantity($porter->id);
$potions->quantity = Product::getQuantity($potions->id);
$pepites->quantity = Product::getQuantity($pepites->id);
$champion->price = $champion->getPrice(true, null, 2); // Get the actual price with tax
$chevalier->price = $chevalier->getPrice(true, null, 2);
$porter->price = $porter->getPrice(true, null, 2);
$potions->price = $potions->getPrice(true, null, 2);
$pepites->price = $pepites->getPrice(true, null, 2);
I'm trying to make assertion that the random text entered in one field appears on next page (confirmation)
I do it like this
When I fill in "edit-title" with random value of length "8"
/**
* Fills in form field with specified id|name|label|value with random string
* Example: And I fill in "bwayne" with random value of length "length"
*
* #When /^(?:|I )fill in "(?P<field>(?:[^"]|\\")*)" with random value of length "(?P<length>(?:[^"]|\\")*)"$/
*/
public function fillFieldWithRandomValue($field, $length)
{
$field = $this->fixStepArgument($field);
$value = $this->generateRandomString($length);
$this->getSession()->getPage()->fillField($field, $value);
}
Than I want to make assertion - something like this:
Then I should see text matching "<RANDOM VALUE ENTERED IN THE PREVIOUS STEP>"
is it possible?
UPDATE:
But how would it look like with setters and getters if i want to use a generateRandomString method multiple times and then get the values of this methods one after another? DO I have to make variables and functions for every test step? like this:
When I fill in "x" with random value of length "8"
And I fill in "y" with random value of length "12"
And I go to other page
Then I should see text matching "VALUE ENTERED TO X"
And I should see text matching "VALUE ENTERED TO Y"
You can create a property and set it in the previous step. And use it in the next one, but assert it if it has value.
Also it would be nice and readable to define that property with proper visibility type
/**
* #var string
*/
private randomString;
/**
* Fills in form field with specified id|name|label|value with random string
* Example: And I fill in "bwayne" with random value of length "length"
*
* #When /^(?:|I )fill in "(?P<field>(?:[^"]|\\")*)" with random value of length "(?P<length>(?:[^"]|\\")*)"$/
*/
public function fillFieldWithRandomValue($field, $length)
{
$field = $this->fixStepArgument($field);
$this->randomString = $this->generateRandomString($length);
$this->getSession()->getPage()->fillField($field, $this->randomString);
}
/**
*
* #Then /^(?:|I )should see that page contains random generated text$/
*/
public function assertPageContainsRandomGeneratedText()
{
//Assertion from phpunit
$this->assertNotNull($this->randomString);
$this->assertPageContainsText($this->randomString);
}
NOTE: Depending on your behat setup - assertion from phpunit might not work.
Since you will will call the generateRandomString method in multiple places then you should also have a method for getting this value like getRandomString like setters and getters.
My recommendation would be to have a class with related methods that handle all the data and not saving in variable in every place you will use data, generate+save and read from the same place anywhere you need.
Tip: You could be more flexible about the step definition and have a default length for the random string in case one one not provided.
High level example:
class Data
{
public static $data = array();
public static function generateRandomString($length = null, $name = null)
{
if ($name = null) {
$name = 'random';
};
if ($length = null) {
$length = 8;
};
// generate string like $string =
return self::$data[$name] = $string;
}
public static function getString($name = null)
{
if ($name = null) {
$name = 'random';
};
// exception handling
if (array_key_exists($name, self::$data) === false) {
return null;
}
return self::$data[$name];
}
}
In context:
/**
* #Then /^I fill in "x" with random value as (.*?)( and length (\d+))?$/
*/
public function iFillInWithRandomValue($selector, $name, $length = null){
$string = Data::generateRandomString($length, $name);
// fill method
}
/**
* #Then /^I should see text matching "first name"$/
*/
public function iShouldSeeTextMatching($variableName){
$string = Data::getString($variableName);
// assert/check method
}
This is high level example, you might need to do some adjustments.
If you have the validation in the same class then you can also have all these in the same class, meaning generateRandomString and getString in the same class with the steps.
I need to add some JavaScript to the order confirmation page that includes details about the order. Although I can access the order id through a BigCommerce global variable, I cannot work out how to get the rest of the order details into my JavaScript.
For instance, I can access the BigCommerce order_id global %%GLOBAL_OrderId%% and use that in a JavaScript alert, but I also need to access the following:
order total
order tax
order shipping
order postcode
And foreach product in the order
product_id
unit_price
quantity
There these global items but when I try to access them they are blank, I presume that I need to loop through the cart contents.
%%GLOBAL_ProductModel%%
%%GLOBAL_ProductPrice%%
%%GLOBAL_ProductQty%%
I have read all the docs I can find. Can anyone give me an idea of how to achieve this. I need the values so I can pass them to a third party JS function for their use. All of that is waiting and ready but I cannot get the data out of Big Commerce templating system. The data is there, on the order.html template page, as the social sharing panel reads it, but again I cannot see how the social sharing snippet is accessing it.
I created a hacky script just for you that pulls the product data (as well as some order details).
It parses the data from the %%GLOBAL_ConversionCode%% template variable, and as such this script should be inserted in order.html immediately after the %%GLOBAL_ConversionCode%% variable.
Specifically, %%GLOBAL_ConversionCode%% outputs to:
<!-- Include the conversion tracking code for all analytics packages -->
<!-- Start conversion code for analytics_googleanalytics -->
<script type="text/javascript">
if(typeof(pageTracker) != 'undefined') {
pageTracker._addTrans(
'196',
'store-name',
'0.00',
'2.12',
'1.92',
'Austin',
'Texas',
'United States'
);
pageTracker._addItem(
'196',
'2004',
'TAKE YOUR TIME: Sweet Body Butter',
'',
'24.96',
'1'
);
pageTracker._trackTrans();
}
</script>
Solution:
<script>
//-------------- Main --------------//
//** Create the order data array from analytics script **//
var data = parseAnalyticsData(getAnalyticsScript());
//console.log(data);
/**
* Retrieve the order details as an object, properties are:
* id - The order ID.
* shipping - The order shipping cost.
* tax - The order tax cost.
* shippingTax - The order shipping tax cost.
* city - The order shipping city.
* state - The order shipping state.
* country - The order shipping country.
*/
var orderDetails = getOrderDetails(data);
console.log("Order ID = %d", orderDetails.id);
console.log("Order shipping city = %s", orderDetails.city);
console.log("Order subtotal = %f", orderDetails.subtotal);
/**
* Retrieve the order product details, as an array of product objects.
* Properties are:
* id - The product ID.
* description - The product description.
* tax - The product tax cost.
* price - The product price per product.
* qty - The product quantity purchased.
*/
var products = getOrderProducts(data);
//** Loop through the products array to access each product **//
console.log("Total number of products = %d", products.length);
for (x=0; x<products.length; x++) {
console.log("--------");
console.log("Item # ", x+1);
console.log("Product ID = %f", products[x].id);
console.log("Product QTY = %f", products[x].qty);
console.log("Product Price = %f", products[x].price);
console.log("--------");
}
//-------------- Functions --------------//
/**
* Parses the DOM to retrieve the order data analytics script.
*/
function getAnalyticsScript() {
var scripts = document.getElementsByTagName('script');
var thisScriptTag = scripts[scripts.length - 2];
var data = thisScriptTag.textContent || thisScriptTag.innerText;
return data;
}
/**
* Parses the raw analytics script element to remove all script
* text, and parse just the order related data into an array.
* #param script <String> - The raw order analytics script.
* #return <mixed> - Array containing the order data.
*/
function parseAnalyticsData(data) {
String.prototype.replaceAll = function(search, replacement) {
var target = this;
return target.split(search).join(replacement);
};
// This is hacky, and probably inefficient, but it removes all
// script related text, so the end result is just a comma separated
// array of the order and product data.
data = data.replace("if(typeof(pageTracker) != 'undefined') {", '');
data = data.replaceAll( 'pageTracker._addTrans(', '');
data = data.replaceAll( ' pageTracker._trackTrans();', '');
data = data.replaceAll( 'pageTracker._addItem(', '');
data = data.replaceAll(');', '');
data = data.replace('}', '');
data = data.replace( /\n/g, ",").replaceAll( ",,",",");
data = data.replace(/\s/g,'');
data = data.split(',');
data = cleanArray(data); // Remove all empty values from array.
return data;
}
/**
* Removes all empty data from array.
* #param array <mixed> - The array to clean.
*/
function cleanArray(array) {
var newArray = new Array();
for (var i = 0; i < array.length; i++) {
if (array[i]) {
newArray.push(array[i]);
}
}
return newArray;
}
/**
* Parse Analytics Data for Order Details
* #param data <mixed> - The order analytics data.
* #return <mixed> - Object containing the order details.
*/
function getOrderDetails(data) {
String.prototype.replaceAll = function(search, replacement) {
var target = this;
return target.split(search).join(replacement);
};
return {
id : parseFloat(data[0].replaceAll("'",'')),
subtotal : ( parseFloat(data[2].replaceAll("'",'')) - (parseFloat(data[3].replaceAll("'",'')) + parseFloat(data[4].replaceAll("'",'')) ) ),
total : parseFloat(data[2].replaceAll("'",'')),
tax : parseFloat(data[3].replaceAll("'",'')),
shipping : parseFloat(data[4].replaceAll("'",'')),
city : data[5].replaceAll("'",''),
state : data[6].replaceAll("'",''),
country : data[7].replaceAll("'",'')
}
}
/**
* Parse Analytics Data for All Order Product Details.
* #param data <mixed> - The order analytics data.
* #return <mixed> - Array containing individual product details.
*/
function getOrderProducts(data) {
String.prototype.replaceAll = function(search, replacement) {
var target = this;
return target.split(search).join(replacement);
};
var counter = -1; // Keep index of details per product.
var productsArray = []; // Init empty array to hold all products.
var product = {}; // Init empty object to hold single product data.
//** Product data starts at index 8 **//
for (x=8; x<data.length; x++) {
counter++;
switch (counter) {
case 1:
product.id = parseFloat(data[x].replaceAll("'",''));
break;
case 2:
product.description = data[x].replaceAll("'",'');
break;
case 3:
product.tax = parseFloat(data[x].replaceAll("'",''));
break;
case 4:
product.price = parseFloat(data[x].replaceAll("'",''));
break;
case 5:
product.qty = parseFloat(data[x].replaceAll("'",''));
counter = -1; // reset counter
productsArray.push(product); // push product to products array
product = {};
break;
}
}
return productsArray;
}
</script>
In my company we have a very specific pricing strategy:
Every Product in our catalog has a baseUsdPrice, for example product Foo has base USD price of 9.99$.
That does not necessary mean that will be your you'll be paying 9.99$. We check your country for price exceptions - for example in GB Foo costs 8.99$
Further more you can choose a currency you want to pay - if you're in GB you can pay in either USD (mentioned 8.99$) or your local currency (in this case GBP).
If you choose to pay by your local currency we calculate an equivalent of 8.99$ to british pounds based on fixed price matrix (for example here that'll be 3.99£)
This the price you pay.
How should I design my Product aggregate root in DDD manner to be clean, cohesive, decoupled and easy to change?
Should paymentPrice be calculated by domain service and its result put as a part of Product aggregate? If so that means my ProductRepository will have methods like product(productId, countryId, currency)
Should I put all calculations in domain service class PaymentPriceCalculator and use visitor pattern like getPaymentPrice(Country, Currency)? What if I need to use paymentPrice from my entity to perform some business rule checks?
I'm trying to wrap my head around it and I think I'm overthinking it and it hurts. ;)
I would lean towards your second option of having a PaymentPriceCalculator. This way you would be able to have different calculators if you ever decided to change the algorithm or use multiple algorithms.
I would not put it in the Product aggregate since the price varies by country. It would also make your Product class more complicated. From a domain point of view, aren't 2 otherwise identical products "equal" even if they are purchased in different countries?
Visitor pattern doesn't really fit here.
I might also have a 2nd service that converts $ into whatever currency is needed. This should be separate service since your domain appears to use dollars for everything until the very end when the user needs to actual pay for stuff. This way you can also add applicable taxes / VAT etc as separate from the logic of figuring out price exceptions by country.
Agree with #dkatzel, aggregate is not a good place to hold calculation.
Assume that the product hold the calculation:
product.paymentPrice(countryId, currency, fixedPriceMatrix)
To test this, you need to build a product in each test case although some cases focus on currency choosing only.
countryId = ...
currency = ...
fixedPriceMatrix = ...
basePrice = ...
countryPrice = ...
product = new Product(id, basePrice, countryPrice...)
paymentPrice = product.paymentPrice(countryId, currency, fixedPriceMatrix)
In real projects, aggregates holds many information (for different purposes) which makes them relatively more difficult to setup in tests.
The easies way to test the current calculation is using a value object PaymentPrice
//in unit test
paymentPrice = new PaymentPrice(basePriceForYourCountry, currency, fixedPriceMatrix)
value = paymentPrice.value()
//the product now holds countryPrice calculation
countryPrice = new Product(id, basePrice).countryPrice(countryId);
You can use PaymentPriceCalculator as a stateless domain service as the factory:
class PaymentPriceCalculator {
PaymentPrice paymentPrice(product, countryId, currency) {
fixedPriceMatrix = fixedPriceMatrixStore.get()
return new PaymentPrice(product.countryPrice(countryId), currency, fixedPriceMatrix())
}
}
The potential change could be:
algorithm change(you can extract PaymentPrice as superclass and introduce various subclass for different algorithms)
more options for user. This may break existing method signature to add more parameters. You can introduce a parameter object to hold countryId, currency and others.
I think the most appropriate design pattern for this problem is Strategy Design Pattern.
Please refer below links to understand how it can be applied in this scenario.
Strategy Design Pattern - Payment Strategy
Strategy Design Pattern - Overview
Please note you can combine Strategy Pattern with other patterns like Factory and Composite if you need more than one strategy to get the final price.
I would declare an object responsible for calculating Product's price, for example:
interface ProductPriceCalculator {
public function determineProductPrice($productId, Currency $currency = null);
}
Since each product has the basePrice and we would like to modify it can be organized like this:
class Product {
private $id;
/**
* Initially base price
* #var Money
*/
private $price;
public function modifyPrice(ProductPriceCalculator $calculator, Currency $currency = null) {
$newPrice = $calculator->determineProductPrice($this->id, $currency);
if ($newPrice !== null) {
$this->price = $newPrice;
}
}
}
Now in this case you need the country to be the price modifier. The solution that makes sense to me is to reflect that in code exactly:
class Country implements ProductPriceCalculator {
private $id;
/**
* #var Currency
*/
private $currency;
/**
* Hashmap<String, Money> where product id evaluates to price in $this country
* #var array
*/
private $productPrices = array();
/**
* #param string $productId
* #param Currency $currency
* #return Money
*/
public function determineProductPrice($productId, Currency $currency = null) {
if (array_key_exists($productId, $this->productPrices)) {
$productPrice = clone $this->productPrices[$productId];
if ($currency !== null) {
$currency = $this->currency;
}
return $productPrice->convertTo($currency);
}
}
}
Now to support money and currency logic:
class Money {
private $value;
private $currency;
public function __construct($amount, Currency $currency) {
$this->value = $amount;
$this->currency = $currency;
}
public function convertTo(Currency $newCurrency) {
if (!$this->currency->equalTo($newCurrency)) {
$this->value *= $newCurrency->ratioTo($this->currency);
$this->currency = $newCurrency;
}
}
}
class Currency {
private $code;
private static $conversionTable = array();
public function equalTo(Currency $currency) {
return $this->code == $currency->code;
}
public function ratioTo(Currency $currency) {
return self::$conversionTable[$this->code . '-' . $currency->code];
}
}
And finally the client would look something like this:
class SomeClient {
public function someAction() {
//initialize $product (it has the base price)
//initialize $selectedCountry with prices for this product
//initialize $selectedCurrency
$product->modifyPrice($selectedCountry, $selectedCurrency);
//here $product contains the price for that country
}
}
Has anyone tried to create a plugin that updates the record's values in the Quote Product form? I created one, because I need a custom formula that calculates the Extended Amount field, but there are automatic calculations in the CRM that fill these fields. This doesn't allow me to update the formula of calculation at all.
What my plugin do, is:
Gets the values from the fields Price per unit, Quantity and Discount % (which is a custom field);
Calculates the value that I need;
Sets it at the extended amount field.
But, none of this works because I get a "Business Process Error";
Basically this error tells me that I can't use the record's guid to access it.
Here is my code:
protected void ExecutePostQuoteProductUpdate(LocalPluginContext localContext)
{
if (localContext == null)
{
throw new ArgumentNullException("localContext");
}
IPluginExecutionContext context = localContext.PluginExecutionContext;
IOrganizationService service = localContext.OrganizationService;
Guid quoteProductID = (Guid)((Entity)context.InputParameters["Target"]).Id;
ColumnSet set = new ColumnSet();
set.AllColumns = true;
var quote = service.Retrieve("quotedetail", quoteProductID, set);
var priceperunit = quote.Attributes["priceperunit"];
var teamleader = quote.Attributes["new_disc"];
var manualdiscountamount = quote.Attributes["manualdiscountamount"];
var volumediscountamount = quote.Attributes["volumediscountamount"];
var VAT = (int)quote.Attributes["new_vat"];
var discountamount = (double)priceperunit * (double)teamleader / 100;
var baseamount = (double)priceperunit - discountamount;
var tax = baseamount * VAT / 100;
var extendedamount = baseamount + tax;
quote.Attributes["new_discountamount"] = discountamount;
quote.Attributes["baseamount"] = baseamount;
quote.Attributes["tax"] = tax;
quote["description"] = priceperunit;
quote.Attributes["extendedamount"] = extendedamount;
service.Update(quote);
}
Please, tell me if there is a way to access those fields and use/set them.
Thanks in Advance!
:/
You cannot update extendedamount directly - instead if you use the built-in fields which automatically calculate it then CRM will take care of this for you.
Those fields are: baseamount, quantity, discountamount, and tax. These are Money fields so you will need to store them as such:
quote.Attributes["baseamount"] = new Money(baseamount); // Money and decimal numbers use the "decimal" datatype and not double.
As for your custom calculation, you just need to convert the percentage stored in your custom field into the actual amount and assign it to the discountamount attribute. Something like:
var discountamount = (decimal)priceperunit * (decimal)teamleader / 100;
// Assigning the actual discount amount will automatically recalculate the extended amount on the line.
// No need to recalculate all fields.
quote.Attributes["discountamount"] = new Money(discountamount);
Note that tax may need to be recalculated to reflect the discount, but the process should work exactly the same.