1: <?php
2: /*
3:
4: *
5: * NOTICE OF LICENSE
6: *
7: * This source file is subject to the Open Software License (OSL 3.0)
8: * or OpenGPL v3 license (GNU Public License V3.0)
9: * that is bundled with this package in the file LICENSE.txt.
10: * It is also available through the world-wide-web at this URL:
11: * http://opensource.org/licenses/osl-3.0.php
12: * or
13: * http://www.gnu.org/licenses/gpl-3.0.txt
14: * If you did not receive a copy of the license and are unable to
15: * obtain it through the world-wide-web, please send an email
16: * to info@e-abi.ee so we can send you a copy immediately.
17: *
18: * DISCLAIMER
19: *
20: * Do not edit or add to this file if you wish to upgrade this module to newer
21: * versions in the future.
22: *
23: * @category Eabi
24: * @package Eabi_Dpd
25: * @copyright Copyright (c) 2014 Aktsiamaailm LLC (http://en.e-abi.ee/)
26: * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
27: * @license http://www.gnu.org/licenses/gpl-3.0.txt GNU Public License V3.0
28: * @author Matis Halmann
29: *
30:
31: */
32:
33: /**
34: * <p>Base class for carriers, which ask customer to pick parcel terminal of choice from dropdown list.</p>
35: * <p>Offers following business functions:</p>
36: * <ul>
37: <li>Chosen parcel terminal is forwarded to the Merchant</li>
38: <li>Monitors if shipment parcel data is sent to remote server</li>
39: <li>Offers PDF packing slip printing function, if subclasses implement actual fetch procedure.</li>
40: <li>Offers auto update functionality for parcel terminals if subclasses implement actual parcel terminal fetch procedure</li>
41: <li>Offers cash on delivery functionality with help of extra plugins.</li>
42: * <li>Offers clickable tracking url or iframe functionality from Magento</li>
43: </ul>
44: * @author matishalmann
45: */
46: abstract class Eabi_Postoffice_Model_Carrier_Abstract extends Mage_Shipping_Model_Carrier_Abstract {
47:
48: /**
49: * <p>Actual URL can be entered here, where %s in the url would be replaced with supplied tracking number.</p>
50: * <p>When tracking number is entered, then user is provided with link to track the status of package on remote server.</p>
51: * @var bool|string
52: */
53: protected $_tracking_url = false;
54:
55: /**
56: * <p>When set to true and $_tracking_url is provided, then instead of clickable link iframe is displayed instead.</p>
57: * @var bool
58: */
59: protected $_track_iframe = false;
60:
61: /**
62: *
63: * @var Mage_Sales_Model_Quote
64: */
65: protected $_quote = false;
66:
67:
68:
69:
70: /**
71: * <p>This method should be implemented in subclasses of this carrier.</p>
72: * <p>Purpose of this method is to return array of Postoffices/Parcel terminals associated to this carrier.</p>
73: * <p>This method is called automatically by Magento's cron and the update frequency is regulated by the carriers
74: * configuration data of <code>update_interval</code> which is expressed in minutes.</p>
75: * <p>Usually the carriers are fetched from some remote location and sometimes the fetching may fail, in this case
76: * this method should return boolean false in order to avoid Postoffice list being empty.</p>
77: *
78: * <p>Each element contained in this array should have the following structure:
79: * <pre>
80: * <code>
81: * array(
82: 'place_id' => (int)unique remote id for this office (Mandatory),
83: 'name' => Unique name for this office (Mandatory),
84: 'city' => City where this office is located,
85: 'county' => County where this office is located,
86: 'description' => Description where this office is located,
87: 'country' => Country ID where this office is located format: EN, EE, FI,
88: 'zip' => Zip code for this office,
89: 'group_sort' => higher number places group higher in the parcel terminal list, defaults to 0,
90: );
91:
92: * </code>
93: * </pre>
94: * </p>
95: * <p>You can also supply <code>group_sort</code> parameter, if you have it. Offices which have greater group_sort parameter are displayed before
96: * other postoffices. Alternatively you can write your own group_sort generator function by overwriting the getGroupSort(group_name) method
97: * which should return integer value.</p>
98: * <p>It is advised to supply at least city or county parameter, because first select menu will be created from group_name parameters
99: * and group_name parameter is basically: county_name/city_name</p>
100: *
101: * @return array of postoffices, where each element is assoc array with described structure.
102: *
103: */
104: abstract public function getOfficeList();
105:
106:
107: /**
108: * <p>If you would like to display certain counties or cities before the others, then you can overwrite this function.</p>
109: * <p>This function should return positive integer. The greater the number returned means this county/city is more important than the others
110: * and should be displayed before the others.</p>
111: * <p>In general postoffices will be displayed according to the following rules:
112: * <ul>
113: * <li>group_sort descending</li>
114: * <li>group_name ascending</li>
115: * <li>name ascending</li>
116: * </ul>
117: * </p>
118: * <p>This function is called only when getOfficeList function does not supply group_sort parameter</p>
119: *
120: *
121: * @param string $group_name county/city or county or city
122: * @return int greater value, means this office belongs to more important group.
123: */
124: public function getGroupSort($group_name) {
125: return 0;
126: }
127:
128: /**
129: * <p>This function is called right after:
130: * <ul>
131: * <li>This carrier is active</li>
132: * <li>Cart's max weight does not exceed the limit</li>
133: * <li>Cart's min weight is not below the limit</li>
134: * </ul>
135: * </p>
136: * <p>If this function returns false, then this carrier is not available.</p>
137: *
138: * @param Mage_Shipping_Model_Rate_Request $request
139: * @return boolean false, if this carrier should not be available.
140: */
141: protected function _isAvailable(Mage_Shipping_Model_Rate_Request $request) {
142: return true;
143: }
144:
145: /**
146: * <p>If carrier supports external tracking URL's then it should return true</p>
147: * @return boolean
148: */
149: public function isTrackingAvailable() {
150: if ($this->getTrackingUrl()) {
151: return true;
152: }
153: return false;
154: }
155:
156:
157:
158: /**
159: * <p>Returns tracking URL for current carrier if one exists.</p>
160: * @return bool|string
161: */
162: public function getTrackingUrl() {
163: return $this->_tracking_url;
164: }
165:
166: /**
167: * <p>When set to true and $_tracking_url is provided, then instead of clickable link iframe is displayed instead.</p>
168: * @return bool
169: */
170: public function getTrackingInIframe() {
171: return $this->_track_iframe;
172: }
173:
174: /**
175: * <p>Attempts to display tracking link or tracking iframe if tracking is supported by the carrier and tracking url is available.</p>
176: * @param string $number tracking number
177: * @return Varien_Object|null
178: */
179: public function getTrackingInfo($number) {
180: if ($this->getTrackingUrl()) {
181: $custom = new Varien_Object();
182:
183: $trackingUrl = sprintf($this->getTrackingUrl(), $number);
184: if ($this->getTrackingInIframe()) {
185:
186: $custom->setTracking('<iframe src="' . $trackingUrl . '" style="border:0px #FFFFFF none;" name="abc" scrolling="no" frameborder="0" marginheight="0px" marginwidth="0px" height="100%" width="100%"></iframe>');
187: } else {
188: $custom->setTracking(sprintf('<a href="%s" target="_blank">%s</a>', $trackingUrl, $this->_getEabi()->__('Tracking URL')));
189: }
190:
191: return $custom;
192: }
193: return null;
194: }
195:
196: /**
197: * Returns true if this Mage_Shipping_Model_Rate_Request is multishipping.
198: *
199: *
200: * @param Mage_Shipping_Model_Rate_Request $request
201: * @return boolean
202: */
203: protected function _isMultishipping(Mage_Shipping_Model_Rate_Request $request) {
204: foreach ($request->getAllItems() as $item) {
205: if ($item instanceof Mage_Sales_Model_Quote_Address_Item) {
206: return true;
207: }
208: break;
209: }
210: return false;
211: }
212:
213:
214: /**
215: * <p>use <code>_isAvailable()</code> function to determine if the carrier is available.</p>
216: * <p>use <code>_calculateAdditionalShippingPrice</code> function to calculate any additional shipping cost.</p>
217: *
218: * <p>Checks for the following conditions:
219: * <ul>
220: * <li>This carrier is active</li>
221: * <li>Cart's max weight does not exceed the limit</li>
222: * <li>Cart's min weight is not below the limit</li>
223: * </ul>
224: * </p>
225: * <p>Calculates the shipping price based on the following:
226: * <ul>
227: * <li>Configurations like <code>enable_free_shipping</code>,<code>free_shipping_from</code> are enabled or disabled </li>
228: * <li>Configuration like <code>handling_fee</code> which is the base shipping price.</li>
229: * <li>Configuration like <code>handling_action</code> which determines whether the shipping price is based on per order or per item</li>
230: * <li>If shopping cart rules specify free shipping.</li>
231: * <li>Price found from the conditions above will be passed as second parameter to the function <code>_calculateAdditionalShippingPrice</code> which in turn can alter the shipping price further.</li>
232: * </ul>
233: * </p>
234: *
235: *
236: * @param Mage_Shipping_Model_Rate_Request $request
237: * @return boolean|Eabi_Postoffice_Model_Carrier_Result false, if carrier is not available.
238: */
239: final public function collectRates(Mage_Shipping_Model_Rate_Request $request) {
240: //determine if this shipping method is available
241: $addressId = (string)$this->getAddressId($request);
242:
243:
244: if (!$this->getConfigData('active')) {
245: $this->clearAddressId($addressId);
246: return false;
247: }
248: if ($this->getConfigData('use_per_item_weight')) {
249: //we compare each item individually
250: //if any of the items is overweight, disable
251: //if any of the items is underweight, disable
252: $loadedProductWeights = array();
253: if ($this->getConfigData('max_package_weight') > 0 || $this->getConfigData('min_package_weight') > 0) {
254: //get request items
255: if ($request->getAllItems()) {
256: foreach ($request->getAllItems() as $requestItem) {
257: $productToWeight = $this->_getProductModel()->load($requestItem->getProduct()->getId());
258: for ($i = 0; $i < $requestItem->getQty(); $i++) {
259: $loadedProductWeights[] = $productToWeight->getWeight();
260: }
261: }
262: }
263: }
264: if ($this->getConfigData('max_package_weight') > 0) {
265: if (max($loadedProductWeights) > (float) $this->getConfigData('max_package_weight')) {
266: $this->clearAddressId($addressId);
267: return false;
268: }
269: }
270:
271: if ($this->getConfigData('min_package_weight') > 0) {
272: if (min($loadedProductWeights) < (float) $this->getConfigData('min_package_weight')) {
273: $this->clearAddressId($addressId);
274: return false;
275: }
276: }
277: $request->setEabiProductWeights($loadedProductWeights);
278:
279: } else {
280: //we summarize weight of all cart and apply the weight rule
281: //total cart is overweight, disable
282: //total cart is underweight, disable
283: if ($this->getConfigData('max_package_weight') > 0) {
284: if ($request->getPackageWeight() > (float) $this->getConfigData('max_package_weight')) {
285: $this->clearAddressId($addressId);
286: return false;
287: }
288: }
289: if ($this->getConfigData('min_package_weight') > 0) {
290: if ($request->getPackageWeight() < (float) $this->getConfigData('min_package_weight')) {
291: $this->clearAddressId($addressId);
292: return false;
293: }
294: }
295: }
296:
297: $isAvailable = $this->_isAvailable($request);
298: if ($isAvailable === false) {
299: $this->clearAddressId($addressId);
300: return false;
301: }
302:
303: //determine the shipping price
304: $price = 0;
305:
306: //fetch the base handling fee
307: $handlingFee = (float)str_replace(',', '.', $this->getConfigData('handling_fee'));
308: //shipping method is available. Find the price.
309: $isFree = false;
310:
311: //if we have defined free shipping from certain cart subtotal level
312: if ($this->getConfigData('enable_free_shipping') && $request->getPackageValueWithDiscount() >= $this->getConfigData('free_shipping_from')) {
313: $price = 0;
314: $isFree = true;
315: }
316:
317: //if there is free shipping defined by shopping cart rule
318: if ($request->getFreeShipping() === true && !$isFree) {
319: $isFree = true;
320: }
321:
322: if (!$isFree) {
323: $freeBoxes = 0;
324: //if there are any free boxes defined by shopping cart rule
325: foreach ($request->getAllItems() as $item) {
326: if ($item->getFreeShipping() && !$item->getProduct()->isVirtual()) {
327: $freeBoxes += $item->getQty();
328: }
329: }
330: $this->setFreeBoxes($freeBoxes);
331: }
332:
333:
334: //if there is no free shipping, attempt to calcualte the price
335: //on per order or per item basis
336: if ($this->getConfigData('handling_action') == 'O' && !$isFree) {
337: //handling action per_order basis
338: $price = $handlingFee;
339: } else if ($this->getConfigData('handling_action') == 'P' && !$isFree) {
340: //handling action per item basis
341: if ($this->getConfigData('use_per_item_weight')) {
342: //calculate number of packages first
343: $price = ($this->_getOfficeHelper()->getNumberOfPackagesFromItemWeights($loadedProductWeights, $this->getConfigData('max_package_weight')) - $this->getFreeBoxes()) * $handlingFee;
344:
345: } else {
346: $price = ($request->getPackageQty() * $handlingFee) - ($this->getFreeBoxes() * $handlingFee);
347: }
348: if ($price == 0) {
349: $isFree = true;
350: }
351: } else if (!$isFree) {
352: //handling action type could not be detected, return false
353: //shipping method not available
354: $this->clearAddressId($addressId);
355:
356: return false;
357: }
358:
359: //if there is any additional logic for calculating the price
360: if (!$isFree) {
361: $price = $this->_calculateAdditionalShippingPrice($request, $price);
362: }
363: if ($price === false) {
364: return false;
365: }
366: Mage::dispatchEvent('eabi_postoffice_collect_rates_terminals_load_before', array(
367: 'request' => $request,
368: 'shipment_method' => $this,
369: ));
370:
371: if ($request->getCancelRateCollection()) {
372: return false;
373: }
374: $session = Mage::getSingleton('core/session');
375:
376: $this->registerAddressId($addressId);
377:
378:
379:
380: $result = Mage::getModel('eabi_postoffice/carrier_result');
381: //list the offices
382: $method = Mage::getModel('shipping/rate_result_method');
383: $method->setCarrier($this->_code);
384: $method->setCarrierTitle($this->getConfigData('title'));
385:
386: $method->setMethod($this->_code);
387:
388: //if COD is enabled, then add COD fee on top of the shipping price
389: $price += $this->_applyPriceFromCod($request);
390:
391: $method->setPrice($price);
392: $loadingText = Mage::helper('eabi_postoffice')->__('Loading offices...');
393: $html = '';
394: if ($this->_isMultishipping($request)) {
395: $html .= '<div id="eabi_carrier_'.$addressId.'_'.$this->_code.'" class="eabi_carrier eabi_carrier_' . $this->_code. '" style="display:inline-block;">'.$loadingText.'</div>';
396: } else {
397: $html .= '<div id="eabi_carrier_'.$this->_code.'" style="display:inline-block;" class="eabi_carrier eabi_carrier_' . $this->_code. '"></div>';
398: }
399: $url = Mage::getUrl('eabi_postoffice/index/office', array('_secure' => true));
400:
401: if (Mage::app()->getStore()->isAdmin()) {
402: $url = Mage::helper('adminhtml')->getUrl('eabi_postoffice/adminhtml_postoffice/office', array('store_id' => $request->getStoreId(), '_secure' => true));
403: }
404:
405: $carrierId = 's_method_'.$this->_code.'_'.$this->_code;
406: $divId = 'eabi_carrier_'.$this->_code;
407: if ($this->_isMultishipping($request)) {
408: $carrierId = 's_method_'.$addressId.'_'.$this->_code.'_'.$this->_code;
409: $divId = 'eabi_carrier_'.$addressId.'_'.$this->_code;
410: }
411: $carrierCode = $this->_code;
412:
413: $html .= <<<EOT
414: <script type="text/javascript">
415: /* <![CDATA[ */
416: $('{$carrierId}').writeAttribute('value', '');
417: new Ajax.Request('{$url}', {
418: method: 'post',
419: parameters: {
420: carrier_id: '{$carrierId}',
421: carrier_code: '{$carrierCode}',
422: div_id: '{$divId}',
423: address_id: '{$addressId}',
424: },
425: onSuccess: function(transport) {
426: $('{$divId}').update(transport.responseJSON.html);
427: }
428: });
429: /* ]]> */
430: </script>
431: EOT;
432: $method->setMethodTitle($html);
433: $terminalsCollection = $this->getTerminals();
434: if ($terminalsCollection->count() == 1) {
435: //if we have only one terminal in the collection, then do not create select drop downs.
436: //just render the single element and return the result.
437: $office = $terminalsCollection->getFirstItem();
438: $method->setMethodTitle($this->getTerminalTitle($office));
439: $method->setCarrier($this->_code);
440: $method->setCarrierTitle($this->getConfigData('title'));
441:
442: $method->setMethod($this->_code.'_'.$office->getRemotePlaceId());
443: $result->append($method);
444: return $result;
445: }
446:
447:
448: $result->append($method);
449:
450:
451: $addedMethods = array();
452: //try to see, if we have any extra methods stored in a session
453: $sessionData = $session->getData('eabi_carrier_' . $this->_code);
454: $shippingAddress = $this->_getAddressModel()->load($addressId);
455: if (is_array($sessionData) && isset($sessionData[$addressId])) {
456: foreach ($sessionData[$addressId] as $subCarrier) {
457:
458: if (isset($subCarrier['country_id']) && $subCarrier['country_id'] != $shippingAddress->getCountryId()) {
459: //on country mismatch do not append the method
460:
461:
462: continue;
463: }
464: $method = Mage::getModel('shipping/rate_result_method');
465: $method->setCarrier($subCarrier['carrier']);
466: $method->setCarrierTitle($subCarrier['carrier_title']);
467: $addedMethods[$subCarrier['method']] = $subCarrier['method'];
468:
469: $method->setMethod($subCarrier['method']);
470: $method->setMethodTitle($subCarrier['method_title']);
471: $method->setPrice($price);
472: $result->append($method);
473: }
474: }
475:
476: //TODO: add the method if we are able to detect the postoffice automatically
477: $additionalOffices = $this->getOfficesFromAddress($request);
478: foreach ($additionalOffices as $office) {
479: $methodName = $this->_code .'_' .$office->getRemotePlaceId();
480: if (!isset($addedMethods[$methodName])) {
481: $method = Mage::getModel('shipping/rate_result_method');
482: $method->setCarrier($this->_code);
483: $method->setCarrierTitle($this->getConfigData('title'));
484: $addedMethods[$methodName] = $method;
485:
486: $method->setMethod($methodName);
487: $method->setMethodTitle($this->getAdminTerminalTitle($office));
488: $method->setPrice($price);
489: $result->append($method);
490:
491: }
492: }
493:
494:
495: return $result;
496:
497: }
498:
499: /**
500: * <p>Returns true if cash on delivery is specified for target address</p>
501: * @param Mage_Customer_Model_Address $address
502: * @return bool
503: */
504: public function isCodEnabled($address) {
505: return $this->getConfigData('enable_cod');
506: }
507:
508:
509: /**
510: * <p>Applies extra price from Cash on Delivery, which is added on top of shipping price</p>
511: * @param Mage_Shipping_Model_Rate_Request $request
512: * @return double|int
513: */
514: protected function _applyPriceFromCod(Mage_Shipping_Model_Rate_Request $request) {
515: if ($this->_getQuote() && !$this->_getQuote()->isVirtual() && $this->getConfigData('enable_cod') && $this->_getQuote()->getPayment() && $this->_getQuote()->getPayment()->getMethod() && $this->_getQuote()->getPayment()->getMethod() == 'eabicodpayment' && strpos($this->_getQuote()->getShippingAddress()->getShippingMethod(), $this->getCarrierCode()) === 0) {
516:
517: return $this->getConfigData('cod_fee');
518: }
519: return 0;
520: }
521:
522: /**
523: * <p>Registers current address id in session, so no outsiders could tamper with selected parcel terminal data.</p>
524: * @param string $addressId
525: * @return Eabi_Postoffice_Model_Carrier_Abstract
526: */
527: private function registerAddressId($addressId) {
528: $addressId = (string)$addressId;
529: $session = Mage::getSingleton('core/session');
530: $sessionData = $session->getData('eabi_allowedcarriers');
531: if (!is_array($sessionData)) {
532: $sessionData = array();
533: }
534: if (!isset($sessionData[$addressId])) {
535: $sessionData[$addressId] = array();
536: }
537: if (!isset($sessionData[$addressId][$this->_code])) {
538: $sessionData[$addressId][$this->_code] = $this->_code;
539: }
540: $session->setData('eabi_allowedcarriers', $sessionData);
541: return $this;
542: }
543:
544: /**
545: * Security related, used to prevent customer to enter arbitrary data as their shipping carrier.
546: *
547: *
548: * @param int $addressId
549: * @return boolean
550: */
551: final public function isAjaxInsertAllowed($addressId) {
552: $addressId = (string)$addressId;
553: $session = Mage::getSingleton('core/session');
554: $sessionData = $session->getData('eabi_allowedcarriers');
555: if (is_array($sessionData) && isset($sessionData[$addressId]) && isset($sessionData[$addressId][$this->_code])) {
556: return true;
557: }
558: return false;
559: }
560:
561: /**
562: * <p>After successful checkout all entered parcel terminal data should be cleared by calling this function.</p>
563: * @param string $addressId
564: * @return Eabi_Postoffice_Model_Carrier_Abstract
565: */
566: private function clearAddressId($addressId) {
567: $addressId = (string)$addressId;
568: $session = Mage::getSingleton('core/session');
569: $sessionData = $session->getData('eabi_allowedcarriers');
570: if (is_array($sessionData) && isset($sessionData[$addressId]) && isset($sessionData[$addressId][$this->_code])) {
571: unset($sessionData[$addressId][$this->_code]);
572: }
573: return $this;
574: }
575:
576: /**
577: * <p>This function is called right after the initial price calculation by the </code>collectRates</code> has been called.</p>
578: * <p>Overwrite this function if you want to apply more complicated price calculation rules, than </code>collectRates</code>
579: * function offers.</p>
580: *
581: *
582: *
583: * @param Mage_Shipping_Model_Rate_Request $request
584: * @param float $price pre-calculated price by the </code>collectRates</code> function.
585: * @return float new price, this carrier should have.
586: */
587: protected function _calculateAdditionalShippingPrice(Mage_Shipping_Model_Rate_Request $request, $price) {
588: return $price;
589: }
590:
591: /**
592: * <p>Overwrite this function in your subclass if you would like to offer automatic postoffice selection based on the user entered address.</p>
593: *
594: * ["dest_country_id"] => string(2) "EE"
595: * ["dest_region_id"] => string(3) "345"
596: * ["dest_region_code"] => string(5) "EE-57"
597: * ["dest_street"] => string(11) "mina ei tea"
598: * ["dest_city"] => string(7) "tallinn"
599: * ["dest_postcode"] => string(5) "12345"
600: * ["package_value"] => float(16.33)
601: * ["package_value_with_discount"] => float(16.33)
602: * ["package_weight"] => float(16.16)
603: * ["package_qty"] => int(2)
604: * ["package_physical_value"] => float(16.33)
605: * ["free_method_weight"] => float(16.16)
606: * ["store_id"] => string(1) "2"
607: * ["website_id"] => string(1) "1"
608: * ["free_shipping"] => int(0)
609: * ["base_currency (Mage_Directory_Model_Currency)"] => array(1) { ["currency_code"] => string(3) "EUR" }
610: * ["package_currency (Mage_Directory_Model_Currency)"] => array(1) { ["currency_code"] => string(3) "EUR" }
611: * ["country_id"] => string(2) "EE"
612: * ["region_id"] => string(1) "0"
613: * ["city"] => string(0) ""
614: * ["postcode"] => string(0) ""
615: *
616: * @param Mage_Shipping_Model_Rate_Request $request
617: * @return array of 'eabi_postoffice/office' models belonging to this carrier.
618: */
619: protected function getOfficesFromAddress(Mage_Shipping_Model_Rate_Request $request) {
620: return array();
621: }
622:
623: /**
624: * <p>Returns distinct group_name,group_id,group_sort as Eabi_Postoffice_Model_Mysql4_Office_Collection of 'eabi_postoffice/office' models</p>
625: * <p>Result of this function is used to render the first select menu (county/city) for this carrier.</p>
626: * <p>If no groups can be found, then this function returns boolean false.</p>
627: *
628: *
629: * @param int $addressId when supplied then only groups from the addressId country are returned.
630: * @return boolean|array
631: */
632: public function getGroups($addressId = null) {
633: if( $this->getConfigData('drop_menu_selection')){
634: return false;
635: }
636: $groups = Mage::getModel('eabi_postoffice/office')->getCollection()
637: ->distinct(true)
638: ->addFieldToFilter('remote_module_name', $this->_code);
639: $groups->getSelect()->reset(Zend_Db_Select::COLUMNS)
640: ->columns(array('group_id', 'group_name', 'group_sort'));
641: $groups->addOrder('group_sort', 'DESC');
642: $groups->addOrder('group_name', 'ASC');
643: if ($addressId) {
644: $address = $this->_getAddressModel()->load($addressId);
645: if ($address->getCountryId()) {
646: $groups->addFieldToFilter('country', $address->getCountryId());
647: }
648: }
649:
650:
651: if ($groups->count() <= 1) {
652: return false;
653: }
654: return $groups;
655: }
656:
657: /**
658: *
659: * <p>Returns Eabi_Postoffice_Model_Mysql4_Office_Collection which should contain the actual postoffices
660: * which belong to the selected group_id in alplabetically sorted order.</p>
661: * <p>If no $groupId is supplied, then all the postoffices are returned.</p>
662: * <p>Offices are sorted by</p>
663: * <ul>
664: <li>group_sort descending</li>
665: <li>group_name ascending</li>
666: <li>name ascending</li>
667: </ul>
668: * @param int $groupId
669: * @param int $addressId when supplied then only offices from the addressId country are returned.
670: * @return Eabi_Postoffice_Model_Mysql4_Office_Collection
671: * @see Eabi_Postoffice_Model_Carrier_Abstract::getGroupSort()
672: */
673: public function getTerminals($groupId = null, $addressId = null) {
674: $terminals = Mage::getModel('eabi_postoffice/office')->getCollection()
675: ->addFieldToFilter('remote_module_name', $this->_code)
676: ->addOrder('group_sort', 'DESC')
677: ->addOrder('group_name', 'ASC')
678: ->addOrder('name', 'ASC')
679: ;
680:
681: if ($addressId) {
682: $address = $this->_getAddressModel()->load($addressId);
683: if ($address->getCountryId()) {
684: $terminals->addFieldToFilter('country', $address->getCountryId());
685: }
686: }
687:
688: if ($groupId !== null) {
689: $terminals->addFieldToFilter('group_id', $groupId);
690: }
691: return $terminals;
692: }
693:
694:
695: /**
696: * <p>Returns the selected postoffice by it's place id.<p>
697: * <p>If no postoffice found, then returns boolean false.</p>
698: *
699: * @param int $placeId
700: * @return Eabi_Postoffice_Model_Office|boolean false
701: */
702: public function getTerminal($placeId) {
703: $places = Mage::getModel('eabi_postoffice/office')->getCollection()
704: ->addFieldToFilter('remote_module_name', $this->_code)
705: ->addFieldToFilter('remote_place_id', $placeId);
706: if ($places->count() == 1) {
707: return $places->getFirstItem();
708: }
709: return false;
710: }
711:
712: /**
713: * Determines how name of each postoffice in the second select menu should be rendered.
714: *
715: *
716: * @param Eabi_Postoffice_Model_Office $office
717: * @return string
718: */
719: public function getTerminalTitle(Eabi_Postoffice_Model_Office $office) {
720: return htmlspecialchars($office->getName());
721: }
722: /**
723: * Determines how name of each county/city in the first select menu should be rendered.
724: *
725: *
726: * @param Eabi_Postoffice_Model_Office $office
727: * @return string
728: */
729: public function getGroupTitle(Eabi_Postoffice_Model_Office $office) {
730: return htmlspecialchars($office->getGroupName());
731: }
732:
733:
734: /**
735: * <p>Should return extra comment for the selected postoffice. For example shipping time.</p>
736: * <p>Extra comment is displayed next to select menu just before price display</p>
737: * <p>Returning boolean false causes comment to be hidden</p>
738: * @param Eabi_Postoffice_Model_Office $office
739: * @return boolean|string
740: */
741: public final function getTerminalComment(Eabi_Postoffice_Model_Office $office) {
742: $terminalCommentResult = new Varien_Object(array(
743: 'comment' => false,
744: 'is_error' => false,
745: ));
746: //get descriptional message (not to do it now)
747: //get future shipping date -> supply start date as parameter
748: //get disabled with message
749:
750: $shippingTimestamp = $this->_getDeliveryTime($office);
751: if ($shippingTimestamp !== false && $this->getConfigData('show_delivery_time')) {
752: $date = date('d.m.Y', $shippingTimestamp);
753: $time = date('H:i', $shippingTimestamp);
754: $dateDiff = (int)(($this->__removeTimePart($shippingTimestamp) - $this->__removeTimePart(time())) / 86400);
755: switch ($dateDiff) {
756: case 0:
757: $date = $this->_getOfficeHelper()->__('today');
758: break;
759: case 1:
760: $date = $this->_getOfficeHelper()->__('tomorrow');
761: break;
762: case 2:
763: $date = $this->_getOfficeHelper()->__('day after tomorrow');
764: break;
765: }
766: if ($time == '00:00') {
767: $terminalCommentResult->setComment($this->_getOfficeHelper()->__('Delivery on %s', $date));
768: } else {
769: $terminalCommentResult->setComment($this->_getOfficeHelper()->__('Delivery on %1$s by %2$s', $date, $time));
770: }
771: }
772:
773: //if we have an error
774: $disabledComment = $this->_getDisabledComment($office);
775:
776: if ($disabledComment) {
777: $terminalCommentResult->setIsError(true);
778: $terminalCommentResult->setComment($disabledComment);
779:
780: //remove selected shipping method (todo at some later date)
781:
782: }
783:
784: Mage::dispatchEvent('eabi_' . $this->_code . '_get_terminal_comment', array(
785: 'selected_office' => $office,
786: 'shipment_method' => $this,
787: 'terminal_comment_result' => $terminalCommentResult,
788: ));
789:
790: return $terminalCommentResult;
791: }
792:
793: /**
794: * <p>Returns calculated delivery time for the specified office</p>
795: * <p>If no delivery time calculation is available, then boolean FALSE is returned</p>
796: * @param Eabi_Postoffice_Model_Office $office
797: * @return boolean|int
798: */
799: protected function _getDeliveryTime(Eabi_Postoffice_Model_Office $office) {
800: return false;
801: }
802:
803: /**
804: * <p>Should return string text, when specified pickup-point is disabled.</p>
805: * <p>If specified pickup-point should not be disabled then this method returns boolean FALSE</p>
806: * @param Eabi_Postoffice_Model_Office $office
807: * @return boolean|string
808: */
809: protected function _getDisabledComment(Eabi_Postoffice_Model_Office $office) {
810: return false;
811: }
812:
813:
814:
815:
816:
817: protected function _getStartingDayStamp($timestamp) {
818: $limitTime = $this->getConfigData('same_day_time_limit');
819: if (!$limitTime) {
820: return null;
821: }
822: $resultTimestamp = $timestamp;
823:
824: $currentTimeParts = explode(',', date('H,i,s', $timestamp));
825: $limitTimeParts = explode(',', $limitTime);
826: $weekDayToLook = (int)date('N', $timestamp);
827: //1 = monday
828: //7 = sunday
829:
830:
831: if ($this->_getSeconds($currentTimeParts) > $this->_getSeconds($limitTimeParts)) {
832: //first day is tomorrow
833: $weekDayToLook++;
834: $resultTimestamp += (int)(24 * 3600);
835: } else {
836: //first day is today
837: }
838:
839: if ($weekDayToLook > 7) {
840: $weekDayToLook -= 7;
841: }
842:
843:
844: return $this->__removeTimePart($resultTimestamp);
845:
846: }
847:
848:
849: /**
850: * <p>Removes hours, minutes, seconds from specified timestamp and returns the result</p>
851: * @param int $timestamp
852: * @return int
853: */
854: private function __removeTimePart($timestamp) {
855: $dateParts = explode(',', date('d,m,Y', $timestamp));
856: return mktime(0, 0, 0, $dateParts[1], $dateParts[0], $dateParts[2]);
857: }
858:
859:
860: /**
861: * Determines how name of customer selected postoffice should be rendered and this is also how merchant sees the selected postoffice.
862: *
863: *
864: * @param Eabi_Postoffice_Model_Office $office
865: * @return string
866: */
867: public function getAdminTerminalTitle(Eabi_Postoffice_Model_Office $office) {
868: $name = $office->getName();
869: if ($office->getGroupName() != '') {
870: $name = $office->getGroupName().' - '.$office->getName();
871: }
872: return htmlspecialchars($name);
873: }
874:
875:
876: /**
877: *
878: * Used to fetch the price from 'sales/quote_address_rate' database table.
879: *
880: * @param type $addressId
881: * @return boolean
882: */
883: final protected function getPriceFromAddressId($addressId) {
884: $calculatedPriceModelCollection = Mage::getModel('sales/quote_address_rate')
885: ->getCollection()
886: ->addFieldToFilter('address_id', $addressId)
887: ->addFieldToFilter('carrier', $this->_code)
888: ->addFieldToFilter('code', $this->_code . '_' . $this->_code)
889: ;
890: if ($calculatedPriceModelCollection->count() == 1) {
891: return $calculatedPriceModelCollection->getFirstItem()->getPrice();
892: }
893: return false;
894: }
895:
896: /**
897: * <p>Initially this carrier does not set up all the shipping methods for this carrier in the 'sales/quote_address_rate' database table.
898: * since storing too many methods in there is not the smartest thing to do</p>
899: * <p>So once the user selects the actual office, an AJAX callback is performed and this one inserts the selected office to the database
900: * and also to the session, in order the customer would easily reach latest selected offices and the order itself could be placed,
901: * since user selected postoffices have to exist in the 'sales/quote_address_rate' database table.</p>
902: * <p>This one results in less entries in 'sales/quote_address_rate' database table.</p>
903: *
904: *
905: * @param int $addressId
906: * @param Eabi_Postoffice_Model_Office $office
907: * @throws Exception
908: */
909: final public function setOfficeToSession($addressId, Eabi_Postoffice_Model_Office $office) {
910:
911: $addressRateModelCollection = Mage::getModel('sales/quote_address_rate')
912: ->getCollection()
913: ->addFieldToFilter('address_id', $addressId)
914: ->addFieldToFilter('carrier', $this->_code)
915: ->addFieldToFilter('code', $this->_code . '_' . $this->_code . '_' . $office->getRemotePlaceId())
916: ;
917: if ($addressRateModelCollection->count() == 0) {
918: //insert
919: $newRate = Mage::getModel('sales/quote_address_rate');
920: $newRate->setData('address_id', $addressId);
921: $newRate->setData('carrier', $this->_code);
922: $newRate->setData('country_id', $office->getCountry());
923: $newRate->setData('code', $this->_code . '_' . $this->_code . '_' . $office->getRemotePlaceId());
924: $newRate->setData('method', $this->_code . '_' . $office->getRemotePlaceId());
925:
926:
927: $title = $this->getAdminTerminalTitle($office);
928:
929: $newRate->setData('carrier_title', $this->getConfigData('title'));
930: $newRate->setData('method_title', $title);
931: $price = $this->getPriceFromAddressId($addressId);
932: if ($price === false) {
933: throw new Exception('Cannot calculate price');
934: }
935: $newRate->setPrice($price);
936: $newRate->save();
937: $session = Mage::getSingleton('core/session');
938: $sessionData = $session->getData('eabi_carrier_' . $this->_code);
939: if (!is_array($sessionData)) {
940: $sessionData = array();
941: }
942: if (!isset($sessionData[(string)$addressId])) {
943: $sessionData[(string)$addressId] = array();
944: }
945: if ($this->getConfigData('disable_session')) {
946: foreach ($sessionData[(string)$addressId] as $k => $v) {
947: if ($k != $this->_code) {
948: unset($sessionData[(string)$addressId][$k]);
949: }
950: }
951:
952: }
953: $sessionData[(string)$addressId][$this->_code . '_' . $this->_code . '_' . $office->getRemotePlaceId()] = $newRate->debug();
954: $session->setData('eabi_carrier_' . $this->_code, $sessionData);
955: }
956: }
957:
958: /**
959: * Clears all the session data created by this carrier.
960: *
961: *
962: * @return \Eabi_Postoffice_Model_Carrier_Abstract
963: */
964: final public function clearSession() {
965: Mage::getSingleton('core/session')->unsetData('eabi_allowedcarriers');
966: Mage::getSingleton('core/session')->unsetData('eabi_carrier_' . $this->_code);
967: return $this;
968: }
969:
970:
971:
972: /**
973: * <p>Sets Magento store config for current carrier specified by key</p>
974: * @param string $key
975: * @param mixed $value
976: * @return Eabi_Postoffice_Model_Carrier_Abstract|boolean
977: */
978: public function setConfigData($key, $value) {
979: if (empty($this->_code)) {
980: return false;
981: }
982: Mage::helper('eabi')->setConfigData('carriers/'.$this->_code.'/'.$key, $value);
983: return $this;
984: }
985:
986: /**
987: * Should return true, when this carrier can automatically send shipment data to third party carrier server.
988: *
989: *
990: * @return bool
991: */
992: public function isAutoSendAvailable() {
993: return (bool)$this->getConfigData('senddata_enable');
994: }
995:
996: /**
997: * <p>Should return the actual generated barcode by the third party carrier server.
998: * Overwrite this method in the subclass.</p>
999: * <p>This function is available to the merchant, if he/she wants to print the packing slip in the order view.</p>
1000: *
1001: *
1002: *
1003: * @param Mage_Sales_Model_Order $order order, to get the barcode for.
1004: * @return boolean|string
1005: */
1006: public function getBarcode(Mage_Sales_Model_Order $order) {
1007: return false;
1008: }
1009:
1010: /**
1011: * <p>Should return the actual Pdf binary which should be packing slip for this order and false in every other case.</p>
1012: * <p>This function is available to the merchant, if he/she wants to print the packing slip in the order view.</p>
1013: *
1014: *
1015: * @param Mage_Sales_Model_Order $order
1016: * @return boolean
1017: */
1018: public function getBarcodePdf(Mage_Sales_Model_Order $order) {
1019: return false;
1020: }
1021:
1022: /**
1023: * <p>Returns true, if data has been sent</p>
1024: * <p>Returns false, if data has not been sent</p>
1025: * <p>Returns null, if data sending is not available.</p>
1026: *
1027: * @param Mage_Sales_Model_Order $order
1028: * @return boolean
1029: */
1030: public function isDataSent(Mage_Sales_Model_Order $order) {
1031: if ($this->isAutoSendAvailable()) {
1032: return false;
1033: }
1034: return null;
1035: }
1036:
1037: /**
1038: *
1039: * If the barcode function is available at all for this carrier.
1040: * @return boolean
1041: */
1042: public function isBarcodeFunctionAvailable() {
1043: return false;
1044: }
1045:
1046:
1047:
1048: /**
1049: * <p> If automatic data sending is available, then this function should be overwritten and the actual data sending should be performed in here.</p>
1050: * <p>Also Configuration value of <code>senddata_enable</code> should be set to '1' or this function will never be called.</p>
1051: * <p>If automatic sending is not applicable, then this function should return boolean false.</p>
1052: * <p>If automatic data sending is successful, then the result will be added to the order comments using <code>print_r()</code> function.</p>
1053: *
1054: *
1055: *
1056: * @param Mage_Sales_Model_Order $order
1057: * @param type $selectedOfficeId remote_place_id - id of the carrier, that the customer selected.
1058: * @return boolean|array
1059: */
1060: public function autoSendData(Mage_Sales_Model_Order $order, $selectedOfficeId) {
1061: return false;
1062: }
1063:
1064: /**
1065: * Wrapper function for the parent class, simply for security and session management.
1066: * For example, when user first selects country, where the carrier is available and then switches country and the carrier is not available any more,
1067: * then the session has to be cleared a bit, in order to avoid the user entering arbitrary data for the previously available carrier.
1068: *
1069: * @param Mage_Shipping_Model_Rate_Request $request
1070: * @return \Mage_Shipping_Model_Rate_Result_Error
1071: */
1072: public function checkAvailableShipCountries(Mage_Shipping_Model_Rate_Request $request) {
1073: $checkResult = parent::checkAvailableShipCountries($request);
1074: if ($checkResult === false || ($checkResult instanceof Mage_Shipping_Model_Rate_Result_Error)) {
1075: $this->clearAddressId($this->getAddressId($request));
1076: }
1077: return $checkResult;
1078: }
1079:
1080: /**
1081: * <p>Indicates if specified order has been picked up by courier.</p>
1082: * <p>Should return the following</p>
1083: * <ul>
1084: <li><b>true</b> - If the order has been picked up by courier</li>
1085: <li><b>false</b> - If the order has not been picked up by courier</li>
1086: <li><b>null</b> - If courier pickup is not applicable to specified order</li>
1087: </ul>
1088: * @param Mage_Sales_Model_Order $order
1089: * @return null|bool
1090: */
1091: public function isPickedUpByCourier(Mage_Sales_Model_Order $order) {
1092: return null;
1093: }
1094:
1095: /**
1096: * Gets the address_id, which will be used in the 'sales/quote_address_rate' database table, in order to handle user selection of postoffice
1097: * related to this carrier.
1098: *
1099: *
1100: * @param Mage_Shipping_Model_Rate_Request $request
1101: * @return type
1102: */
1103: protected function getAddressId(Mage_Shipping_Model_Rate_Request $request) {
1104: if (Mage::app()->getStore()->isAdmin()) {
1105: return Mage::getSingleton('adminhtml/sales_order_create')->getQuote()->getShippingAddress()->getId();
1106: } else if ($this->_isMultishipping($request)) {
1107: $shippingAddresses = Mage::getSingleton('checkout/type_multishipping')->getQuote()->getAllShippingAddresses();
1108: foreach ($shippingAddresses as $shippingAddress) {
1109: if ($this->_compareAddressToRequest($request, $shippingAddress)) {
1110: if ($shippingAddress->getId() <= 0) {
1111: $shippingAddress->save();
1112: }
1113: return $shippingAddress->getId();
1114: }
1115: }
1116: } else {
1117: return Mage::getSingleton('checkout/session')->getQuote()->getShippingAddress()->getId();
1118: }
1119:
1120: }
1121:
1122: /**
1123: * <p>In multishipping scenarios shipping addresses may not be saved and thus lacking correct id. In such scenario they need to be compared manually to detect the correct chosen parcel terminal</p>
1124: * @param Mage_Shipping_Model_Rate_Request $request
1125: * @param Mage_Sales_Model_Quote_Address $address
1126: * @return boolean
1127: */
1128: protected function _compareAddressToRequest(Mage_Shipping_Model_Rate_Request $request, $address) {
1129: $compareFields = array(
1130: 'dest_country_id' => 'country_id',
1131: 'dest_region_id' => 'region_id',
1132: 'dest_street' => 'street',
1133: 'dest_city' => 'city',
1134: 'dest_postcode' => 'postcode',
1135: );
1136: foreach ($compareFields as $requestField => $addressField) {
1137: if ($request->getData($requestField) != $address->getData($addressField)) {
1138: return false;
1139: }
1140: }
1141: return true;
1142: }
1143:
1144: /**
1145: * <p>Converts array input of three elements into seconds</p>
1146: * <p>First element in array should represent hours</p>
1147: * <p>Second element in array should represent minutes</p>
1148: * <p>Third element in array should represent seconds</p>
1149: * @param array $input
1150: * @return int
1151: */
1152: protected function _getSeconds($input) {
1153: $multipliers = array(
1154: 3600,
1155: 60,
1156: 1,
1157: );
1158: $sum = 0;
1159: foreach ($multipliers as $i => $multiplier) {
1160: $sum += (int)($input[$i] * $multiplier);
1161: }
1162: return $sum;
1163: }
1164:
1165:
1166: /**
1167: * <p>Gets current carrier code for usage in other helper classes.</p>
1168: * @return string
1169: */
1170: public function getCode() {
1171: return $this->_code;
1172: }
1173:
1174:
1175:
1176: /**
1177: *
1178: * @return Mage_Sales_Model_Quote_Address
1179: */
1180: protected function _getAddressModel() {
1181: return Mage::getModel('sales/quote_address');
1182: }
1183:
1184: /**
1185: *
1186: * @return Mage_Catalog_Model_Product
1187: */
1188: protected function _getProductModel() {
1189: return Mage::getModel('catalog/product');
1190: }
1191:
1192: protected function _getOfficeModel() {
1193: return Mage::getModel('eabi_postoffice/office');
1194: }
1195:
1196:
1197: /**
1198: *
1199: * @return Eabi_Livehandler_Helper_Data
1200: */
1201: protected function _getEabi() {
1202: return Mage::helper('eabi');
1203: }
1204:
1205: /**
1206: *
1207: * @return Eabi_Postoffice_Helper_Data
1208: */
1209: protected function _getOfficeHelper() {
1210: return Mage::helper('eabi_postoffice');
1211: }
1212:
1213:
1214: /**
1215: *
1216: * @return Eabi_Postoffice_Helper_Countrycode
1217: */
1218: protected function _getDialCodeHelper() {
1219: return Mage::helper('eabi_postoffice/countrycode');
1220: }
1221:
1222: /**
1223: * Return checkout session object
1224: *
1225: * @return Mage_Checkout_Model_Session
1226: */
1227: protected function _getCheckoutSession() {
1228: if (Mage::app()->getStore()->isAdmin()) {
1229: return Mage::getSingleton('adminhtml/session_quote');
1230: }
1231: return Mage::getSingleton('checkout/session');
1232: }
1233:
1234: /**
1235: * Return checkout quote object
1236: *
1237: * @return Mage_Sales_Model_Quote
1238: */
1239: protected function _getQuote() {
1240: if (!$this->_quote) {
1241: $this->_quote = $this->_getCheckoutSession()->getQuote();
1242: }
1243: return $this->_quote;
1244: }
1245:
1246:
1247:
1248: }
1249:
1250: