Extending Promotions module

Promotions are dedicated to attract customers attention about expired, current and upcoming shop events. Usually it can be used to increase shop sales. Standard version allows to design promotion look and, if needed, setup it for specific user groups.

Tutorial explains how extended standard functionality by creating custom module for the “promotion queue” business case. Tutorial show how to:
1. create promotions products administration;
2. frontend setup for promotions queuing;
3. queue logics implementation.

Administration

To access standard promotions administration area go to “Admin > Customer Info > Promotions”. You can modify promotion description, HTML/Smarty code, change images, leading urls and other parameters. “Active” or “Active From/To” fields allows to define expiration status. Additionally its allowed to assign user groups.

In case your business model requires specific promotion relations with other objects e.g. products, you should create your own assignment solution. Example shows how to create AJAX based product assignment popup.

Product assignment

1. You should extend current promotion administration class “admin/actions_main.php” to define your own template and AJAX popup (object assignment) handling logic:

<?php
/**
* Promotion manager class
*/
class custPromotions_Actions_Main extends custPromotions_Actions_Main_parent
{
/**
* Alternative promotions manager template
* Change it to your own when customizing
*
* @var string
*/
protected $_sThisTemplate = "<YOUR CUSTOM TEMPLATE NAME>";
 
/**
* Calls parent::render(), includes related data for AJAX popup and
* returns name of template to render
*
* @return string
*/
public function render()
{
$sTemplate = parent::render();
 
// fetching promotion object
if ( ( $oPromotion = $this->getViewDataElement( "edit" ) ) && 
( $iAoc = oxConfig::getParameter( "<POPUP IDENTIFIER>" ) ) ) {
$sPopup = false;
// finding which AJAX popup will be shown
switch( $iAoc ) {
case <IDENTIFIER 1>:
$sPopup = 'custpromotions_products';
break;
// here you can define other custom popups..
}
// inlcuding popup files..
if ( $sPopup ) {
$aColumns = array();
include_once "inc/{$sPopup}.inc.php";
$this->_aViewData['oxajax'] = $aColumns;
return "popups/{$sPopup}.tpl";
}
}
// "aoc" parameter usually means standard assignment popup call..
return oxConfig::getParameter( "aoc" ) ? $sTemplate : $this->_sThisTemplate;
}
}

2. Copy and customize standard “out/admin/tpl/actions_main.tpl” template and add object assignment button:

<input type="button" value="[{ oxmultilang ident="<PRODUCT ASSIGNMENT BUTTON LANGUAGE CONSTANT>" }]" class="edittext" onclick="JavaScript:showDialog('&cl=actions_main&<POPUP IDENTIFIER>=1&oxid=[{ $oxid }]');" [{ $readonly }]>

3. Object assignment functionality should be created (e.g. “admin/inc/custpromotions_products.inc.php”):

<?php

// defining fields which will be shown; fields should be on top of file.. // 1 – field name // 2 – table name // 3 – if field is displayed on page load // 4 – if field is multilanguage // 5 – if field is used for object identification (used when assigning object) // // ‘container1’ usually is left side container, where not assigned records are shown // ‘container2’ is right side container, where assigned records are displayed // $aColumns = array( ‘container1’ => array( array( ‘oxartnum’, ‘oxarticles’, 1, 0, 0 ), array( ‘oxtitle’, ‘oxarticles’, 1, 1, 0 ), array( ‘oxean’, ‘oxarticles’, 1, 0, 0 ), array( ‘oxmpn’, ‘oxarticles’, 0, 0, 0 ), array( ‘oxprice’, ‘oxarticles’, 0, 0, 0 ), array( ‘oxstock’, ‘oxarticles’, 0, 0, 0 ), array( ‘oxid’, ‘oxarticles’, 0, 0, 1 ) ), ‘container2’ => array( array( ‘oxartnum’, ‘oxarticles’, 1, 0, 0 ), array( ‘oxtitle’, ‘oxarticles’, 1, 1, 0 ), array( ‘oxean’, ‘oxarticles’, 1, 0, 0 ), array( ‘oxmpn’, ‘oxarticles’, 0, 0, 0 ), array( ‘oxprice’, ‘oxarticles’, 0, 0, 0 ), array( ‘oxstock’, ‘oxarticles’, 0, 0, 0 ), array( ‘oxid’, ‘oxobject2action’, 0, 0, 1 ) ) ); /**

* Class manages promotion products
*/

class ajaxComponent extends ajaxListComponent {

/**
* Returns SQL query for data to fetc
*
* @return string
*/
protected function _getQuery()
{
$myConfig = $this->getConfig();
// looking for table/view
$sArtTable = getViewName('oxarticles');
$sCatTable = getViewName('oxcategories');
$sO2CView  = getViewName('oxobject2category');
$sPromotionId      = oxConfig::getParameter( 'oxid' );
$sSynchPromotionId = oxConfig::getParameter( 'synchoxid' );
// category selected or not ?
if ( !$sPromotionId ) {
// not yet assigned records..
$sQAdd  = " from $sArtTable where 1 ";
$sQAdd .= $myConfig->getConfigParam( 'blVariantsSelection' )?'':"and $sArtTable.oxparentid = '' ";
} else {
// selected category ?
if ( $sSynchPromotionId && $sPromotionId != $sSynchPromotionId ) {
// not yes assigned objects filtered by active category..
$sQAdd  = " from $sO2CView left join $sArtTable on ";
$sQAdd .= $myConfig->getConfigParam( 'blVariantsSelection' )?" ( $sArtTable.oxid=$sO2CView.oxobjectid or $sArtTable.oxparentid=$sO2CView.oxobjectid)":" $sArtTable.oxid=$sO2CView.oxobjectid ";
$sQAdd .= "where $sO2CView.oxcatnid = '$sPromotionId' ";
} else {
// allready assigned records
$sQAdd  = ' from oxobject2action left join '.$sArtTable.' on '.$sArtTable.'.oxid=oxobject2action.oxobjectid ';
$sQAdd .= 'where oxobject2action.oxactionid = "'.$sPromotionId.'" and oxobject2action.oxclass = "oxarticle" ';
}
}
// filtering allready assigned records
if ( $sSynchPromotionId && $sSynchPromotionId != $sPromotionId) {
$sQAdd .= 'and '.$sArtTable.'.oxid not in ( ';
$sQAdd .= 'select '.$sArtTable.'.oxid from oxobject2action left join '.$sArtTable.' on '.$sArtTable.'.oxid=oxobject2action.oxobjectid ';
$sQAdd .= 'where oxobject2action.oxactionid = "'.$sSynchPromotionId.'" and oxobject2action.oxclass = "oxarticle" ) ';
}
return $sQAdd;
}
/**
* Adds filter SQL to current query
*
* @param string $sQ query to add filter condition
*
* @return string
*/
protected function _addFilter( $sQ )
{
$sArtTable = getViewName('oxarticles');
$sQ = parent::_addFilter( $sQ );
// display variants or not ?
$sQ .= $this->getConfig()->getConfigParam( 'blVariantsSelection' ) ? ' group by '.$sArtTable.'.oxid ' : '';
return $sQ;
}
/**
* Removes product from promotion
*
* @return null
*/
public function removeProductFromPromotion()
{
$aChosenArt = $this->_getActionIds( 'oxobject2action.oxid' );
// removing all
if ( oxConfig::getParameter( 'all' ) ) {
$sQ = $this->_addFilter( "delete oxobject2action.* ".$this->_getQuery() );
oxDb::getDb()->Execute( $sQ );
} elseif ( is_array( $aChosenArt ) ) {
$sQ = "delete from oxobject2action where oxobject2action.oxid in (" . implode( ", ", oxDb::getInstance()->quoteArray( $aChosenArt ) ) . ") ";
oxDb::getDb()->Execute( $sQ );
}
}
/**
* Adds protuct to promotion
*
* @return null
*/
public function addProductToPromotion()
{
$aChosenArt = $this->_getActionIds( 'oxarticles.oxid' );
$soxId      = oxConfig::getParameter( 'synchoxid');
// adding
if ( oxConfig::getParameter( 'all' ) ) {
$sArtTable  = getViewName('oxarticles');
$aChosenArt = $this->_getAll( $this->_addFilter( "select $sArtTable.oxid ".$this->_getQuery() ) );
}
if ( $soxId && $soxId != "-1" && is_array( $aChosenArt ) ) {
foreach ( $aChosenArt as $sChosenArt) {
$oObject2Promotion = oxNew( 'oxbase' );
$oObject2Promotion->init( 'oxobject2action' );
$oObject2Promotion->oxobject2action__oxactionid = new oxField( $soxId );
$oObject2Promotion->oxobject2action__oxobjectid = new oxField( $sChosenArt );
// assigned object class name
$oObject2Promotion->oxobject2action__oxclass    = new oxField( "oxarticle" );
$oObject2Promotion->save();
}
}
}
/**
* Formats and returns chunk of SQL query string with definition of
* fields to load from DB. Adds subselect to get variant title from parent article
*
* @return string
*/
protected function _getQueryCols()
{
$myConfig = $this->getConfig();
$sLangTag = oxLang::getInstance()->getLanguageTag();
$sQ = '';
$blSep = false;
$aVisiblecols = $this->_getVisibleColNames();
foreach ( $aVisiblecols as $iCnt => $aCol ) {
if ( $blSep )
$sQ .= ', ';
$sViewTable = getViewName( $aCol[1] );
// multilanguage
$sCol = $aCol[3]?$aCol[0].$sLangTag:$aCol[0];
if ( $myConfig->getConfigParam( 'blVariantsSelection' ) && $aCol[0] == 'oxtitle' ) {
$sVarSelect = "$sViewTable.oxvarselect".$sLangTag;
$sQ .= " IF( $sViewTable.$sCol != '', $sViewTable.$sCol, CONCAT((select oxart.$sCol from $sViewTable as oxart where oxart.oxid = $sViewTable.oxparentid),', ',$sVarSelect)) as _" . $iCnt;
} else {
$sQ  .= $sViewTable . '.' . $sCol . ' as _' . $iCnt;
}
$blSep = true;
}
$aIdentCols = $this->_getIdentColNames();
foreach ( $aIdentCols as $iCnt => $aCol ) {
if ( $blSep )
$sQ .= ', ';
// multilanguage
$sCol = $aCol[3]?$aCol[0].$sLangTag:$aCol[0];
$sQ  .= getViewName( $aCol[1] ) . '.' . $sCol . ' as _' . $iCnt;
}
return " $sQ ";
}
}

If you need other type of assignment you can find examples in “admin/inc/”.

4. Now you need to create AJAX popup “out/admin/tpl/pupups/custpromotions_products.inc.tpl”:

[{include file="popups/headitem.tpl" title="GENERAL_ADMIN_TITLE"|oxmultilangassign}]
 <script type="text/javascript">
initAoc = function()
{
YAHOO.oxid.container1 = new YAHOO.oxid.aoc( 'container1',
[ [{ foreach from=$oxajax.container1 item=aItem key=iKey }]
[{$sSep}][{strip}]{ key:'_[{ $iKey }]', ident: [{if $aItem.4 }]true[{else}]false[{/if}]
[{if !$aItem.4 }],
label: '[{ oxmultilang ident="GENERAL_AJAX_SORT_"|cat:$aItem.0|oxupper }]',
visible: [{if $aItem.2 }]true[{else}]false[{/if}]
[{/if}]}
[{/strip}]
[{assign var="sSep" value=","}]
[{ /foreach }] ],
'[{ $oViewConf->getAjaxLink() }]cmpid=container1&container=custpromotions_products">&synchoxid=[{ $oxid }]'
);
[{assign var="sSep" value=""}]
YAHOO.oxid.container2 = new YAHOO.oxid.aoc( 'container2',
[ [{ foreach from=$oxajax.container2 item=aItem key=iKey }]
[{$sSep}][{strip}]{ key:'_[{ $iKey }]', ident: [{if $aItem.4 }]true[{else}]false[{/if}]
[{if !$aItem.4 }],
label: '[{ oxmultilang ident="GENERAL_AJAX_SORT_"|cat:$aItem.0|oxupper }]',
visible: [{if $aItem.2 }]true[{else}]false[{/if}],
formatter: YAHOO.oxid.aoc.custFormatter
[{/if}]}
[{/strip}]
[{assign var="sSep" value=","}]
[{ /foreach }] ],
'[{ $oViewConf->getAjaxLink() }]cmpid=container2&container=custpromotions_products">&oxid=[{ $oxid }]'
)
YAHOO.oxid.container1.modRequest = function( sRequest )
{
oSelect = $('artcat');
if ( oSelect.selectedIndex ) {
sRequest += '&oxid='+oSelect.options[oSelect.selectedIndex].value+'&synchoxid=[{ $oxid }]';
}
return sRequest;
}
YAHOO.oxid.container1.filterCat = function( e, OObj )
{
YAHOO.oxid.container1.getPage( 0 );
}
YAHOO.oxid.container1.getDropAction = function()
{
// url which is executed when assigning products to promotion
return 'fnc=addProductToPromotion';
}
YAHOO.oxid.container2.getDropAction = function()
{
// url which is executed when unassigning products
return 'fnc=removeProductFromPromotion';
}
$E.addListener( $('artcat'), "change", YAHOO.oxid.container1.filterCat, $('artcat') );
}
$E.onDOMReady( initAoc );
</script>
<table width="100%">
<colgroup>
<col span="2" width="50%" />
</colgroup>
<tr class="edittext">
<!-- standart tooltips -->
<td colspan="2">[{ oxmultilang ident="GENERAL_AJAX_DESCRIPTION" }]<br>[{ oxmultilang ident="GENERAL_FILTERING" }]<br /><br /></td>
</tr>
<tr class="edittext">
<!-- fields description -->
<td align="center"><b>[{ oxmultilang ident="<DISPLAYS NOT ASSIGNED PRODUCTS>" }]</b></td>
<td align="center"><b>[{ oxmultilang ident="<DISPLAYS ASSIGNED PRODUCTS>" }]</b></td>
</tr>
<tr>
<!-- category selection box -->
<td class="oxid-aoc-category">
<select name="artcat" id="artcat">
[{foreach from=$artcattree->aList item=pcat}]
<option value="[{ $pcat->oxcategories__oxid->value }]">[{ $pcat->oxcategories__oxtitle->value }]</option>
[{/foreach}]
</select>
</td>
<td></td>
</tr>
<tr>
<!-- place where AJAX containers are mounted -->
<td valign="top" id="container1"></td>
<td valign="top" id="container2"></td>
</tr>
<tr>
<!-- ASSIGN/REMOVE all records buttons -->
<td class="oxid-aoc-actions">
<input type="button" value="[{ oxmultilang ident="GENERAL_AJAX_ASSIGNALL" }]" id="container1_btn">
</td>
<td class="oxid-aoc-actions">
<input type="button" value="[{ oxmultilang ident="GENERAL_AJAX_UNASSIGNALL" }]" id="container2_btn">
</td>
</tr>
</table>

</body>

</html>

In “out/admin/tpl/popups/” folder you can find other assignment popup examples. As now you created all needed components, you may easily assign preferred object by draging them from left to right side.

Front-end

This example does not affect standard promotions module appearance so no special template changes required. In case you want to customize UI please modify related files:

\out\basic\src\promotions.css         - promotions CSS file; 
\out\basic\tpl\_footer.tpl            - footer, includes JS based promotion UI manager functions;
\out\basic\tpl\_header.tpl            - header, includes ptomotions CSS file; 
\out\basic\tpl\start.tpl              - start template, includes promotions canvas.

Module logic

“custPromotions_oxActions” class extends oxActions. It adds additional functionality such as:

1. getting next promotion (from the sorted list (queue));
2. retrieving stock sum of all related products;
3. functionality to stop current promotion and start next one recursively checking the stock.

<?php
/**
* Stock management support for promotions
*/
class custPromotions_oxActions extends custPromotions_oxActions_parent
{
/**
* return next promotion to start after current one stops
*
* @return oxactions
*/
public function getNextAction()
{
$sId = oxDb::getDb()->getOne(
"select oxid from oxactions where oxactive=1 and oxtype=2 and oxshopid = '" . $this->getShopId() . "' and oxsort>"
.oxDb::getDb()->quote($this->oxactions__oxsort->getRawValue())
." order by oxtitle limit 1"
);
if ($sId) {
$oRet = oxNew('oxactions');
if ($oRet->load($sId)) {
return $oRet;
}
}
return null;
}
 
/**
* return the stock of all assigned articles
*
* @return double
*/
public function getArticlesStock()
{
return oxDb::getDb()->getOne(
"select sum(art.oxstock) from oxobject2action as o2a "
." left join oxarticles as art on art.oxid=o2a.OXOBJECTID"
." where o2a.OXACTIONID="
.oxDb::getDb()->quote($this->getId())
." and o2a.OXCLASS='oxarticle'"
);
}
 
/**
* check all assigned articles for stock and
* stop current promotion if stock is 0 and
* then activate the next promotion
*
* @return null
*/
public function checkStock()
{
if ($this->isRunning()) {
if ($this->getArticlesStock() == 0) {
$this->stop();
if ( ($oNext = $this->getNextAction()) ) {
$oNext->start();
$oNext->checkStock();
}
}
}
}
}

custPromotions_oxActions::getNextAction() implements next action object getter by using oxsort database field.
custPromotions_oxActions::getArticlesStock() retrieves sum of articles stock by using object to promotion (oxobject2action) relation table.
custPromotions_oxActions::checkStock() method implements logic of the business model:

Firstly, this promotion is checked if it is “running” – only active promotions can be stopped and the next one activated. Then the count of this promotions related articles stock is tested. If the articles are sold out, the promotion is stopped by calling the parent method “stop()”. If there is a next promotion available, it is started, and the stock checking is performed on it.

custPromotions_oxArticle module class adds the related promotions getter and the promotion stock checking functionlity invoker after saving a product.

<?php
/**
* oxArticle module:
* on article save method invoke assigned
* promotions stock management
*/
class custPromotions_oxArticle extends custPromotions_oxArticle_parent
{
/**
* return a list of assigned promotions
*
* @return oxList
*/
protected function _getRelatedActions()
{
$oList = oxNew('oxList');
$oList->init('oxactions', 'oxactions');
$oList->selectString(
"select act.* from oxobject2action as o2a "
." left join oxactions as act on act.oxid=o2a.OXACTIONID"
." where o2a.OXOBJECTID="
.oxDb::getDb()->quote($this->getId())
." and o2a.OXCLASS='oxarticle'"
." and act.oxactive=1"
);
return $oList;
}
 
/**
* after this article saving invoke related promotions stock management
*
* @return bool
*/
public function save() {
$ret = parent::save();
if (!$this->oxarticles__oxstock->value) {
foreach($this->_getRelatedActions() as $oAction) {
$oAction->checkStock();
}
}
return $ret;
}
}

custPromotions_oxArticle::_getRelatedActions() method uses same oxobject2action table for retrieving list of promotions, which have this product assigned. After saving a product, all related promotions are notified, and stock checking functionality is invoked in the methodcustPromotions_oxArticle::save(). For better performance, this functionality is invoked only if the current article stock is 0.

Module installation

1. Module files should be copied to related folders:

/modules/custompromotion/admin/custpromotions_actions_main.php
/modules/custompromotion/core/custpromotions_oxactions.php
/modules/custompromotion/core/custpromotions_oxarticle.php
/admin/inc/custpromotions_products.inc.php
/out/admin/tpl/pupups/custpromotions_products.inc.tpl

2. Open “Admin > Core settings > Settings” and append “Modules” field with:

oxactions => custompromotion/core/custpromotions_oxactions
oxarticle => custompromotion/core/custpromotions_oxarticle
actions_main => custompromotion/admin/custpromotions_actions_main

Note: if the base promotion functionality is used as a module, then custpromotions_oxactions should extend not the oxactions class of OXID eShop, but the class from the base module:

oxactions => oxscpromotions/core/oxscpromotions_oxactions&custompromotion/core/custpromotions_oxactions
oxarticle => custompromotion/core/custpromotions_oxarticle
actions_main => custompromotion/admin/custpromotions_actions_main


0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

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