Thursday, April 29, 2010

How to add a block above the 3 columns in 3 columns layout

Problem: Adding on home page a block above the 3 columns
http://www.magentocommerce.com/boards/viewthread/180617/

Possible solution:
Here is how i did it.
Edit app/design/frontend/{interface}/{theme}/layout/page.xml
In the <default> tag add this
<block type="core/text_list" name="top" as="top" translate="label">
<label>Top Row</label>
</block>
Now go to admin panel: Cms->Pages and edit the homepage.
I've created a new phtml file in which I placed the content I want on top
Let's say it's app/design/frontend/{interface}/{theme}/template/callouts/top_callout.phtml
In the design tab for 'Layout Update XML' I put this:

<reference name="top">
<block type="core/template" name="top.callout" template="callouts/top_callout.phtml" />
</reference>
You can put every block you want here. I just used this as an example. Just put it inside
<reference name="top">
....
</reference>
Now edit app/design/frontend/{interface}/{theme}/template/page/3columns.phtml
and add this line
<?php echo $this->getChildHtml('top') ?>
right under
<?php echo $this->getChildHtml('header') ?>
Save all files you edited and clear the contents of 'var/cache/'

I attached an image to see how it looks like.



View full size image

Thursday, April 22, 2010

How to change invoice status in Admin panel

Problem: When creating Invoice directly from Order in admin the invoice automatically gets marked as Paid. how can it be created with the status 'Pending' and marked as paid later. http://www.magentocommerce.com/boards/viewthread/54391/

Possible solution:
In order to create a 'pending' invoice, the payment method you use for the order must allow you to.
For that you need to add in the payment method's model this member:
protected $_canCapture = true;
You can add it right under
protected $_code  = 'PAYMENT CODE HERE';
For example if you want to do this for Check/money order you need to add the line above in app/code/core/Mage/Payment/Model/Method/Checkmo.php

After this when you create an invoice you will have an option near the save button to set the invoice status. (see picture attached). To create a pending invoice select 'Not capture'.
If you want to mark it later as paid edit the invoice and between the buttons on to one called 'Capture' (see picture). Well you get the idea....

Tuesday, April 13, 2010

Handle multi-language

Possible solution:
This is actually very simple. One store view = one language.
You download the desired language pack. For example French(France).
You should get a file called fr_FR_default_full_package.zip (or any other archive format)
Unzip this file over the root of your Magento application.
After that go to the admin panel. System->Configuration. Select from the top-left drop-down the store view you want to be in French and under the General tab 'Locale options' set Locale as French(France).
Save and clear the cache and you should have a store view in French. As for the products and categories again is simple. Edit the product you desire, Select from the top-left drop-down the French store view and enter the French values for the text attributes.

Monday, April 12, 2010

Move footer links to the top

Problem: How to move footer links to the top of the page
More details here:http://www.magentocommerce.com/boards/viewthread/178685/ or here http://www.magentocommerce.com/boards/viewthread/177423/

Possible solution:
Edit this file:
app/design/frontend/{interface}/{theme}/layout/page.xml
Look for this line:
<block type="page/template_links" name="footer_links" as="footer_links" template="page/template/links.phtml"/>
It should be inside of an other <block> tag named footer.
Remove it from there and put it inside the <block> named 'header' (same file).
It should look like this (or close to it):
BEFORE:
<block type="page/html_header" name="header" as="header">
<block type="page/template_links" name="top.links" as="topLinks"/>
<block type="page/switch" name="store_language" as="store_language" template="page/switch/languages.phtml"/>
<block type="core/text_list" name="top.menu" as="topMenu"/>
</block>
AFTER:
<block type="page/html_header" name="header" as="header">
<block type="page/template_links" name="top.links" as="topLinks"/>
<block type="page/switch" name="store_language" as="store_language" template="page/switch/languages.phtml"/>
<block type="core/text_list" name="top.menu" as="topMenu"/>
<block type="page/template_links" name="footer_links" as="footer_links" template="page/template/links.phtml"/>
</block>

Then edit app/design/frontend/{interface}/{theme}/template/page/html/header.phtml
Add this line at the end of the file (or where ever it fits your needs)
<?php echo $this->getChildHtml('footer_links') ?>
Clear the contents of dir var/cache and it should work. You might need to do some css work so the new added links would look nice.

Thursday, April 8, 2010

Adding a sidebar login box

Problem:
How to add a login box on sidebar.
More details here http://www.magentocommerce.com/boards/viewthread/178324/

Possible solution:
First create a phtml file. Let's call it mini.login.phtml.
Place it in app\design\frontend\{interface}\{theme}\template\customer\form\mini.login.phtml
Make sure the form has the same action as in login.phtml Also give the inputs the same name as they have in login.phtml.

Edit app\design\frontend\{interface}\{theme}\layout\customer.xml
under the tag <customer_logged_out> put the following
<reference name="left">
<block type="customer/form_login" name="customer_mini_login" before="-" template="customer/form/mini.login.phtml"/>
</reference>
Clear the cache and refresh.
This will put the mini.login at the top of your left column.If you want it lower play with the 'before' attribute. (You can even replace it with after)
See more details here. http://www.magentocommerce.com/design_guide/articles/intro-to-layouts

The side effect of this is that all the pages that contain this block will have the meta-tille 'Customer Login'. (at least for version 1.4.0.1)
To avoid this you have 2 options.
The 'quick and dirty' one.
Edit app\code\core\Mage\Customer\Block\Form\Login.php and in method _prepareLayout() remove this line:
$this->getLayout()->getBlock('head')->setTitle(Mage::helper('customer')->__('Customer Login'));
or remove the method completely (same effect).
This will work but the problem will reappear after an upgrade.
The 'by the book' option.
Overwrite the Mage_Customer_Block_Form_Login block. See tutorial here:
[http://magedev.com/2009/06/03/magento-overriding-model-block-or-helper/
After that in the the class that extends Mage_Customer_Block_Form_Login create the method _prepareLayout() that does nothing. Just returns the current object.
protected function _prepareLayout()
{
return $this;
}

Thursday, April 1, 2010

Create bulk discount rules

Problem:
Create bulk discount rules with unique coupon codes.
Details found here: http://www.magentocommerce.com/boards/viewthread/177465/
Possible solution:
This is not an extension, it's just a 'quick and dirty' script that I hope some day (when I have the time) will grow up and become an extension.

put the code below into a file (let's call it coupons.php) and follow the instructions below:

<?php
$mageFilename = 'app/Mage.php';

require_once $mageFilename;

Varien_Profiler::enable();

Mage::setIsDeveloperMode(true);

ini_set('display_errors', 1);

umask(0);
Mage::app('default');
Mage::register('isSecureArea', 1);
function generateUniqueId($length = null){
$rndId = crypt(uniqid(rand(),1)); 
$rndId = strip_tags(stripslashes($rndId)); 
$rndId = str_replace(array(".", "$"),"",$rndId); 
$rndId = strrev(str_replace("/","",$rndId));
if (!is_null($rndId)){
return strtoupper(substr($rndId, 0, $length));
} 
return strtoupper($rndId);
} 
function getAllCustomerGroups(){
//get all customer groups
$customerGroups = Mage::getModel('customer/group')->getCollection();
$groups = array();
foreach ($customerGroups as $group){
$groups[] = $group->getId();
}
return $groups;
}
function getAllWbsites(){
//get all wabsites
$websites = Mage::getModel('core/website')->getCollection();
$websiteIds = array();
foreach ($websites as $website){
$websiteIds[] = $website->getId();
}
return $websiteIds;
}
//read comments for each line
function generateRule(){
$uniqueId = generateUniqueId(10);
$rule = Mage::getModel('salesrule/rule');
$rule->setName($uniqueId);
$rule->setDescription('Generated by tzyganu');
$rule->setFromDate(date('Y-m-d'));//starting today
//$rule->setToDate('2011-01-01');//if you need an expiration date
$rule->setCouponCode($uniqueId);
$rule->setUsesPerCoupon(1);//number of allowed uses for this coupon
$rule->setUsesPerCustomer(1);//number of allowed uses for this coupon for each customer
$rule->setCustomerGroupIds(getAllCustomerGroups());//if you want only certain groups replace getAllCustomerGroups() with an array of desired ids 
$rule->setIsActive(1);
$rule->setStopRulesProcessing(0);//set to 1 if you want all other rules after this to not be processed
$rule->setIsRss(0);//set to 1 if you want this rule to be public in rss
$rule->setIsAdvanced(1);//have no idea what it means :)
$rule->setProductIds('');
$rule->setSortOrder(0);// order in which the rules will be applied

$rule->setSimpleAction('by_percent');
//all available discount types
//by_percent - Percent of product price discount
//by_fixed - Fixed amount discount
//cart_fixed - Fixed amount discount for whole cart
//buy_x_get_y - Buy X get Y free (discount amount is Y)

$rule->setDiscountAmount('20');//the discount amount/percent. if SimpleAction is by_percent this value must be <= 100
$rule->setDiscountQty(0);//Maximum Qty Discount is Applied to
$rule->setDiscountStep(0);//used for buy_x_get_y; This is X
$rule->setSimpleFreeShipping(0);//set to 1 for Free shipping
$rule->setApplyToShipping(1);//set to 0 if you don't want the rule to be applied to shipping
$rule->setWebsiteIds(getAllWbsites());//if you want only certain websites replace getAllWbsites() with an array of desired ids

$conditions = array();
$conditions[1] = array(
'type' => 'salesrule/rule_condition_combine',
'aggregator' => 'all',
'value' => "1", //[UPDATE] added quotes on the value. Thanks Aziz Rattani [/UPDATE]
'new_child' => ''
);
//the conditions above are for 'if all of these conditions are true'
//for if any one of the conditions is true set 'aggregator' to 'any'
//for if all of the conditions are false set 'value' to 0.
//for if any one of the conditions is false set 'aggregator' to 'any' and 'value' to 0
$conditions['1--1'] = Array
(
'type' => 'salesrule/rule_condition_address',
'attribute' => 'base_subtotal',
'operator' => '>=',
'value' => 200
);
//the constraints above are for 'Subtotal is equal or grater than 200'
//for 'equal or less than' set 'operator' to '<='... You get the idea other operators for numbers: '==', '!=', '>', '<'
//for 'is one of' set operator to '()';
//for 'is not one of' set operator to '!()';
//in this example the constraint is on the subtotal
//for other attributes you can change the value for 'attribute' to: 'total_qty', 'weight', 'payment_method', 'shipping_method', 'postcode', 'region', 'region_id', 'country_id'

//to add an other constraint on product attributes (not cart attributes like above) uncomment and change the following:
/*
$conditions['1--2'] = array
(
'type' => 'salesrule/rule_condition_product_found',//-> means 'if all of the following are true' - same rules as above for 'aggregator' and 'value'
//other values for type: 'salesrule/rule_condition_product_subselect' 'salesrule/rule_condition_combine'
'value' => 1,
'aggregator' => 'all',
'new_child' => '', 
);

$conditions['1--2--1'] = array
(
'type' => 'salesrule/rule_condition_product',
'attribute' => 'sku',
'operator' => '==',
'value' => '12',
);
*/
//$conditions['1--2--1'] means sku equals 12. For other constraints change 'attribute', 'operator'(see list above), 'value'

$rule->setData('conditions',$conditions);
$rule->loadPost($rule->getData());
$rule->save();
//[UPDATE]if you work with Mangento EE and you want to link banners to your rule uncomment the line of code below
//Mage::getResourceModel('enterprise_banner/banner')->bindBannersToSalesRule($rule->getId(), array(1,2));//the array(1,2, ...) is the array with all the banners you want to link to the rule.
//[/UPDATE]
}
?>


[UPDATE]. This was made for Magento 1.3 and lower. For versions >= 1.4 you have to add a new line
$rule->setCouponType(2);
Add it just before
$rule->save();
(Thanks Raj for pointing this out).
[/UPDATE]

[UPDATE 1.1]
For Magento 1.4 and later there are store labels for the discount rules.
To add this construct the following array
$labels = array();
$labels[0] = 'Default store label';//default store label
$labels[1] = 'Label for store with id 1';
//....
$labels[n] = 'Label for store with id n';
//add one line for each store view you have. The key is the store view ID

and before calling $rule->save(); add this line
$rule->setStoreLabels($labels);

(Thanks Sigmund for the update)
[/UPDATE 1.1]

Put the newly created file in the root of your magento installation (same level as index.php)
Walk through the file read all the comments and configure your discount settings (some programming skills are requred)
After everything is done add the following code at the end of the file (before php closing tag)

for ($i=1;$i<=200;$i++){//replace 200 with the number of coupons you want
generateRule();
}
[UPDATE 1.2]
If you want a nice script to import coupons using csv files with code, description and discount check this out: http://www.gielberkers.com/bulk-import-coupon-codes-in-magento/
Nice job Giel :). [/UPDATE 1.2]
call the page in you browser (http://your.magento.root/coupons.php) or run it from the command line.
Now check your admin for the new added rules.
PS (this does not work yet for 'Actions' tab).

Tuesday, March 16, 2010

Jut for fun!

While working on a theme for Magento 1.4.0.1 I found this in app\design\frontend\base\default\template\checkout\onepage\shipping.phtml between lines 49 and 56
<?php if(false): ?>
<div class="fields">
<label for="shipping:email" class="required"><em>*</em><?php echo $this->__('Email Address') ?></label>
<div class="input-box">
<input type="text" name="shipping[email]" id="shipping:email" value="<?php echo $this->htmlEscape($this->getAddress()->getEmail()) ?>" title="<?php echo $this->__('Email Address') ?>" class="input-text validate-email required-entry" />
</div>
</div>
<?php endif ?>

Good one Magento Team.

Monday, March 1, 2010

Retrieve attributes by groupId

Problem: how to retrieve product attributes by group id.
See here: http://www.magentocommerce.com/boards/viewthread/79694/

Possible solution:
<?php
require_once 'path/to/app_dir/app/Mage.php';
Mage::app('default');
$product = Mage::getModel('catalog/product');
$product->setAttributeSetId('THE ATTRIBUTE SET YOU WANT TO USE');
$groupId = 'THE ID OF THE GROUP YOU WANT TO RETRIEVE';

$collection = $product->getAttributes($groupId, false);
foreach($collection as $attribute){
//do something with the attribute
}
?> 

Saturday, February 27, 2010

IPv6 for online customers

Problem : how to make Magento show the ipv6 ip address on online customers.
See full text here http://www.magentocommerce.com/boards/viewthread/79160/

Possible (untested) solution. If someone tests it please tell me the results also:

In theory you cannot do this, because in the database the fields for ip address are 'bigint' (see tables that start with log_visitor_)
All the ip addresses stored in this table 'suffer' a modification with ip2long() method. Unfortunately php does not support IPv6 (at least that's what I know. If I'm wrong someone please correct me).
Enough with the problems.
Here is what you can try.
Change the type of the columns for ip addresses from bigint to varchar.
In app/code/core/Mage/Log/Model/Visitor.php look for these lines:
$this->addData(array(
'server_addr'           => $helper->getServerAddr(true),
'remote_addr'           => $helper->getRemoteAddr(true), 
....
and remove the 'true' parameters from the functions.
$this->addData(array(
'server_addr'           => $helper->getServerAddr(),
'remote_addr'           => $helper->getRemoteAddr(), 
This should save the ip addresses as strings.

We are half way there. Now all you have to do is make sure that the ip addresses are rendered in the back-end grid you need to change the \app\code\core\Mage\Adminhtml\Block\Customer\Online\Grid\Renderer\Ip.php file.
Change this
public function render(Varien_Object $row)
{
return long2ip($row->getData($this->getColumn()->getIndex()));
}

to
public function render(Varien_Object $row)
{
return $row->getData($this->getColumn()->getIndex());
}

or to this, for backwards compatibility (to see the previous saved ip addresses)
public function render(Varien_Object $row)
{
$val = $this->getColumn()->getIndex();
if (is_int($val)){
return long2ip($val);
}
return $val;
}

Clear the cache and test it.
I have no way of testing this but it seams to me that this is the way to go.

(Of course you don't need to modify the core files if you plan to upgrade, you can overwrite them the proper magento way ).

Monday, February 1, 2010

Save an object into the database with a specific id. (insert - not update)

Problem can be found here:

Possible solution:

The presence of an id means update and if the id is null it means is insert.
So when you try to insert and add an id Magento is actually trying to update the entity but the id you specify does not exist.
It's something similar to this:
UPDATE some_table SET some_field = some_value WHERE id = some_id; 
The sql goes through but nothing gets updated.
If you want to modify something (but I don't advice you to do this) you can change the Mage_Core_Model_Mysql4_Abstract and Mage_Core_Model_Abstract models (the save() method for both of them.)

If you look inside the save method of Mage_Core_Model_Mysql4_Abstract you will see that there's an 'if' statement
if (!is_null($object->getId())) {...}
This means exactly what I explained above.

Here is an idea on how you can make magento do an insert with a specified id.
Overwrite the Mage_Core_Model_Abstract::save() method to accept and other parameter.
Let's call it $alwaysInsert and set it's default value to false (so you will not interfere with the other objects that use save()).
Then pass the $alwaysInsert parameter to the save method from the resource.
Your new method could look like this:
public function save($alwaysInsert = false)
{
$this->_getResource()->beginTransaction();
try {
$this->_beforeSave();
$this->_getResource()->save($this, $alwaysInsert);
$this->_afterSave();

$this->_getResource()->commit();
}
catch (Exception $e){
$this->_getResource()->rollBack();
throw $e;
}
return $this;
}

In Mage_Core_Model_Mysql4_Abstract, change the method save() to accept 2 parameters.
It could look like this:
public function save(Mage_Core_Model_Abstract $object, bool $alwaysInsert)
{
if ($object->isDeleted()) {
return $this->delete($object);
}

$this->_beforeSave($object);
$this->_checkUnique($object);

if (!is_null($object->getId()) && !$alwaysInsert) {
$condition = $this->_getWriteAdapter()->quoteInto($this->getIdFieldName().'=?', $object->getId());
$this->_getWriteAdapter()->update($this->getMainTable(), $this->_prepareDataForSave($object), $condition);
} else {
$this->_getWriteAdapter()->insert($this->getMainTable(), $this->_prepareDataForSave($object));
$object->setId($this->_getWriteAdapter()->lastInsertId($this->getMainTable()));
}

$this->_afterSave($object);

return $this;
} 

WARNING:
There are 2 issues (at least that is all I can think about now)
1. I didn't test this. You can test it and tell everyone the results
2. I'm not sure how Magento behaves when extending an abstract class. Normally it shouldn't cause any problems, but make sure the class(es) you are working with extend the new class(es) that you just created (extensions of Mage_Core_Model_Abstract and Mage_Core_Model_Mysql4_Abstract not these classes themselves).

If instead of overwriting you decide to modify the core this should take care of these issues, but you are not suppose to modify the code.
I hope this works for you. If it doesn't I'm sorry.