Stuff & Nonsense

Magento: Round up currency conversions

Just a quick and dirty (no, not that kind of dirty) tutorial today. One of the websites I work on had the requirement to make its prices look ‘nicer’ when converted to another currency. Currently (little currency pun for you there) the prices look fine in the default currency, which is good old pound sterling like so: £8.00 but when a customer selects one of the other available currencies they see something like $17.34. So, how to make the prices look nice no matter what the currency?

A good, but time consuming way of fixing this problem would be to enter fixed prices for each product in every currency, but who wants to go to that amount of effort? A much easier way would be to round up whenever a currecny conversion is done. Luckily, thanks to Magento’s excellent extensibility this is actually incredibly simple.

First we track down the class which magento uses to deal with currency conversions. A quick google search reveals that the class we want is a Model found in:

/app/code/core/Mage/Directory/Model/Currency.php

And the class name is: Mage_Directory_Model_Currency

Helpfully, we only need to override one method in this class, the aptly named ‘convert’ which looks like this:

 

/**
 * Convert price to currency format
 *
 * @param double $price
 * @param string $toCurrency
 * @return double
 */
 public function convert($price, $toCurrency=null)
 {
     if (is_null($toCurrency)) {
       return $price;
     }
    elseif ($rate = $this->getRate($toCurrency)) {
       return $price*$rate;
    }
    throw new Exception(Mage::helper('directory')->__('Undefined rate from "%s-%s".', $this->getCode(), $toCurrency->getCode()));
 }

The actual code change we need to make here is trivial, all we need to do is change this:

return $price* $rate;

to this:

return ceil($price*$rate);

And since the actual code is so simple, I thought I’d use the rest of this post to go over the various methods of overriding classes like this in magento, and the pros & cons of each.  For standard magento classes like this one, we basically have three ways of making changes to theirfunctionality.  I’ll discuss these in ascending order of… ‘goodness’ (for want of a better term) so if you just want to know the best way, skip to the end.

First up, we have editing the original source file.  This will live somewhere under /app/code/core (in our case it’s /app/code/core/Mage/Directory/) and editing files in here is a bad idea.  Any changes you make will be lost if you upgrade magento, and if you change something and break it you have to deal with restoring backed up files and that can get really messy.

Option 2 is to copy the directory structure of /core into /local (or /community, the effect is the same).  Magento’s auto class loader has a stictly defined way of ‘looking’ for files containing the class it’s currently loading.  Anything in ‘community’ will override the same file in ‘core’ and anything in ‘local’ will override the other 2.  So, if we create a copy of /app/code/core/Mage/Directory/Currency.php in /ap/code/local/Mage/Currency.php and make our changes in there, our changed file will have priority over the original and override it.  The downside to this approach is that it’s not particularly portable or elegant.  You have to copy the entire original file to make one simple change, and you have to replicate the folder structure under /core to make it work… which isn’t particularly nice.

Luckily, Magento offers us a third option (although really, this should always be option 1) and that’s to make use of Magento’s ‘rewrite’ functionality.  Using this, we can override a specific method within a Magento class, we can namespace our files so they stay separated from Magento and other third party add-ons and it’s generally speaking a much cleaner and more elegant solution.  In this example my namespace is going to be ‘Tallpaul’ and my modulename is ‘Rounding’.  Be careful when choosing a namespace and make note of case sensitivity.  I like to keep my namespaces all lowercase apart from the initial letter, and this works well but I’ve had problems in the past with camel cased namespaces (for example TallPaul)…magento is very sensitive about case.

So first we need to create our class, this goes in: /app/code/local/Tallpaul/Rounding/Model/Currency.php

and it looks like this:

class Tallpaul_Rounding_Model_Currency extends Mage_Directory_Model_Currency{
 /**
 * Convert price to currency format
 *
 * @param   double $price
 * @param   string $toCurrency
 * @return  double
 */
 public function convert($price, $toCurrency=null)
 {
    if (is_null($toCurrency)) {
      return $price;
    }
    elseif ($rate = $this->getRate($toCurrency)) {
       return ceil($price*$rate);
    }
    throw new Exception(Mage::helper('directory')->__('Undefined rate from "%s-%s".', $this->getCode(), $toCurrency->getCode()));
 }
}

No surprises here, we’re extending from the original magento class we want to add functionality to. Notice how the only method we declare here is the one we want to override.  Any other methods are handled by the original class, so we don’t need to worry about them.

Now, lets tell Magento that we want our class to ‘rewrite’ the magento class.  We do this with this file:/app/code/local/Tallpaul/Rounding/etc/config.xml

And that looks like this:

<?xml version="1.0" encoding="UTF-8"?>
 <config>
   <modules>
     <Tallpaul_Rounding>
       <version>0.1.0</version>
     </Tallpaul_Rounding>
   </modules>
 <global>
   <models>
     <directory>
       <rewrite>
         <currency>Tallpaul_Rounding_Model_Currency</currency>
       </rewrite>
     </directory>
   </models>
 </global>
 </config>

Here we declare our module and tell magento what version it is, then under the ‘global’ tag we define our rewrite.  In this case we want to rewrite a ‘Model’ so we use the models tag, and the model we want to rewrite is one under the ‘directory’ module.  Specifically it’s the ‘currency’model.  On the file system this corresponds to our original magento class file /app/code/core/directory/model/currency.php.  The mapping between the xml here and the classpath is a little difficult to wrap your head around, but once you’ve worked with Magento a while it suddenly clicks into place and you can easily work out where something lives based on its xml ‘description’.

Now we’ve told Magento what our module does, we need to ‘register’ it with magento and activate it.  As with all modules we do this by adding an xml file to /app/code/etc/.  By convention this should be named ‘namespace_modulename’, which gives us a filename of: /app/code/etc/Tallpaul_rounding.xml and this file looks like this:

<?xml version="1.0"?>
   <config>
     <modules>
       <Tallpaul_Rounding>
         <active>true</active>
         <codePool>local</codePool>
         <depends>
           <Mage_Directory/>
         </depends>
       </Tallpaul_Rounding>
     </modules>
   </config>

A couple of things to note in this file:  First up the ‘codePool’ tag.  This corresponds to the directory under ‘code’ that our modules lives in (so local, community or core).  Also, I’ve added a dependency to ‘Mage_directory’, ie: the module we’re adding functionality to.  This is just to make sure that core module is definitely loaded before we try to add our functionality.   Also worth noting is that you can have as many modules as you want in this activation file by adding more tags under the ‘modules’ tag.  By convention, activation files which contain multiple modules are usually used to activate all the modules in a single namespace and are named ‘Namespace_All.xml.

Anyway, with our activation file in place the module should now work as expected.

Because this is such a simple and trivial mod to produce, I don’t think I need to add downloadable source to this article…however, if you really are that lazy let me know in the comments and I’ll get it uploaded 😀

 

8 thoughts on “Magento: Round up currency conversions

  1. Hi Paul, thanks a lot for the extension and information shared. It is working as expected. I have learned something new today. bye Michal

  2. Hi there,
    I was looking for a solution a long time. I thought I found it now. unfortunately it does not work with configurable products. we sell clothes and as soon as you chose a size, the ugly price appears again. so, I am afraid, the mentioned code change need to be done somewhere else, too if you use configurabel products.

    any idea how to handle this?

    I wish you a great 2013.

    Mark

  3. Hi, has anybody tried this solution with paypal (express) as the payment solution?
    I’ve used this tutorial, and just found out, that paypal is not getting the converted prices, as the prices that paypal will charge the costumer is not rounded up.

    This causes a conflict that the ordre is not paid when the user is charged eg. 19.30 € but the order says 20 €…

    Does anyone have a solution to this?

  4. Hi Mark,

    I’ve done exactly like you but it now leads to a more complicated thing.

    Normally that would convert the price correctly when going to checkout, the baseSubtotal and baseGrandTotal, but it’s calculating very wrong. ie. my base currency: MYR, current currency: SGD. The product is 49.90 MYR. so when converted into SGD: 19. But guess what? baseSubtotal and baseGrandTotal when add the product to cart and check out: 40.11 MYR. the rate is 0.4012. Wow! I’m pulling my hair out to know why this is happening…:sigh:

  5. My base currency is USD
    now my product price is can say $10 and if someone convert it in INR its look like Rs1,000.31 but want to show it only Rs1,000 this is the code in price.phtml echo $_coreHelper->currency($_price, true, true)
    can you tell me how to format price like Rs1,000

    Thanks

  6. This causes a bug with shopping cart promotions using a discount price that is a fraction.
    ie: 1.25 rebate per item for 4 items, makes the total rebate 4$ instead of 5$.

    Looking for another method… Cheers!
    Max.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.