1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36:
37:
38: Yii::import('application.models.X2Model');
39:
40: 41: 42: 43: 44: 45: 46: 47: 48: 49:
50: class Quote extends X2Model {
51:
52: public $supportsWorkflow = false;
53:
54: 55: 56: 57:
58: private $_lineItems;
59:
60: private $_contact;
61:
62: 63: 64: 65:
66: private $_deleteLineItems;
67:
68: 69: 70: 71:
72: private $_productLines;
73: 74: 75: 76:
77: private $_adjustmentLines;
78:
79:
80: 81: 82: 83:
84: public $hasLineItemErrors = false;
85: public $lineItemErrors = array();
86:
87: public static function lineItemOrder($i0,$i1) {
88: return $i0->lineNumber < $i1->lineNumber ? -1 : 1;
89: }
90:
91: 92: 93:
94: public function getLineItems() {
95: if (!isset($this->_lineItems)) {
96: $lineItems = $this->getRelated('products');
97: if(count(array_filter($lineItems,function($li){return empty($li->lineNumber);})) > 0) {
98:
99: foreach($lineItems as $i => $li) {
100: $li->lineNumber = $i;
101: $li->save();
102: }
103: }
104: usort($lineItems,'self::lineItemOrder');
105: $this->_lineItems = array();
106: foreach($lineItems as $li) {
107: $this->_lineItems[(int) $li->lineNumber] = $li;
108: }
109: }
110: return $this->_lineItems;
111: }
112:
113: 114: 115:
116: public function getAdjustmentLines(){
117: if(!isset($this->_adjustmentLines))
118: $this->_adjustmentLines = array_filter(
119: $this->lineItems,function($li){return $li->isTotalAdjustment;});
120: return $this->_adjustmentLines;
121: }
122:
123: 124: 125:
126: public function getProductLines(){
127: if(!isset($this->_productLines))
128: $this->_productLines = array_filter(
129: $this->lineItems,function($li){return !$li->isTotalAdjustment;});
130: return $this->_productLines;
131: }
132:
133: 134: 135: 136:
137: public static function model($className = __CLASS__) {
138: return parent::model($className);
139: }
140:
141: 142: 143:
144: public function relations(){
145:
146:
147: return array_merge(parent::relations(), array(
148: 'products' => array(
149: self::HAS_MANY, 'QuoteProduct', 'quoteId', 'order' => 'lineNumber ASC'),
150: 'contact' => array(
151: self::BELONGS_TO, 'Contacts', array('associatedContacts' => 'nameId'))
152: ));
153: }
154:
155: 156: 157:
158: public function tableName() {
159: return 'x2_quotes';
160: }
161:
162: public function behaviors() {
163: return array_merge(parent::behaviors(), array(
164: 'X2LinkableBehavior' => array(
165: 'class' => 'X2LinkableBehavior',
166: 'module' => 'quotes'
167: ),
168: 'ERememberFiltersBehavior' => array(
169: 'class' => 'application.components.ERememberFiltersBehavior',
170: 'defaults' => array(),
171: 'defaultStickOnClear' => false
172: )
173: ));
174: }
175:
176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187:
188: public function setLineItems(array $items, $save = false, $skipProcessing=false) {
189: if ($skipProcessing) {
190: $this->_lineItems = $items;
191: return;
192: }
193:
194: $this->_deleteLineItems = array();
195: if (count($items) === 0) {
196: QuoteProduct::model()->deleteAllByAttributes(array('quoteId' => $this->id));
197: return true;
198: }
199:
200:
201: $typeErrMsg = 'The setter of Quote.lineItems requires an array of QuoteProduct objects or '.
202: '[attribute]=>[value] arrays.';
203: $firstElt = reset($items);
204: $type = gettype($firstElt);
205: if ($type != 'object' && $type != 'array')
206: throw new Exception($typeErrMsg);
207: if ($type == 'object')
208: if (get_class($firstElt) != 'QuoteProduct')
209: throw new Exception($typeErrMsg);
210:
211:
212: $existingItemIds = array();
213: $newItems = array();
214: $itemSet = array();
215: $existingItems = array();
216: foreach ($this->lineItems as $item) {
217: if ($item->isNewRecord) {
218:
219:
220: $item->save();
221: }
222: $existingItems[$item->id] = $item;
223: $existingItemIds[] = (int) $item->id;
224: }
225:
226:
227: if (isset($items['']))
228: unset($items['']);
229: if ($type == 'object') {
230: foreach ($items as $item) {
231: if (in_array($item->id, $existingItemIds)) {
232: $itemSet[$item->id] = $existingItems[$item->id];
233: $itemSet[$item->id]->attributes = $item->attributes;
234: } else {
235: $newItems[] = $item;
236: }
237: }
238: } else if ($type == 'array') {
239: foreach ($items as $item) {
240: $new = false;
241: if (isset($item['id'])) {
242: $id = $item['id'];
243: if (in_array($id, $existingItemIds)) {
244: $itemSet[$id] = $existingItems[$item['id']];
245: $itemSet[$id]->attributes = $item;
246: } else
247: $new = true;
248: } else
249: $new = true;
250:
251: if ($new) {
252: $itemObj = new QuoteProduct;
253: $itemObj->attributes = $item;
254: $newItems[] = $itemObj;
255: }
256: }
257: }
258:
259:
260: $itemIds = array_keys($itemSet);
261: $deleteItemIds = array_diff($existingItemIds, $itemIds);
262: $updateItemIds = array_intersect($existingItemIds, $itemIds);
263:
264:
265: $this->_lineItems = array_merge($newItems, array_values($itemSet));
266: usort($this->_lineItems,'self::lineItemOrder');
267: $this->_deleteLineItems = array_map(
268: function($id) use($existingItems) {return $existingItems[$id];}, $deleteItemIds);
269:
270:
271:
272:
273:
274: $defaultCurrency = empty($this->currency)?
275: Yii::app()->settings->currency:$this->currency;
276:
277: $curSym = Yii::app()->locale->getCurrencySymbol($defaultCurrency);
278: if (is_null($curSym))
279: $curSym = $defaultCurrency;
280:
281: foreach($this->_lineItems as $lineItem) {
282: $lineItem->quoteId = $this->id;
283: $product = X2Model::model('Products')->findByAttributes(array('name'=>$lineItem->name));
284: if (isset($product))
285: $lineItem->productId = $product->id;
286: if(empty($lineItem->currency))
287: $lineItem->currency = $defaultCurrency;
288: if($lineItem->isPercentAdjustment) {
289: $lineItem->adjustment = Fields::strToNumeric(
290: $lineItem->adjustment,'percentage');
291: } else {
292: $lineItem->adjustment = Fields::strToNumeric(
293: $lineItem->adjustment,'currency',$curSym);
294: }
295: $lineItem->price = Fields::strToNumeric($lineItem->price,'currency',$curSym);
296: $lineItem->total = Fields::strToNumeric($lineItem->total,'currency',$curSym);
297: }
298:
299:
300: $this->hasLineItemErrors = false;
301: $this->lineItemErrors = array();
302: foreach ($this->_lineItems as $item) {
303: $itemValid = $item->validate();
304: if (!$itemValid) {
305: $this->hasLineItemErrors = true;
306: foreach ($item->errors as $attribute => $errors)
307: foreach ($errors as $error)
308: $this->lineItemErrors[] = $error;
309: }
310: }
311: $this->lineItemErrors = array_unique($this->lineItemErrors);
312:
313:
314: $this->_adjustmentLines = null;
315: $this->_productLines = null;
316:
317:
318: if($save && !$this->hasLineItemErrors)
319: $this->saveLineItems();
320: }
321:
322: 323: 324:
325: public function saveLineItems(){
326:
327: if(isset($this->_lineItems)){
328: foreach($this->_lineItems as $item){
329: $item->quoteId = $this->id;
330: $product = X2Model::model('Products')->findByAttributes(array(
331: 'name'=>$item->name
332: ));
333: if (isset($product))
334: $item->productId = $product->id;
335: $item->save();
336: }
337: }
338: if(isset($this->_deleteLineItems)) {
339:
340: foreach($this->_deleteLineItems as $item)
341: $item->delete();
342: $this->_deleteLineItems = null;
343: }
344: }
345:
346: public function getContactId () {
347: list ($name, $id) = Fields::nameAndId ($this->associatedContacts);
348: return $id;
349: }
350:
351: public function getAccountId () {
352: list ($name, $id) = Fields::nameAndId ($this->accountName);
353: return $id;
354: }
355:
356: 357: 358:
359: public function createActionRecord() {
360: if(!empty($this->contactId)) {
361: $this->createAssociatedAction ('contacts', $this->contactId);
362: }
363: if(!empty($this->accountName)) {
364: $this->createAssociatedAction ('accounts', $this->accountId);
365: }
366: }
367:
368: public function createAssociatedAction ($type, $id) {
369: $now = time();
370: $actionAttributes = array(
371: 'type' => 'quotes',
372: 'actionDescription' => $this->id,
373: 'completeDate' => $now,
374: 'dueDate' => $now,
375: 'createDate' => $now,
376: 'lastUpdated' => $now,
377: 'complete' => 'Yes',
378: 'completedBy' => $this->createdBy,
379: 'updatedBy' => $this->updatedBy
380: );
381: $action = new Actions();
382: $action->attributes = $actionAttributes;
383: $action->associationType = $type;
384: $action->associationId = $id;
385: $action->save();
386: }
387:
388: 389: 390:
391: public function createEventRecord() {
392:
393:
394:
395:
396:
397:
398:
399:
400:
401: }
402:
403: public static function getStatusList() {
404: $field = Fields::model()->findByAttributes(array('modelName' => 'Quote', 'fieldName' => 'status'));
405: $dropdown = Dropdowns::model()->findByPk($field->linkType);
406: return CJSON::decode($dropdown->options, true);
407:
408: 409: 410: 411: 412: 413: 414:
415: }
416:
417: 418: 419: 420: 421: 422:
423: public function productTable($emailTable = false) {
424: if (!YII_UNIT_TESTING)
425: Yii::app()->clientScript->registerCssFile (
426: Yii::app()->getModule('quotes')->assetsUrl.'/css/productTable.css'
427: );
428: $pad = 4;
429:
430: $tableStyle = 'border-collapse: collapse; width: 100%;';
431: $thStyle = 'padding: 5px; border: 1px solid black; background:#eee;';
432: $thProductStyle = $thStyle;
433: if(!$emailTable)
434: $tableStyle .= 'display: inline;';
435: else
436: $thProductStyle .= "width:60%;";
437: $defaultStyle = 'padding: 5px;border-spacing:0;';
438: $tdStyle = "$defaultStyle;border-left: 1px solid black; border-right: 1px solid black;";
439: $tdFooterStyle = "$tdStyle;border-bottom: 1px solid black";
440: $tdBoxStyle = "$tdFooterStyle;border-top: 1px solid black";
441:
442:
443: $thProduct = '<th style="'.$thProductStyle.'">{c}</th>';
444: $tdDef = '<td style="'.$defaultStyle.'">{c}</td>';
445: $td = '<td style="'.$tdStyle.'">{c}</td>';
446: $tdFooter = '<td style="'.$tdFooterStyle.'">{c}</td>';
447: $tdBox = '<td style="'.$tdBoxStyle.'">{c}</td>';
448: $hr = '<hr style="width: 100%;height:2px;background:black;" />';
449: $tr = '<tr>{c}</tr>';
450: $colRange = range(2,7);
451: $span = array_combine($colRange,array_map(function($s){
452: return "<td colspan=\"$s\"></td>";},$colRange));
453: $span[1] = '<td></td>';
454:
455: $markup = array();
456:
457:
458: $markup[] = "<table class='quotes-product-table' style=\"$tableStyle\"><thead>";
459: $row = array ();
460: foreach(array(
461: 'Line Item' => '20%; min-width: 200px;',
462: 'Unit Price' => '17.5%',
463: 'Quantity' => '15%',
464: 'Adjustment' => '15%',
465: 'Comments' => '15%',
466: 'Price' => '20%'
467: ) as $columnHeader => $width) {
468: $row[] =
469: '<th style="'.$thStyle."width: $width;".'">'.
470: Yii::t('products',$columnHeader).
471: '</th>';
472: }
473: $markup[] = str_replace('{c}',implode("\n",$row),$tr);
474:
475:
476: $markup[] = "</thead>";
477:
478:
479: $n_li = count($this->productLines);
480: $i = 1;
481:
482:
483: $markup[] = '<tbody>';
484: foreach($this->productLines as $ln=>$li) {
485:
486: $row = array();
487:
488: foreach(array('name','price','quantity','adjustment','description','total') as $attr) {
489: $row[] = str_replace('{c}',$li->renderAttribute($attr),($i==$n_li?$tdFooter:$td));
490: }
491:
492: $markup[] = str_replace('{c}',implode('',$row),$tr);
493: $i++;
494: }
495:
496: $markup[] = '</tbody>';
497: $markup[] = '<tbody>';
498:
499: $i = 1;
500: $n_adj = count($this->adjustmentLines);
501:
502: if($n_adj) {
503:
504: $row = array($span[$pad]);
505: $row[] = str_replace('{c}','<strong>'.Yii::t('quotes','Subtotal').'</strong>',$tdDef);
506: $row[] = str_replace('{c}','<strong>'.Yii::app()->locale->numberFormatter->formatCurrency($this->subtotal,$this->currency).'</strong>',$tdDef);
507: $markup[] = str_replace('{c}',implode('',$row),$tr);
508: $markup[] = '</tbody>';
509:
510: $markup[] = '<tbody>';
511: foreach($this->adjustmentLines as $ln => $li) {
512:
513: $row = array($span[$pad]);
514: $row[] = str_replace('{c}',$li->renderAttribute('name').(!empty($li->description) ? ' ('.$li->renderAttribute('description').')':''),$tdDef);
515: $row[] = str_replace('{c}',$li->renderAttribute('adjustment'),$tdDef);
516:
517: $markup[] = str_replace('{c}',implode('',$row),$tr);
518: $i++;
519: }
520: $markup[] = '</tbody>';
521: $markup[] = '<tbody>';
522: }
523:
524:
525: $row = array($span[$pad]);
526: $row[] = str_replace('{c}','<strong>'.Yii::t('quotes','Total').'</strong>',$tdDef);
527: $row[] = str_replace('{c}','<strong>'.Yii::app()->locale->numberFormatter->formatCurrency($this->total,$this->currency).'</strong>',$tdBox);
528: $markup[] = str_replace('{c}',implode('',$row),$tr);
529: $markup[] = '</tbody>';
530:
531:
532: $markup[] = '</table>';
533:
534: return implode("\n",$markup);
535: }
536:
537: public static function getNames() {
538:
539: $names = array(0 => "None");
540:
541: foreach (Yii::app()->db->createCommand()->select('id,name')->from('x2_quotes')->queryAll(false) as $row)
542: $names[$row[0]] = $row[1];
543:
544: return $names;
545: }
546:
547: public static function parseUsers($userArray) {
548: return implode(', ', $userArray);
549: }
550:
551: public static function parseUsersTwo($arr) {
552: $str = "";
553: if(is_array($arr)){
554: $arr=array_keys($arr);
555: $str=implode(', ',$arr);
556: }
557: $str = substr($str, 0, strlen($str) - 2);
558:
559: return $str;
560: }
561:
562: public static function parseContacts($contactArray) {
563: return implode(' ', $contactArray);
564: }
565:
566: public static function parseContactsTwo($arr) {
567: $str = "";
568: foreach ($arr as $id => $contact) {
569: $str.=$id . " ";
570: }
571: return $str;
572: }
573:
574: public static function getQuotesLinks($accountId) {
575:
576: $quotesList = X2Model::model('Quote')->findAllByAttributes(array('accountName' => $accountId));
577:
578:
579: $links = array();
580: foreach ($quotesList as $model) {
581: $links[] = CHtml::link($model->name, array('/quotes/quotes/view', 'id' => $model->id));
582: }
583: return implode(', ', $links);
584: }
585:
586: public static function editContactArray($arr, $model) {
587:
588: $pieces = explode(" ", $model->associatedContacts);
589: unset($arr[0]);
590:
591: foreach ($pieces as $contact) {
592: if (array_key_exists($contact, $arr)) {
593: unset($arr[$contact]);
594: }
595: }
596:
597: return $arr;
598: }
599:
600: public static function editUserArray($arr, $model) {
601:
602: $pieces = explode(', ', $model->assignedTo);
603: unset($arr['Anyone']);
604: unset($arr['admin']);
605: foreach ($pieces as $user) {
606: if (array_key_exists($user, $arr)) {
607: unset($arr[$user]);
608: }
609: }
610: return $arr;
611: }
612:
613: public static function editUsersInverse($arr) {
614:
615: $data = array();
616:
617: foreach ($arr as $username) {
618: if ($username != '')
619: $data[] = User::model()->findByAttributes(array('username' => $username));
620: }
621:
622: $temp = array();
623: if (isset($data)) {
624: foreach ($data as $item) {
625: if (isset($item))
626: $temp[$item->username] = $item->firstName . ' ' . $item->lastName;
627: }
628: }
629: return $temp;
630: }
631:
632: public static function editContactsInverse($arr) {
633: $data = array();
634:
635: foreach ($arr as $id) {
636: if ($id != '')
637: $data[] = X2Model::model('Contacts')->findByPk($id);
638: }
639: $temp = array();
640:
641: foreach ($data as $item) {
642: $temp[$item->id] = $item->firstName . ' ' . $item->lastName;
643: }
644: return $temp;
645: }
646:
647: public function search($pageSize=null, $uniqueId=null) {
648: $pageSize = $pageSize === null ? Profile::getResultsPerPage() : $pageSize;
649: $criteria = new CDbCriteria;
650: $parameters = array('limit' => ceil($pageSize));
651: $criteria->scopes = array('findAll' => array($parameters));
652: $criteria->addCondition("(t.type!='invoice' and t.type!='dummyQuote') OR t.type IS NULL");
653:
654: return $this->searchBase($criteria, $pageSize);
655: }
656:
657: public function searchInvoice() {
658: $criteria = new CDbCriteria;
659: $parameters = array('limit' => ceil(Profile::getResultsPerPage()));
660: $criteria->scopes = array('findAll' => array($parameters));
661: $criteria->addCondition("t.type='invoice'");
662:
663: return $this->searchBase($criteria);
664: }
665:
666: public function getName () {
667: if ($this->name == '') {
668: return $this->id;
669: } else {
670: return $this->name;
671: }
672: }
673:
674: public function searchAdmin() {
675: $criteria = new CDbCriteria;
676:
677: return $this->searchBase($criteria);
678: }
679:
680: public function searchBase(
681: $criteria, $pageSize=null, $showHidden = false) {
682:
683: return parent::searchBase($criteria, $pageSize, $showHidden);
684: }
685:
686: 687: 688: 689:
690: public function productNames() {
691: $products = Product::model()->findAll(
692: array(
693: 'select' => 'id, name',
694: 'condition' => 'status=:active',
695: 'params' => array(':active' => 'Active'),
696: )
697: );
698: $productNames = array(0 => '');
699: foreach ($products as $product)
700: $productNames[$product->id] = $product->name;
701:
702:
703: $quoteProducts = QuoteProduct::model()->findAll(
704: array(
705: 'select' => 'productId, name',
706: 'condition' => 'quoteId=:quoteId',
707: 'params' => array(':quoteId' => $this->id),
708: )
709: );
710: foreach ($quoteProducts as $qp)
711: if (!isset($productNames[$qp->productId]))
712: $productNames[$qp->productId] = $qp->name;
713:
714: return $productNames;
715: }
716:
717: public function productPrices() {
718: $products = Product::model()->findAll(
719: array(
720: 'select' => 'id, price',
721: 'condition' => 'status=:active',
722: 'params' => array(':active' => 'Active'),
723: )
724: );
725: $productPrices = array(0 => '');
726: foreach ($products as $product)
727: $productPrices[$product->id] = $product->price;
728:
729:
730: $quoteProducts = QuoteProduct::model()->findAll(
731: array(
732: 'select' => 'productId, price',
733: 'condition' => 'quoteId=:quoteId',
734: 'params' => array(':quoteId' => $this->id),
735: )
736: );
737: foreach ($quoteProducts as $qp)
738: if (!isset($productPrices[$qp->productId]))
739: $productPrices[$qp->productId] = $qp->price;
740:
741: return $productPrices;
742: }
743:
744: public function activeProducts() {
745: $products = Product::model()->findAllByAttributes(array('status' => 'Active'));
746: $inactive = Product::model()->findAllByAttributes(array('status' => 'Inactive'));
747: $quoteProducts = QuoteProduct::model()->findAll(
748: array(
749: 'select' => 'productId',
750: 'condition' => 'quoteId=:quoteId',
751: 'params' => array(':quoteId' => $this->id),
752: )
753: );
754: foreach ($quoteProducts as $qp)
755: foreach ($inactive as $i)
756: if ($qp->productId == $i->id)
757: $products[] = $i;
758: return $products;
759: }
760:
761: 762: 763:
764: public function beforeDelete(){
765: QuoteProduct::model()->deleteAllByAttributes(array('quoteId'=>$this->id));
766:
767:
768: Relationships::model()->deleteAllByAttributes(
769: array('firstType' => 'quotes', 'firstId' => $this->id));
770:
771:
772: $contact = $this->contact;
773: if(!empty($contact)){
774: $action = new Actions;
775: $action->associationType = 'contacts';
776: $action->type = 'quotesDeleted';
777: $action->associationId = $contact->id;
778: $action->associationName = $contact->name;
779: $action->assignedTo = Yii::app()->getSuModel()->username;
780: $action->completedBy = Yii::app()->getSuModel()->username;
781: $action->createDate = time();
782: $action->dueDate = time();
783: $action->completeDate = time();
784: $action->visibility = 1;
785: $action->complete = 'Yes';
786: $action->actionDescription =
787: "Deleted Quote: <span style=\"font-weight:bold;\">{$this->id}</span> {$this->name}";
788:
789: $action->save();
790: }
791: return parent::beforeDelete();
792: }
793: }
794: