1 | <?php |
||
2 | |||
3 | namespace SilverShop\Model; |
||
4 | |||
5 | use SilverShop\Cart\OrderTotalCalculator; |
||
6 | use SilverShop\Checkout\OrderEmailNotifier; |
||
7 | use SilverShop\Extension\MemberExtension; |
||
8 | use SilverShop\Extension\ShopConfigExtension; |
||
9 | use SilverShop\Model\Modifiers\OrderModifier; |
||
10 | use SilverShop\ORM\Filters\MultiFieldPartialMatchFilter; |
||
11 | use SilverShop\ORM\OrderItemList; |
||
12 | use SilverShop\Page\AccountPage; |
||
13 | use SilverShop\Page\CheckoutPage; |
||
14 | use SilverShop\ShopTools; |
||
15 | use SilverStripe\Control\Controller; |
||
16 | use SilverStripe\Control\Director; |
||
17 | use SilverStripe\Dev\Debug; |
||
18 | use SilverStripe\Forms\CheckboxSetField; |
||
19 | use SilverStripe\Forms\DateField; |
||
20 | use SilverStripe\Forms\DropdownField; |
||
21 | use SilverStripe\Forms\FieldList; |
||
22 | use SilverStripe\Forms\LiteralField; |
||
23 | use SilverStripe\Forms\Tab; |
||
24 | use SilverStripe\Forms\TabSet; |
||
25 | use SilverStripe\Omnipay\Extensions\Payable; |
||
26 | use SilverStripe\ORM\DataObject; |
||
27 | use SilverStripe\ORM\FieldType\DBCurrency; |
||
28 | use SilverStripe\ORM\FieldType\DBDatetime; |
||
29 | use SilverStripe\ORM\FieldType\DBEnum; |
||
30 | use SilverStripe\ORM\Filters\GreaterThanFilter; |
||
31 | use SilverStripe\ORM\Filters\LessThanFilter; |
||
32 | use SilverStripe\ORM\HasManyList; |
||
33 | use SilverStripe\ORM\Search\SearchContext; |
||
34 | use SilverStripe\ORM\UnsavedRelationList; |
||
35 | use SilverStripe\Security\Member; |
||
36 | use SilverStripe\Security\Security; |
||
37 | |||
38 | /** |
||
39 | * The order class is a databound object for handling Orders |
||
40 | * within SilverStripe. |
||
41 | * |
||
42 | * @mixin Payable |
||
43 | * |
||
44 | * @property DBCurrency $Total |
||
45 | * @property string $Reference |
||
46 | * @property DBDatetime $Placed |
||
47 | * @property DBDatetime $Paid |
||
48 | * @property DBDatetime $ReceiptSent |
||
49 | * @property DBDatetime $Printed |
||
50 | * @property DBDatetime $Dispatched |
||
51 | * @property DBEnum $Status |
||
52 | * @property string $FirstName |
||
53 | * @property string $Surname |
||
54 | * @property string $Email |
||
55 | * @property string $Notes |
||
56 | * @property string $IPAddress |
||
57 | * @property bool $SeparateBillingAddress |
||
58 | * @property string $Locale |
||
59 | * @property int $MemberID |
||
60 | * @property int $ShippingAddressID |
||
61 | * @property int $BillingAddressID |
||
62 | * @method Member|MemberExtension Member() |
||
63 | * @method Address BillingAddress() |
||
64 | * @method Address ShippingAddress() |
||
65 | * @method OrderItem[]|HasManyList Items() |
||
66 | * @method OrderModifier[]|HasManyList Modifiers() |
||
67 | * @method OrderStatusLog[]|HasManyList OrderStatusLogs() |
||
68 | */ |
||
69 | class Order extends DataObject |
||
70 | { |
||
71 | /** |
||
72 | * Status codes and what they mean: |
||
73 | * |
||
74 | * Unpaid (default): Order created but no successful payment by customer yet |
||
75 | * Query: Order not being processed yet (customer has a query, or could be out of stock) |
||
76 | * Paid: Order successfully paid for by customer |
||
77 | * Processing: Order paid for, package is currently being processed before shipping to customer |
||
78 | * Sent: Order paid for, processed for shipping, and now sent to the customer |
||
79 | * Complete: Order completed (paid and shipped). Customer assumed to have received their goods |
||
80 | * AdminCancelled: Order cancelled by the administrator |
||
81 | * MemberCancelled: Order cancelled by the customer (Member) |
||
82 | */ |
||
83 | private static $db = [ |
||
84 | 'Total' => 'Currency', |
||
85 | 'Reference' => 'Varchar', //allow for customised order numbering schemes |
||
86 | //status |
||
87 | 'Placed' => 'Datetime', //date the order was placed (went from Cart to Order) |
||
88 | 'Paid' => 'Datetime', //no outstanding payment left |
||
89 | 'ReceiptSent' => 'Datetime', //receipt emailed to customer |
||
90 | 'Printed' => 'Datetime', |
||
91 | 'Dispatched' => 'Datetime', //products have been sent to customer |
||
92 | 'Status' => "Enum('Unpaid,Paid,Processing,Sent,Complete,AdminCancelled,MemberCancelled,Cart','Cart')", |
||
93 | //customer (for guest orders) |
||
94 | 'FirstName' => 'Varchar', |
||
95 | 'Surname' => 'Varchar', |
||
96 | 'Email' => 'Varchar', |
||
97 | 'Notes' => 'Text', |
||
98 | 'IPAddress' => 'Varchar(15)', |
||
99 | //separate shipping |
||
100 | 'SeparateBillingAddress' => 'Boolean', |
||
101 | // keep track of customer locale |
||
102 | 'Locale' => 'Locale', |
||
103 | ]; |
||
104 | |||
105 | private static $has_one = [ |
||
106 | 'Member' => Member::class, |
||
107 | 'ShippingAddress' => Address::class, |
||
108 | 'BillingAddress' => Address::class, |
||
109 | ]; |
||
110 | |||
111 | private static $has_many = [ |
||
112 | 'Items' => OrderItem::class, |
||
113 | 'Modifiers' => OrderModifier::class, |
||
114 | 'OrderStatusLogs' => OrderStatusLog::class, |
||
115 | ]; |
||
116 | |||
117 | private static $defaults = [ |
||
118 | 'Status' => 'Cart', |
||
119 | ]; |
||
120 | |||
121 | private static $casting = [ |
||
122 | 'FullBillingAddress' => 'Text', |
||
123 | 'FullShippingAddress' => 'Text', |
||
124 | 'Total' => 'Currency', |
||
125 | 'SubTotal' => 'Currency', |
||
126 | 'TotalPaid' => 'Currency', |
||
127 | 'Shipping' => 'Currency', |
||
128 | 'TotalOutstanding' => 'Currency', |
||
129 | ]; |
||
130 | |||
131 | private static $summary_fields = [ |
||
132 | 'Reference', |
||
133 | 'Placed', |
||
134 | 'Name', |
||
135 | 'LatestEmail', |
||
136 | 'Total', |
||
137 | 'StatusI18N', |
||
138 | ]; |
||
139 | |||
140 | private static $searchable_fields = [ |
||
141 | 'Reference', |
||
142 | 'Name', |
||
143 | 'Email', |
||
144 | 'Status' => [ |
||
145 | 'filter' => 'ExactMatchFilter', |
||
146 | 'field' => CheckboxSetField::class, |
||
147 | ], |
||
148 | ]; |
||
149 | |||
150 | private static $table_name = 'SilverShop_Order'; |
||
151 | |||
152 | private static $singular_name = 'Order'; |
||
153 | |||
154 | private static $plural_name = 'Orders'; |
||
155 | |||
156 | private static $default_sort = '"Placed" DESC, "Created" DESC'; |
||
157 | |||
158 | /** |
||
159 | * Statuses for orders that have been placed. |
||
160 | * |
||
161 | * @config |
||
162 | */ |
||
163 | private static $placed_status = [ |
||
164 | 'Paid', |
||
165 | 'Unpaid', |
||
166 | 'Processing', |
||
167 | 'Sent', |
||
168 | 'Complete', |
||
169 | 'MemberCancelled', |
||
170 | 'AdminCancelled', |
||
171 | ]; |
||
172 | |||
173 | /** |
||
174 | * Statuses for which an order can be paid for |
||
175 | * |
||
176 | * @config |
||
177 | */ |
||
178 | private static $payable_status = [ |
||
179 | 'Cart', |
||
180 | 'Unpaid', |
||
181 | 'Processing', |
||
182 | 'Sent', |
||
183 | ]; |
||
184 | |||
185 | /** |
||
186 | * Statuses that shouldn't show in user account. |
||
187 | * |
||
188 | * @config |
||
189 | */ |
||
190 | private static $hidden_status = ['Cart']; |
||
191 | |||
192 | |||
193 | /** |
||
194 | * Statuses that should be logged in the Order-Status-Log |
||
195 | * |
||
196 | * @config |
||
197 | * @var array |
||
198 | */ |
||
199 | private static $log_status = []; |
||
200 | |||
201 | /** |
||
202 | * Whether or not an order can be cancelled before payment |
||
203 | * |
||
204 | * @config |
||
205 | * @var bool |
||
206 | */ |
||
207 | private static $cancel_before_payment = true; |
||
208 | |||
209 | /** |
||
210 | * Whether or not an order can be cancelled before processing |
||
211 | * |
||
212 | * @config |
||
213 | * @var bool |
||
214 | */ |
||
215 | private static $cancel_before_processing = false; |
||
216 | |||
217 | /** |
||
218 | * Whether or not an order can be cancelled before sending |
||
219 | * |
||
220 | * @config |
||
221 | * @var bool |
||
222 | */ |
||
223 | private static $cancel_before_sending = false; |
||
224 | |||
225 | /** |
||
226 | * Whether or not an order can be cancelled after sending |
||
227 | * |
||
228 | * @config |
||
229 | * @var bool |
||
230 | */ |
||
231 | private static $cancel_after_sending = false; |
||
232 | |||
233 | /** |
||
234 | * Place an order before payment processing begins |
||
235 | * |
||
236 | * @config |
||
237 | * @var boolean |
||
238 | */ |
||
239 | private static $place_before_payment = false; |
||
240 | |||
241 | /** |
||
242 | * Modifiers represent the additional charges or |
||
243 | * deductions associated to an order, such as |
||
244 | * shipping, taxes, vouchers etc. |
||
245 | * |
||
246 | * @config |
||
247 | * @var array |
||
248 | */ |
||
249 | private static $modifiers = []; |
||
250 | |||
251 | /** |
||
252 | * Rounding precision of order amounts |
||
253 | * |
||
254 | * @config |
||
255 | * @var int |
||
256 | */ |
||
257 | private static $rounding_precision = 2; |
||
258 | |||
259 | /** |
||
260 | * Minimal length (number of decimals) of order reference ids |
||
261 | * |
||
262 | * @config |
||
263 | * @var int |
||
264 | */ |
||
265 | private static $reference_id_padding = 5; |
||
266 | |||
267 | /** |
||
268 | * Will allow completion of orders with GrandTotal=0, |
||
269 | * which could be the case for orders paid with loyalty points or vouchers. |
||
270 | * Will send the "Paid" date on the order, even though no actual payment was taken. |
||
271 | * Will trigger the payment related extension points: |
||
272 | * Order->onPayment, OrderItem->onPayment, Order->onPaid. |
||
273 | * |
||
274 | * @config |
||
275 | * @var boolean |
||
276 | */ |
||
277 | private static $allow_zero_order_total = false; |
||
278 | |||
279 | /** |
||
280 | * A flag indicating that an order-status-log entry should be written |
||
281 | * |
||
282 | * @var bool |
||
283 | */ |
||
284 | protected $flagOrderStatusWrite = false; |
||
285 | |||
286 | public static function get_order_status_options() |
||
287 | { |
||
288 | $values = array(); |
||
289 | foreach (singleton(Order::class)->dbObject('Status')->enumValues(false) as $value) { |
||
290 | $values[$value] = _t(__CLASS__ . '.STATUS_' . strtoupper($value), $value); |
||
291 | } |
||
292 | return $values; |
||
293 | } |
||
294 | |||
295 | /** |
||
296 | * Create CMS fields for cms viewing and editing orders |
||
297 | */ |
||
298 | public function getCMSFields() |
||
299 | { |
||
300 | $fields = FieldList::create(TabSet::create('Root', Tab::create('Main'))); |
||
301 | $fs = '<div class="field">'; |
||
302 | $fe = '</div>'; |
||
303 | $parts = array( |
||
304 | DropdownField::create('Status', $this->fieldLabel('Status'), self::get_order_status_options()), |
||
305 | LiteralField::create('Customer', $fs . $this->renderWith('SilverShop\Admin\OrderAdmin_Customer') . $fe), |
||
306 | LiteralField::create('Addresses', $fs . $this->renderWith('SilverShop\Admin\OrderAdmin_Addresses') . $fe), |
||
307 | LiteralField::create('Content', $fs . $this->renderWith('SilverShop\Admin\OrderAdmin_Content') . $fe), |
||
308 | ); |
||
309 | if ($this->Notes) { |
||
310 | $parts[] = LiteralField::create('Notes', $fs . $this->renderWith('SilverShop\Admin\OrderAdmin_Notes') . $fe); |
||
311 | } |
||
312 | $fields->addFieldsToTab('Root.Main', $parts); |
||
313 | $this->extend('updateCMSFields', $fields); |
||
314 | if ($payments = $fields->fieldByName('Root.Payments.Payments')) { |
||
315 | $fields->removeByName('Payments'); |
||
316 | $fields->insertAfter('Content', $payments); |
||
317 | $payments->addExtraClass('order-payments'); |
||
318 | } |
||
319 | |||
320 | return $fields; |
||
321 | } |
||
322 | |||
323 | /** |
||
324 | * Augment field labels |
||
325 | */ |
||
326 | public function fieldLabels($includerelations = true) |
||
327 | { |
||
328 | $labels = parent::fieldLabels($includerelations); |
||
329 | |||
330 | $labels['Name'] = _t('SilverShop\Generic.Customer', 'Customer'); |
||
331 | $labels['LatestEmail'] = _t(__CLASS__ . '.db_Email', 'Email'); |
||
332 | $labels['StatusI18N'] = _t(__CLASS__ . '.db_Status', 'Status'); |
||
333 | |||
334 | return $labels; |
||
335 | } |
||
336 | |||
337 | /** |
||
338 | * Adjust scafolded search context |
||
339 | * |
||
340 | * @return SearchContext the updated search context |
||
341 | */ |
||
342 | public function getDefaultSearchContext() |
||
343 | { |
||
344 | $context = parent::getDefaultSearchContext(); |
||
345 | $fields = $context->getFields(); |
||
346 | |||
347 | $validStates = self::config()->placed_status; |
||
348 | $statusOptions = array_filter(self::get_order_status_options(), function ($k) use ($validStates) { |
||
349 | return in_array($k, $validStates); |
||
350 | }, ARRAY_FILTER_USE_KEY); |
||
351 | |||
352 | $fields->push( |
||
353 | // TODO: Allow filtering by multiple statuses |
||
354 | DropdownField::create('Status', $this->fieldLabel('Status')) |
||
355 | ->setSource($statusOptions) |
||
356 | ->setHasEmptyDefault(true) |
||
357 | ); |
||
358 | |||
359 | // add date range filtering |
||
360 | $fields->insertBefore( |
||
361 | 'Status', |
||
362 | DateField::create('DateFrom', _t(__CLASS__ . '.DateFrom', 'Date from')) |
||
363 | ); |
||
364 | |||
365 | $fields->insertBefore( |
||
366 | 'Status', |
||
367 | DateField::create('DateTo', _t(__CLASS__ . '.DateTo', 'Date to')) |
||
368 | ); |
||
369 | |||
370 | // get the array, to maniplulate name, and fullname seperately |
||
371 | $filters = $context->getFilters(); |
||
372 | $filters['DateFrom'] = GreaterThanFilter::create('Placed'); |
||
373 | $filters['DateTo'] = LessThanFilter::create('Placed'); |
||
374 | |||
375 | // filter customer need to use a bunch of different sources |
||
376 | $filters['Name'] = MultiFieldPartialMatchFilter::create( |
||
377 | 'FirstName', |
||
378 | false, |
||
379 | ['SplitWords'], |
||
380 | [ |
||
381 | 'Surname', |
||
382 | 'Member.FirstName', |
||
383 | 'Member.Surname', |
||
384 | 'BillingAddress.FirstName', |
||
385 | 'BillingAddress.Surname', |
||
386 | 'ShippingAddress.FirstName', |
||
387 | 'ShippingAddress.Surname', |
||
388 | ] |
||
389 | ); |
||
390 | |||
391 | $context->setFilters($filters); |
||
392 | |||
393 | $this->extend('updateDefaultSearchContext', $context); |
||
394 | return $context; |
||
395 | } |
||
396 | |||
397 | /** |
||
398 | * Hack for swapping out relation list with OrderItemList |
||
399 | * |
||
400 | * @inheritdoc |
||
401 | */ |
||
402 | public function getComponents($componentName, $id = null) |
||
403 | { |
||
404 | $components = parent::getComponents($componentName, $id); |
||
405 | if ($componentName === 'Items' && get_class($components) !== UnsavedRelationList::class) { |
||
406 | $query = $components->dataQuery(); |
||
407 | $components = OrderItemList::create(OrderItem::class, 'OrderID'); |
||
408 | $components->setDataQuery($query); |
||
409 | $components = $components->forForeignID($this->ID); |
||
410 | } |
||
411 | return $components; |
||
412 | } |
||
413 | |||
414 | /** |
||
415 | * Returns the subtotal of the items for this order. |
||
416 | */ |
||
417 | public function SubTotal() |
||
418 | { |
||
419 | if ($this->Items()->exists()) { |
||
420 | return $this->Items()->SubTotal(); |
||
421 | } |
||
422 | |||
423 | return 0; |
||
424 | } |
||
425 | |||
426 | /** |
||
427 | * Calculate the total |
||
428 | * |
||
429 | * @return float the final total |
||
430 | */ |
||
431 | public function calculate() |
||
432 | { |
||
433 | if (!$this->IsCart()) { |
||
434 | return $this->Total; |
||
435 | } |
||
436 | $calculator = OrderTotalCalculator::create($this); |
||
437 | return $this->Total = $calculator->calculate(); |
||
438 | } |
||
439 | |||
440 | /** |
||
441 | * This is needed to maintain backwards compatiability with |
||
442 | * some subsystems using modifiers. eg discounts |
||
443 | */ |
||
444 | public function getModifier($className, $forcecreate = false) |
||
445 | { |
||
446 | $calculator = OrderTotalCalculator::create($this); |
||
447 | return $calculator->getModifier($className, $forcecreate); |
||
448 | } |
||
449 | |||
450 | /** |
||
451 | * Enforce rounding precision when setting total |
||
452 | */ |
||
453 | public function setTotal($val) |
||
454 | { |
||
455 | $this->setField('Total', round($val, self::$rounding_precision)); |
||
456 | } |
||
457 | |||
458 | /** |
||
459 | * Get final value of order. |
||
460 | * Retrieves value from DataObject's record array. |
||
461 | */ |
||
462 | public function Total() |
||
463 | { |
||
464 | return $this->getField('Total'); |
||
465 | } |
||
466 | |||
467 | /** |
||
468 | * Alias for Total. |
||
469 | */ |
||
470 | public function GrandTotal() |
||
471 | { |
||
472 | return $this->Total(); |
||
473 | } |
||
474 | |||
475 | /** |
||
476 | * Calculate how much is left to be paid on the order. |
||
477 | * Enforces rounding precision. |
||
478 | * |
||
479 | * Payments that have been authorized via a non-manual gateway should count towards the total paid amount. |
||
480 | * However, it's possible to exclude these by setting the $includeAuthorized parameter to false, which is |
||
481 | * useful to determine the status of the Order. Order status should only change to 'Paid' when all |
||
482 | * payments are 'Captured'. |
||
483 | * |
||
484 | * @param bool $includeAuthorized whether or not to include authorized payments (excluding manual payments) |
||
485 | * @return float |
||
486 | */ |
||
487 | public function TotalOutstanding($includeAuthorized = true) |
||
488 | { |
||
489 | return round( |
||
490 | $this->GrandTotal() - ($includeAuthorized ? $this->TotalPaidOrAuthorized() : $this->TotalPaid()), |
||
491 | self::config()->rounding_precision |
||
492 | ); |
||
493 | } |
||
494 | |||
495 | /** |
||
496 | * Get the order status. This will return a localized value if available. |
||
497 | * |
||
498 | * @return string the payment status |
||
499 | */ |
||
500 | public function getStatusI18N() |
||
501 | { |
||
502 | return _t(__CLASS__ . '.STATUS_' . strtoupper($this->Status), $this->Status); |
||
503 | } |
||
504 | |||
505 | /** |
||
506 | * Get the link for finishing order processing. |
||
507 | */ |
||
508 | public function Link() |
||
509 | { |
||
510 | if (Security::getCurrentUser()) { |
||
511 | $link = Controller::join_links(AccountPage::find_link(), 'order', $this->ID); |
||
0 ignored issues
–
show
Unused Code
introduced
by
Loading history...
|
|||
512 | } |
||
513 | |||
514 | $link = CheckoutPage::find_link(false, 'order', $this->ID); |
||
515 | |||
516 | $this->extend('updateLink', $link); |
||
517 | |||
518 | return $link; |
||
519 | } |
||
520 | |||
521 | /** |
||
522 | * Returns TRUE if the order can be cancelled |
||
523 | * PRECONDITION: Order is in the DB. |
||
524 | * |
||
525 | * @return boolean |
||
526 | */ |
||
527 | public function canCancel($member = null) |
||
528 | { |
||
529 | $extended = $this->extendedCan(__FUNCTION__, $member); |
||
530 | if ($extended !== null) { |
||
531 | return $extended; |
||
532 | } |
||
533 | |||
534 | switch ($this->Status) { |
||
535 | case 'Unpaid' : |
||
536 | return self::config()->cancel_before_payment; |
||
537 | case 'Paid' : |
||
538 | return self::config()->cancel_before_processing; |
||
539 | case 'Processing' : |
||
540 | return self::config()->cancel_before_sending; |
||
541 | case 'Sent' : |
||
542 | case 'Complete' : |
||
543 | return self::config()->cancel_after_sending; |
||
544 | } |
||
545 | return false; |
||
546 | } |
||
547 | |||
548 | /** |
||
549 | * Check if an order can be paid for. |
||
550 | * |
||
551 | * @return boolean |
||
552 | */ |
||
553 | public function canPay($member = null) |
||
554 | { |
||
555 | $extended = $this->extendedCan(__FUNCTION__, $member); |
||
556 | if ($extended !== null) { |
||
557 | return $extended; |
||
558 | } |
||
559 | |||
560 | if (!in_array($this->Status, self::config()->payable_status)) { |
||
561 | return false; |
||
562 | } |
||
563 | if ($this->TotalOutstanding(true) > 0 && empty($this->Paid)) { |
||
564 | return true; |
||
565 | } |
||
566 | return false; |
||
567 | } |
||
568 | |||
569 | /** |
||
570 | * Prevent deleting orders. |
||
571 | * |
||
572 | * @return boolean |
||
573 | */ |
||
574 | public function canDelete($member = null) |
||
575 | { |
||
576 | $extended = $this->extendedCan(__FUNCTION__, $member); |
||
577 | if ($extended !== null) { |
||
578 | return $extended; |
||
579 | } |
||
580 | |||
581 | return false; |
||
582 | } |
||
583 | |||
584 | /** |
||
585 | * Check if an order can be viewed. |
||
586 | * |
||
587 | * @return boolean |
||
588 | */ |
||
589 | public function canView($member = null) |
||
590 | { |
||
591 | $extended = $this->extendedCan(__FUNCTION__, $member); |
||
592 | if ($extended !== null) { |
||
593 | return $extended; |
||
594 | } |
||
595 | |||
596 | return true; |
||
597 | } |
||
598 | |||
599 | /** |
||
600 | * Check if an order can be edited. |
||
601 | * |
||
602 | * @return boolean |
||
603 | */ |
||
604 | public function canEdit($member = null) |
||
605 | { |
||
606 | $extended = $this->extendedCan(__FUNCTION__, $member); |
||
607 | if ($extended !== null) { |
||
608 | return $extended; |
||
609 | } |
||
610 | |||
611 | return true; |
||
612 | } |
||
613 | |||
614 | /** |
||
615 | * Prevent standard creation of orders. |
||
616 | * |
||
617 | * @return boolean |
||
618 | */ |
||
619 | public function canCreate($member = null, $context = array()) |
||
620 | { |
||
621 | $extended = $this->extendedCan(__FUNCTION__, $member, $context); |
||
622 | if ($extended !== null) { |
||
623 | return $extended; |
||
624 | } |
||
625 | |||
626 | return false; |
||
627 | } |
||
628 | |||
629 | /** |
||
630 | * Return the currency of this order. |
||
631 | * Note: this is a fixed value across the entire site. |
||
632 | * |
||
633 | * @return string |
||
634 | */ |
||
635 | public function Currency() |
||
636 | { |
||
637 | return ShopConfigExtension::get_site_currency(); |
||
638 | } |
||
639 | |||
640 | /** |
||
641 | * Get the latest email for this order.z |
||
642 | */ |
||
643 | public function getLatestEmail() |
||
644 | { |
||
645 | if ($this->MemberID && ($this->Member()->LastEdited > $this->LastEdited || !$this->Email)) { |
||
646 | return $this->Member()->Email; |
||
647 | } |
||
648 | return $this->getField('Email'); |
||
649 | } |
||
650 | |||
651 | /** |
||
652 | * Gets the name of the customer. |
||
653 | */ |
||
654 | public function getName() |
||
655 | { |
||
656 | $firstname = $this->FirstName ? $this->FirstName : $this->Member()->FirstName; |
||
657 | $surname = $this->FirstName ? $this->Surname : $this->Member()->Surname; |
||
658 | return implode(' ', array_filter(array($firstname, $surname))); |
||
659 | } |
||
660 | |||
661 | public function getTitle() |
||
662 | { |
||
663 | return $this->Reference . ' - ' . $this->dbObject('Placed')->Nice(); |
||
664 | } |
||
665 | |||
666 | /** |
||
667 | * Get shipping address, or member default shipping address. |
||
668 | */ |
||
669 | public function getShippingAddress() |
||
670 | { |
||
671 | return $this->getAddress('Shipping'); |
||
672 | } |
||
673 | |||
674 | /** |
||
675 | * Get billing address, if marked to use seperate address, otherwise use shipping address, |
||
676 | * or the member default billing address. |
||
677 | */ |
||
678 | public function getBillingAddress() |
||
679 | { |
||
680 | if (!$this->SeparateBillingAddress && $this->ShippingAddressID === $this->BillingAddressID) { |
||
681 | return $this->getShippingAddress(); |
||
682 | } else { |
||
683 | return $this->getAddress('Billing'); |
||
684 | } |
||
685 | } |
||
686 | |||
687 | /** |
||
688 | * @param string $type - Billing or Shipping |
||
689 | * @return Address |
||
690 | * @throws \Exception |
||
691 | */ |
||
692 | protected function getAddress($type) |
||
693 | { |
||
694 | $address = $this->getComponent($type . 'Address'); |
||
695 | |||
696 | if (!$address || !$address->exists() && $this->Member()) { |
||
697 | $address = $this->Member()->{"Default${type}Address"}(); |
||
698 | } |
||
699 | |||
700 | if (empty($address->Surname) && empty($address->FirstName)) { |
||
701 | if ($member = $this->Member()) { |
||
702 | // If there's a member object, use information from the Member. |
||
703 | // The information from Order should have precendence if set though! |
||
704 | $address->FirstName = $this->FirstName ?: $member->FirstName; |
||
705 | $address->Surname = $this->Surname ?: $member->Surname; |
||
706 | } else { |
||
707 | $address->FirstName = $this->FirstName; |
||
708 | $address->Surname = $this->Surname; |
||
709 | } |
||
710 | } |
||
711 | |||
712 | return $address; |
||
713 | } |
||
714 | |||
715 | /** |
||
716 | * Check if the two addresses saved differ. |
||
717 | * |
||
718 | * @return boolean |
||
719 | */ |
||
720 | public function getAddressesDiffer() |
||
721 | { |
||
722 | return $this->SeparateBillingAddress || $this->ShippingAddressID !== $this->BillingAddressID; |
||
723 | } |
||
724 | |||
725 | /** |
||
726 | * Has this order been sent to the customer? |
||
727 | * (at "Sent" status). |
||
728 | * |
||
729 | * @return boolean |
||
730 | */ |
||
731 | public function IsSent() |
||
732 | { |
||
733 | return $this->Status == 'Sent'; |
||
734 | } |
||
735 | |||
736 | /** |
||
737 | * Is this order currently being processed? |
||
738 | * (at "Sent" OR "Processing" status). |
||
739 | * |
||
740 | * @return boolean |
||
741 | */ |
||
742 | public function IsProcessing() |
||
743 | { |
||
744 | return $this->IsSent() || $this->Status == 'Processing'; |
||
745 | } |
||
746 | |||
747 | /** |
||
748 | * Return whether this Order has been paid for (Status == Paid) |
||
749 | * or Status == Processing, where it's been paid for, but is |
||
750 | * currently in a processing state. |
||
751 | * |
||
752 | * @return boolean |
||
753 | */ |
||
754 | public function IsPaid() |
||
755 | { |
||
756 | return (boolean)$this->Paid || $this->Status == 'Paid'; |
||
757 | } |
||
758 | |||
759 | public function IsCart() |
||
760 | { |
||
761 | return $this->Status == 'Cart'; |
||
762 | } |
||
763 | |||
764 | /** |
||
765 | * Create a unique reference identifier string for this order. |
||
766 | */ |
||
767 | public function generateReference() |
||
768 | { |
||
769 | $reference = str_pad($this->ID, self::$reference_id_padding, '0', STR_PAD_LEFT); |
||
770 | |||
771 | $this->extend('generateReference', $reference); |
||
772 | |||
773 | $candidate = $reference; |
||
774 | //prevent generating references that are the same |
||
775 | $count = 0; |
||
776 | while (Order::get()->filter('Reference', $candidate)->count() > 0) { |
||
777 | $count++; |
||
778 | $candidate = $reference . '' . $count; |
||
779 | } |
||
780 | $this->Reference = $candidate; |
||
781 | } |
||
782 | |||
783 | /** |
||
784 | * Get the reference for this order, or fall back to order ID. |
||
785 | */ |
||
786 | public function getReference() |
||
787 | { |
||
788 | return $this->getField('Reference') ? $this->getField('Reference') : $this->ID; |
||
789 | } |
||
790 | |||
791 | /** |
||
792 | * Force creating an order reference |
||
793 | */ |
||
794 | protected function onBeforeWrite() |
||
795 | { |
||
796 | parent::onBeforeWrite(); |
||
797 | if (!$this->getField('Reference') && in_array($this->Status, self::$placed_status)) { |
||
798 | $this->generateReference(); |
||
799 | } |
||
800 | |||
801 | // perform status transition |
||
802 | if ($this->isInDB() && $this->isChanged('Status')) { |
||
803 | $this->statusTransition( |
||
804 | empty($this->original['Status']) ? 'Cart' : $this->original['Status'], |
||
805 | $this->Status |
||
806 | ); |
||
807 | } |
||
808 | |||
809 | // While the order is unfinished/cart, always store the current locale with the order. |
||
810 | // We do this everytime an order is saved, because the user might change locale (language-switch). |
||
811 | if ($this->Status == 'Cart') { |
||
812 | $this->Locale = ShopTools::get_current_locale(); |
||
813 | } |
||
814 | } |
||
815 | |||
816 | /** |
||
817 | * Called from @see onBeforeWrite whenever status changes |
||
818 | * |
||
819 | * @param string $fromStatus status to transition away from |
||
820 | * @param string $toStatus target status |
||
821 | */ |
||
822 | protected function statusTransition($fromStatus, $toStatus) |
||
823 | { |
||
824 | // Add extension hook to react to order status transitions. |
||
825 | $this->extend('onStatusChange', $fromStatus, $toStatus); |
||
826 | |||
827 | if ($toStatus == 'Paid' && !$this->Paid) { |
||
828 | $this->Paid = DBDatetime::now()->Rfc2822(); |
||
829 | foreach ($this->Items() as $item) { |
||
830 | $item->onPayment(); |
||
831 | } |
||
832 | //all payment is settled |
||
833 | $this->extend('onPaid'); |
||
834 | |||
835 | if (!$this->ReceiptSent) { |
||
836 | OrderEmailNotifier::create($this)->sendReceipt(); |
||
837 | $this->ReceiptSent = DBDatetime::now()->Rfc2822(); |
||
838 | } |
||
839 | } |
||
840 | |||
841 | $logStatus = $this->config()->log_status; |
||
842 | if (!empty($logStatus) && in_array($toStatus, $logStatus)) { |
||
843 | $this->flagOrderStatusWrite = $fromStatus != $toStatus; |
||
844 | } |
||
845 | } |
||
846 | |||
847 | /** |
||
848 | * delete attributes, statuslogs, and payments |
||
849 | */ |
||
850 | protected function onBeforeDelete() |
||
851 | { |
||
852 | foreach ($this->Items() as $item) { |
||
853 | $item->delete(); |
||
854 | } |
||
855 | |||
856 | foreach ($this->Modifiers() as $modifier) { |
||
857 | $modifier->delete(); |
||
858 | } |
||
859 | |||
860 | foreach ($this->OrderStatusLogs() as $logEntry) { |
||
861 | $logEntry->delete(); |
||
862 | } |
||
863 | |||
864 | // just remove the payment relations… |
||
865 | // that way payment objects still persist (they might be relevant for book-keeping?) |
||
866 | $this->Payments()->removeAll(); |
||
867 | |||
868 | parent::onBeforeDelete(); |
||
869 | } |
||
870 | |||
871 | public function onAfterWrite() |
||
872 | { |
||
873 | parent::onAfterWrite(); |
||
874 | |||
875 | //create an OrderStatusLog |
||
876 | if ($this->flagOrderStatusWrite) { |
||
877 | $this->flagOrderStatusWrite = false; |
||
878 | $log = OrderStatusLog::create(); |
||
879 | |||
880 | // populate OrderStatusLog |
||
881 | $log->Title = _t( |
||
882 | 'SilverShop\ShopEmail.StatusChanged', |
||
883 | 'Status for order #{OrderNo} changed to "{OrderStatus}"', |
||
884 | '', |
||
885 | ['OrderNo' => $this->Reference, 'OrderStatus' => $this->getStatusI18N()] |
||
886 | ); |
||
887 | $log->Note = _t('SilverShop\ShopEmail.StatusChange' . $this->Status . 'Note', $this->Status . 'Note'); |
||
888 | $log->OrderID = $this->ID; |
||
889 | OrderEmailNotifier::create($this)->sendStatusChange($log->Title, $log->Note); |
||
890 | $log->SentToCustomer = true; |
||
891 | $this->extend('updateOrderStatusLog', $log); |
||
892 | $log->write(); |
||
893 | } |
||
894 | } |
||
895 | |||
896 | public function debug() |
||
897 | { |
||
898 | if (Director::is_cli()) { |
||
899 | // Temporarily disabled. |
||
900 | // TODO: Reactivate when the following issue got fixed: https://github.com/silverstripe/silverstripe-framework/issues/7827 |
||
901 | return ''; |
||
902 | } |
||
903 | |||
904 | $val = "<div class='order'><h1>" . static::class . "</h1>\n<ul>\n"; |
||
905 | if ($this->record) { |
||
906 | foreach ($this->record as $fieldName => $fieldVal) { |
||
907 | $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n"; |
||
908 | } |
||
909 | } |
||
910 | $val .= "</ul>\n"; |
||
911 | $val .= "<div class='items'><h2>Items</h2>"; |
||
912 | if ($items = $this->Items()) { |
||
913 | $val .= $this->Items()->debug(); |
||
914 | } |
||
915 | $val .= "</div><div class='modifiers'><h2>Modifiers</h2>"; |
||
916 | if ($modifiers = $this->Modifiers()) { |
||
917 | $val .= $modifiers->debug(); |
||
918 | } |
||
919 | $val .= "</div></div>"; |
||
920 | |||
921 | return $val; |
||
922 | } |
||
923 | |||
924 | /** |
||
925 | * Provide i18n entities for the order class |
||
926 | * |
||
927 | * @return array |
||
928 | */ |
||
929 | public function provideI18nEntities() |
||
930 | { |
||
931 | $entities = parent::provideI18nEntities(); |
||
932 | |||
933 | // collect all the payment status values |
||
934 | foreach ($this->dbObject('Status')->enumValues() as $value) { |
||
935 | $key = strtoupper($value); |
||
936 | $entities[__CLASS__ . ".STATUS_$key"] = array( |
||
937 | $value, |
||
938 | "Translation of the order status '$value'", |
||
939 | ); |
||
940 | } |
||
941 | |||
942 | return $entities; |
||
943 | } |
||
944 | } |
||
945 |