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; |
||||||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
Loading history...
|
|||||||
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 | $link = CheckoutPage::find_link(false, 'order', $this->ID); |
||||||
511 | |||||||
512 | if (Security::getCurrentUser()) { |
||||||
513 | $link = Controller::join_links(AccountPage::find_link(), 'order', $this->ID); |
||||||
514 | } |
||||||
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)) { |
||||||
0 ignored issues
–
show
|
|||||||
646 | return $this->Member()->Email; |
||||||
0 ignored issues
–
show
|
|||||||
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; |
||||||
0 ignored issues
–
show
|
|||||||
657 | $surname = $this->FirstName ? $this->Surname : $this->Member()->Surname; |
||||||
0 ignored issues
–
show
|
|||||||
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(); |
||||||
0 ignored issues
–
show
The method
Payments() does not exist on SilverShop\Model\Order . Since you implemented __call , consider adding a @method annotation.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
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 |