1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace SilverCommerce\OrdersAdmin\Forms\GridField; |
4
|
|
|
|
5
|
|
|
use LogicException; |
6
|
|
|
use SilverStripe\Core\Convert; |
7
|
|
|
use SilverStripe\ORM\DataList; |
8
|
|
|
use SilverStripe\View\SSViewer; |
9
|
|
|
use SilverStripe\View\ArrayData; |
10
|
|
|
use SilverStripe\Forms\FieldList; |
11
|
|
|
use SilverStripe\Forms\TextField; |
12
|
|
|
use SilverStripe\Control\Controller; |
13
|
|
|
use SilverStripe\Core\Config\Config; |
14
|
|
|
use SilverStripe\Control\HTTPResponse; |
15
|
|
|
use SilverCommerce\TaxAdmin\Model\TaxRate; |
16
|
|
|
use SilverStripe\Forms\GridField\GridField; |
17
|
|
|
use SilverStripe\Forms\GridField\GridField_FormAction; |
18
|
|
|
use Doctrine\Instantiator\Exception\UnexpectedValueException; |
19
|
|
|
use SilverStripe\Dev\Debug; |
20
|
|
|
use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* A specific gridfield field designed to allow the creation of a new |
24
|
|
|
* order item and that auto completes all fields from a pre-defined |
25
|
|
|
* object (default Product). |
26
|
|
|
* |
27
|
|
|
* @package orders-admin |
28
|
|
|
* |
29
|
|
|
* @author ilateral <[email protected]> |
30
|
|
|
* @author Michael Strong <[email protected]> |
31
|
|
|
**/ |
32
|
|
|
class AddLineItem extends GridFieldAddExistingAutocompleter |
33
|
|
|
{ |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* Default field to create the dataobject by should be Title. |
37
|
|
|
* |
38
|
|
|
* @var string |
39
|
|
|
**/ |
40
|
|
|
protected $dataObjectField = "Title"; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Default field to create the dataobject from. |
44
|
|
|
* |
45
|
|
|
* @var string |
46
|
|
|
**/ |
47
|
|
|
protected $source_class = "Product"; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* When we check for existing items, should we check based on all |
51
|
|
|
* filters or any of the chosen (setting this to true uses |
52
|
|
|
* $list->filter() where as false uses $list->filterAny()) |
53
|
|
|
* |
54
|
|
|
* @var boolean |
55
|
|
|
*/ |
56
|
|
|
protected $strict_filter = true; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* What filter should the initial search use? |
60
|
|
|
* |
61
|
|
|
* @var string |
62
|
|
|
*/ |
63
|
|
|
protected $search_filter = 'PartialMatch'; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* Fields that we try and find our source object based on |
67
|
|
|
* |
68
|
|
|
* @var array |
69
|
|
|
**/ |
70
|
|
|
protected $filter_fields = [ |
71
|
|
|
"Title", |
72
|
|
|
"StockID" |
73
|
|
|
]; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* Fields that we use to filter items for our autocomplete |
77
|
|
|
* |
78
|
|
|
* @var array |
79
|
|
|
**/ |
80
|
|
|
protected $autocomplete_fields = [ |
81
|
|
|
"Title", |
82
|
|
|
"StockID" |
83
|
|
|
]; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* If filter fails, set this field when creating |
87
|
|
|
* |
88
|
|
|
* @var String |
89
|
|
|
**/ |
90
|
|
|
protected $create_field = "Title"; |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* Fields that we are mapping from the source object to our item |
94
|
|
|
* |
95
|
|
|
* @var array |
96
|
|
|
**/ |
97
|
|
|
protected $source_fields = [ |
98
|
|
|
"Title" => "Title", |
99
|
|
|
"StockID" => "StockID", |
100
|
|
|
"BasePrice" => "BasePrice" |
101
|
|
|
]; |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* This is the field that we attempt to match a TAX rate to |
105
|
|
|
* when setting up an order item |
106
|
|
|
* |
107
|
|
|
* @var string |
108
|
|
|
**/ |
109
|
|
|
protected $source_tax_field = "TaxPercentage"; |
110
|
|
|
|
111
|
|
|
public function getSourceClass() |
112
|
|
|
{ |
113
|
|
|
return $this->source_class; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
public function setSourceClass($class) |
117
|
|
|
{ |
118
|
|
|
$this->source_class = $class; |
119
|
|
|
return $this; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
public function getStrictFilter() |
123
|
|
|
{ |
124
|
|
|
return $this->strict_filter; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
public function setStrictFilter($bool) |
128
|
|
|
{ |
129
|
|
|
$this->strict_filter = $bool; |
130
|
|
|
return $this; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
public function getFilterFields() |
134
|
|
|
{ |
135
|
|
|
return $this->filter_fields; |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
public function setFilterFields($fields) |
139
|
|
|
{ |
140
|
|
|
$this->filter_fields = $fields; |
141
|
|
|
return $this; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
public function getAutocompleteFields() |
145
|
|
|
{ |
146
|
|
|
return $this->autocomplete_fields; |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
public function setAutocompleteFields($fields) |
150
|
|
|
{ |
151
|
|
|
$this->autocomplete_fields = $fields; |
152
|
|
|
return $this; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
public function getCreateField() |
156
|
|
|
{ |
157
|
|
|
return $this->create_field; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
public function setCreateField($field) |
161
|
|
|
{ |
162
|
|
|
$this->create_field = $field; |
163
|
|
|
return $this; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
public function getSourceFields() |
167
|
|
|
{ |
168
|
|
|
return $this->source_fields; |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
public function setSourceFields($fields) |
172
|
|
|
{ |
173
|
|
|
$this->source_fields = $fields; |
174
|
|
|
return $this; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
public function getSourceTaxField() |
178
|
|
|
{ |
179
|
|
|
return $this->source_tax_field; |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
public function setSourceTaxField($field) |
183
|
|
|
{ |
184
|
|
|
$this->source_tax_field = $field; |
185
|
|
|
return $this; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
public function getSearchFilter() |
189
|
|
|
{ |
190
|
|
|
return $this->search_filter; |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
public function setSearchFilter(string $search_filter) |
194
|
|
|
{ |
195
|
|
|
$this->search_filter = $search_filter; |
196
|
|
|
return $this; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* Handles the add action for the given DataObject |
202
|
|
|
* |
203
|
|
|
* @param $grid GridFIeld |
204
|
|
|
* @param $actionName string |
205
|
|
|
* @param $arguments mixed |
206
|
|
|
* @param $data array |
207
|
|
|
**/ |
208
|
|
|
public function handleAction(GridField $grid, $actionName, $arguments, $data) |
209
|
|
|
{ |
210
|
|
|
if ($actionName == "addto") { |
211
|
|
|
// Get our submitted fields and object class |
212
|
|
|
$dbField = $this->getDataObjectField(); |
213
|
|
|
$objClass = $grid->getModelClass(); |
214
|
|
|
$source_class = $this->getSourceClass(); |
215
|
|
|
$source_item = null; |
216
|
|
|
$list = $grid->getList(); |
217
|
|
|
$filter = []; |
218
|
|
|
|
219
|
|
|
// Has the user used autocomplete |
220
|
|
|
if (isset($data['relationID']) && $data['relationID']) { |
221
|
|
|
$string = $data['relationID']; |
222
|
|
|
} else { |
223
|
|
|
$string = null; |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
foreach ($this->getFilterFields() as $filter_field) { |
227
|
|
|
$filter[$filter_field] = $string; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
// First check if we already have an object or if we need to |
231
|
|
|
// create one |
232
|
|
|
if ($this->getStrictFilter()) { |
233
|
|
|
$existing_obj = $list |
234
|
|
|
->filter($filter) |
|
|
|
|
235
|
|
|
->first(); |
236
|
|
|
} else { |
237
|
|
|
$existing_obj = $list |
238
|
|
|
->filterAny($filter) |
|
|
|
|
239
|
|
|
->first(); |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
if ($existing_obj) { |
243
|
|
|
$obj = $existing_obj; |
244
|
|
|
} else { |
245
|
|
|
$obj = $objClass::create(); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
// Is this a valid field |
249
|
|
|
if (!$obj->hasField($dbField)) { |
250
|
|
|
throw new UnexpectedValueException("Invalid field (" . $dbField . ") on " . $obj->ClassName . "."); |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
if ($obj->ID && $obj->canEdit()) { |
254
|
|
|
// An existing record and can edit, update quantity |
255
|
|
|
$curr_qty = ($obj->Quantity) ? $obj->Quantity : 0; |
256
|
|
|
|
257
|
|
|
$obj->setCastedField( |
258
|
|
|
"Quantity", |
259
|
|
|
$curr_qty + 1 |
260
|
|
|
); |
261
|
|
|
|
262
|
|
|
$list->add($obj); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
if (!$obj->ID && $obj->canCreate()) { |
266
|
|
|
// If source item not set, try and get one or get a |
267
|
|
|
// an existing record |
268
|
|
|
if (!$source_item && class_exists($source_class)) { |
|
|
|
|
269
|
|
|
$source_item = $source_class::get() |
270
|
|
|
->filterAny($filter) |
271
|
|
|
->first(); |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
if ($source_item) { |
275
|
|
|
foreach ($this->getSourceFields() as $obj_field => $source_field) { |
276
|
|
|
$obj->setCastedField( |
277
|
|
|
$obj_field, |
278
|
|
|
$source_item->$source_field |
279
|
|
|
); |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
// Setup the tax |
283
|
|
|
$tax_field = $this->getSourceTaxField(); |
284
|
|
|
$tax = TaxRate::get()->find("Rate", $source_item->$tax_field); |
285
|
|
|
if ($tax) { |
|
|
|
|
286
|
|
|
$obj->TaxRateID = $tax->ID; |
287
|
|
|
} |
288
|
|
|
} else { |
289
|
|
|
$obj->setCastedField($this->getCreateField(), $string); |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
$obj->setCastedField("Quantity", 1); |
293
|
|
|
$list->add($obj, []); |
|
|
|
|
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
// Finally, issue a redirect to update totals |
297
|
|
|
$controller = Controller::curr(); |
298
|
|
|
|
299
|
|
|
$response = $controller->response; |
300
|
|
|
$response->addHeader('X-Pjax', 'Content'); |
301
|
|
|
$response->addHeader('X-Reload', true); |
|
|
|
|
302
|
|
|
|
303
|
|
|
return $controller->redirect($grid->getForm()->controller->Link(), 302); |
304
|
|
|
} |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
/** |
308
|
|
|
* Renders the TextField and add button to the GridField. |
309
|
|
|
* |
310
|
|
|
* @param $girdField GridField |
311
|
|
|
* |
312
|
|
|
* @return string HTML |
313
|
|
|
**/ |
314
|
|
|
public function getHTMLFragments($gridField) |
315
|
|
|
{ |
316
|
|
|
$forTemplate = ArrayData::create([ |
317
|
|
|
'Fields' => FieldList::create() |
318
|
|
|
]); |
319
|
|
|
|
320
|
|
|
$searchField = TextField::create( |
321
|
|
|
'gridfield_relationsearch', |
322
|
|
|
_t('SilverStripe\\Forms\\GridField\\GridField.RelationSearch', "Relation search") |
323
|
|
|
)->setAttribute('data-search-url', Controller::join_links($gridField->Link('search'))) |
324
|
|
|
->setAttribute( |
325
|
|
|
'placeholder', |
326
|
|
|
_t( |
327
|
|
|
__CLASS__ . ".TypeToAdd", |
328
|
|
|
"Type to add by {Filters} or {Title}", |
329
|
|
|
"Inform the user what to add based on", |
330
|
|
|
[ |
331
|
|
|
"Filters" => implode(", ", $this->getFilterFields()), |
332
|
|
|
"Title" => $this->getCreateField() |
333
|
|
|
] |
334
|
|
|
) |
335
|
|
|
)->addExtraClass('relation-search no-change-track action_gridfield_relationsearch'); |
336
|
|
|
|
337
|
|
|
$findAction = GridField_FormAction::create( |
338
|
|
|
$gridField, |
339
|
|
|
'gridfield_relationfind', |
340
|
|
|
_t('SilverStripe\\Forms\\GridField\\GridField.Find', "Find"), |
341
|
|
|
'find', |
342
|
|
|
'find' |
343
|
|
|
)->setAttribute('data-icon', 'relationfind') |
344
|
|
|
->addExtraClass('action_gridfield_relationfind'); |
345
|
|
|
|
346
|
|
|
$addAction = GridField_FormAction::create( |
347
|
|
|
$gridField, |
348
|
|
|
'gridfield_relationadd', |
349
|
|
|
_t(__CLASS__ . 'Add', "Add"), |
350
|
|
|
'addto', |
351
|
|
|
'addto' |
352
|
|
|
)->setAttribute('data-icon', 'plus-circled') |
353
|
|
|
->addExtraClass('btn btn-primary font-icon-plus-circled action_gridfield_relationadd'); |
354
|
|
|
|
355
|
|
|
// If an object is not found, disable the action |
356
|
|
|
if (!is_int($gridField->State->GridFieldAddRelation(null))) { |
357
|
|
|
$addAction->setReadonly(true); |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
$forTemplate->Fields->push($searchField); |
361
|
|
|
$forTemplate->Fields->push($findAction); |
362
|
|
|
$forTemplate->Fields->push($addAction); |
363
|
|
|
|
364
|
|
|
if ($form = $gridField->getForm()) { |
365
|
|
|
$forTemplate->Fields->setForm($form); |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
$template = SSViewer::get_templates_by_class($this, '', __CLASS__); |
369
|
|
|
return [ |
370
|
|
|
$this->targetFragment => $forTemplate->renderWith($template) |
371
|
|
|
]; |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
/** |
375
|
|
|
* Returns a json array of a search results that can be used by for |
376
|
|
|
* example Jquery.ui.autosuggestion |
377
|
|
|
* |
378
|
|
|
* @param GridField $gridField |
379
|
|
|
* @param SS_HTTPRequest $request |
|
|
|
|
380
|
|
|
*/ |
381
|
|
|
public function doSearch($gridField, $request) |
382
|
|
|
{ |
383
|
|
|
$source_class = $this->getSourceClass(); |
384
|
|
|
$search_filter = $this->getSearchFilter(); |
385
|
|
|
$params = []; |
386
|
|
|
|
387
|
|
|
// Do we have filter fields setup? |
388
|
|
|
if ($this->getAutocompleteFields()) { |
389
|
|
|
$search_fields = $this->getAutocompleteFields(); |
390
|
|
|
} else { |
391
|
|
|
$search_fields = $this->scaffoldSearchFields($source_class); |
392
|
|
|
} |
393
|
|
|
|
394
|
|
|
if (!$search_fields) { |
395
|
|
|
throw new LogicException( |
396
|
|
|
sprintf( |
397
|
|
|
'GridFieldAddExistingAutocompleter: No searchable fields could be found for class "%s"', |
398
|
|
|
$source_class |
399
|
|
|
) |
400
|
|
|
); |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
foreach ($search_fields as $search_field) { |
404
|
|
|
$name = (strpos($search_field, ':') !== false) ? $search_field : $search_field . ":" . $search_filter; |
405
|
|
|
$params[$name] = $request->getVar('gridfield_relationsearch'); |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
$json = []; |
409
|
|
|
Config::nest(); |
410
|
|
|
|
411
|
|
|
if (class_exists($source_class)) { |
412
|
|
|
$results = DataList::create($source_class) |
413
|
|
|
->filterAny($params) |
414
|
|
|
->sort(strtok($search_fields[0], ':'), 'ASC') |
415
|
|
|
->limit($this->getResultsLimit()); |
416
|
|
|
|
417
|
|
|
$originalSourceFileComments = SSViewer::config()->get('source_file_comments'); |
418
|
|
|
|
419
|
|
|
SSViewer::config()->update('source_file_comments', false); |
420
|
|
|
$viewer = SSViewer::fromString($this->resultsFormat); |
421
|
|
|
|
422
|
|
|
foreach ($results as $result) { |
423
|
|
|
$title = Convert::html2raw($viewer->process($result)); |
424
|
|
|
$json[] = [ |
425
|
|
|
'label' => $title, |
426
|
|
|
'value' => $title, |
427
|
|
|
'id' => $title, |
428
|
|
|
]; |
429
|
|
|
} |
430
|
|
|
|
431
|
|
|
SSViewer::config()->update('source_file_comments', $originalSourceFileComments); |
432
|
|
|
} |
433
|
|
|
|
434
|
|
|
Config::unnest(); |
435
|
|
|
$response = new HTTPResponse(json_encode($json)); |
436
|
|
|
$response->addHeader('Content-Type', 'application/json'); |
437
|
|
|
return $response; |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
/** |
441
|
|
|
* Returns the database field for which we'll add the new data object. |
442
|
|
|
* |
443
|
|
|
* @return string |
444
|
|
|
**/ |
445
|
|
|
public function getDataObjectField() |
446
|
|
|
{ |
447
|
|
|
return $this->dataObjectField; |
448
|
|
|
} |
449
|
|
|
|
450
|
|
|
/** |
451
|
|
|
* Set the database field. |
452
|
|
|
* |
453
|
|
|
* @param $field string |
454
|
|
|
**/ |
455
|
|
|
public function setDataObjectField($field) |
456
|
|
|
{ |
457
|
|
|
$this->dataObjectField = (string) $field; |
458
|
|
|
} |
459
|
|
|
} |
460
|
|
|
|