1 | <?php |
||
2 | |||
3 | namespace SilverShop\Model\Variation; |
||
4 | |||
5 | use SilverShop\Cart\ShoppingCart; |
||
6 | use SilverShop\Model\Buyable; |
||
7 | use SilverShop\Model\Order; |
||
8 | use SilverShop\Page\Product; |
||
9 | use SilverStripe\AssetAdmin\Forms\UploadField; |
||
0 ignored issues
–
show
|
|||
10 | use SilverStripe\Assets\Image; |
||
11 | use SilverStripe\Forms\FieldList; |
||
12 | use SilverStripe\Forms\LiteralField; |
||
13 | use SilverStripe\Forms\TextField; |
||
14 | use SilverStripe\ORM\ArrayList; |
||
15 | use SilverStripe\ORM\DataObject; |
||
16 | use SilverStripe\ORM\FieldType\DBCurrency; |
||
17 | use SilverStripe\ORM\FieldType\DBDecimal; |
||
18 | use SilverStripe\ORM\ManyManyList; |
||
19 | use SilverStripe\Versioned\Versioned; |
||
20 | |||
21 | /** |
||
22 | * Product Variation |
||
23 | * |
||
24 | * Provides a means for specifying many variations on a product. |
||
25 | * Used in combination with ProductAttributes, such as color, size. |
||
26 | * A variation will specify one particular combination, such as red, and large. |
||
27 | * |
||
28 | * @property string $InternalItemID |
||
29 | * @property DBCurrency $Price |
||
30 | * @property DBDecimal $Weight |
||
31 | * @property DBDecimal $Height |
||
32 | * @property DBDecimal $Width |
||
33 | * @property DBDecimal $Depth |
||
34 | * @property int $ProductID |
||
35 | * @property int $ImageID |
||
36 | * @method Product Product() |
||
37 | * @method Image Image() |
||
38 | * @method AttributeValue[]|ManyManyList AttributeValues() |
||
39 | */ |
||
40 | class Variation extends DataObject implements Buyable |
||
41 | { |
||
42 | private static $db = [ |
||
43 | 'InternalItemID' => 'Varchar(30)', |
||
44 | 'Price' => 'Currency(19,4)', |
||
45 | |||
46 | //physical properties |
||
47 | //TODO: Move to an extension |
||
48 | 'Weight' => 'Decimal(12,5)', |
||
49 | 'Height' => 'Decimal(12,5)', |
||
50 | 'Width' => 'Decimal(12,5)', |
||
51 | 'Depth' => 'Decimal(12,5)' |
||
52 | ]; |
||
53 | |||
54 | private static $has_one = [ |
||
55 | 'Product' => Product::class, |
||
56 | 'Image' => Image::class |
||
57 | ]; |
||
58 | |||
59 | private static $owns = [ |
||
60 | 'Image' |
||
61 | ]; |
||
62 | |||
63 | private static $many_many = [ |
||
64 | 'AttributeValues' => AttributeValue::class |
||
65 | ]; |
||
66 | |||
67 | private static $casting = [ |
||
68 | 'Title' => 'Text', |
||
69 | 'Price' => 'Currency' |
||
70 | ]; |
||
71 | |||
72 | private static $versioning = [ |
||
73 | 'Live' |
||
74 | ]; |
||
75 | |||
76 | private static $extensions = [ |
||
77 | Versioned::class . '.versioned' |
||
78 | ]; |
||
79 | |||
80 | private static $summary_fields = [ |
||
81 | 'InternalItemID' => 'Product Code', |
||
82 | //'Product.Title' => 'Product', |
||
83 | 'Title' => 'Variation', |
||
84 | 'Price' => 'Price' |
||
85 | ]; |
||
86 | |||
87 | private static $searchable_fields = [ |
||
88 | 'Product.Title', |
||
89 | 'InternalItemID' |
||
90 | ]; |
||
91 | |||
92 | private static $indexes = [ |
||
93 | 'InternalItemID' => true, |
||
94 | 'LastEdited' => true |
||
95 | ]; |
||
96 | |||
97 | private static $singular_name = 'Variation'; |
||
98 | |||
99 | private static $plural_name = 'Variations'; |
||
100 | |||
101 | private static $default_sort = 'InternalItemID'; |
||
102 | |||
103 | private static $order_item = OrderItem::class; |
||
104 | |||
105 | private static $table_name = 'SilverShop_Variation'; |
||
106 | |||
107 | /** |
||
108 | * @config |
||
109 | * @var bool |
||
110 | */ |
||
111 | private static $title_has_label = true; |
||
112 | |||
113 | /** |
||
114 | * @config |
||
115 | * @var string |
||
116 | */ |
||
117 | private static $title_separator = ':'; |
||
118 | |||
119 | /** |
||
120 | * @config |
||
121 | * @var string |
||
122 | */ |
||
123 | private static $title_glue = ', '; |
||
124 | |||
125 | public function getCMSFields() |
||
126 | { |
||
127 | $fields = FieldList::create( |
||
128 | TextField::create('InternalItemID', _t('SilverShop\Page\Product.Code', 'Product Code')), |
||
129 | TextField::create('Price', _t('SilverShop\Page\Product.db_BasePrice', 'Price')) |
||
130 | ); |
||
131 | //add attributes dropdowns |
||
132 | $attributes = $this->Product()->VariationAttributeTypes(); |
||
133 | if ($attributes->exists()) { |
||
134 | foreach ($attributes as $attribute) { |
||
135 | if ($field = $attribute->getDropDownField()) { |
||
136 | if ($value = $this->AttributeValues()->find('TypeID', $attribute->ID)) { |
||
137 | $field->setValue($value->ID); |
||
138 | } |
||
139 | $fields->push($field); |
||
140 | } else { |
||
141 | $fields->push( |
||
142 | LiteralField::create( |
||
143 | 'novalues' . $attribute->Name, |
||
144 | '<p class="message warning">' . |
||
145 | _t( |
||
146 | __CLASS__ . '.NoAttributeValuesMessage', |
||
147 | '{attribute} has no values to choose from. You can create them in the "Products" > "Product Attribute Type" section of the CMS.', |
||
148 | 'Warning that will be shown if an attribute doesn\'t have any values', |
||
149 | ['attribute' => $attribute->Name] |
||
150 | ) . |
||
151 | '</p>' |
||
152 | ) |
||
153 | ); |
||
154 | } |
||
155 | //TODO: allow setting custom values here, rather than visiting the products section |
||
156 | } |
||
157 | } else { |
||
158 | $fields->push( |
||
159 | LiteralField::create( |
||
160 | 'savefirst', |
||
161 | '<p class="message warning">' . |
||
162 | _t( |
||
163 | __CLASS__ . '.MustSaveFirstMessage', |
||
164 | 'You can choose variation attributes after saving for the first time, if they exist.' |
||
165 | ) . |
||
166 | '</p>' |
||
167 | ) |
||
168 | ); |
||
169 | } |
||
170 | $fields->push( |
||
171 | UploadField::create('Image', _t('SilverShop\Page\Product.Image', 'Product Image')) |
||
172 | ); |
||
173 | |||
174 | //physical measurement units |
||
175 | $fieldSubstitutes = [ |
||
176 | 'LengthUnit' => Product::config()->length_unit |
||
177 | ]; |
||
178 | |||
179 | //physical measurements |
||
180 | $fields->push( |
||
181 | TextField::create( |
||
182 | 'Weight', |
||
183 | _t( |
||
184 | 'SilverShop\Page\Product.WeightWithUnit', |
||
185 | 'Weight ({WeightUnit})', |
||
186 | '', |
||
187 | [ |
||
188 | 'WeightUnit' => Product::config()->weight_unit |
||
189 | ] |
||
190 | ), |
||
191 | '', |
||
192 | 12 |
||
193 | ) |
||
194 | ); |
||
195 | |||
196 | $fields->push( |
||
197 | TextField::create( |
||
198 | 'Height', |
||
199 | _t('SilverShop\Page\Product.HeightWithUnit', 'Height ({LengthUnit})', '', $fieldSubstitutes), |
||
200 | '', |
||
201 | 12 |
||
202 | ) |
||
203 | ); |
||
204 | |||
205 | $fields->push( |
||
206 | TextField::create( |
||
207 | 'Width', |
||
208 | _t('SilverShop\Page\Product.WidthWithUnit', 'Width ({LengthUnit})', '', $fieldSubstitutes), |
||
209 | '', |
||
210 | 12 |
||
211 | ) |
||
212 | ); |
||
213 | |||
214 | $fields->push( |
||
215 | TextField::create( |
||
216 | 'Depth', |
||
217 | _t('SilverShop\Page\Product.DepthWithUnit', 'Depth ({LengthUnit})', '', $fieldSubstitutes), |
||
218 | '', |
||
219 | 12 |
||
220 | ) |
||
221 | ); |
||
222 | |||
223 | $this->extend('updateCMSFields', $fields); |
||
224 | |||
225 | return $fields; |
||
226 | } |
||
227 | |||
228 | /** |
||
229 | * Save selected attributes - somewhat of a hack. |
||
230 | */ |
||
231 | public function onBeforeWrite() |
||
232 | { |
||
233 | parent::onBeforeWrite(); |
||
234 | |||
235 | if (isset($_POST['ProductAttributes']) && is_array($_POST['ProductAttributes'])) { |
||
236 | $this->AttributeValues()->setByIDList(array_values($_POST['ProductAttributes'])); |
||
237 | } |
||
238 | |||
239 | $img = $this->Image(); |
||
240 | |||
241 | if ($img && $img->exists()) { |
||
242 | $img->doPublish(); |
||
243 | } |
||
244 | } |
||
245 | |||
246 | public function getTitle() |
||
247 | { |
||
248 | $values = $this->AttributeValues(); |
||
249 | if ($values->exists()) { |
||
250 | $labelvalues = array(); |
||
251 | foreach ($values as $value) { |
||
252 | if (self::config()->title_has_label) { |
||
253 | $labelvalues[] = $value->Type()->Label . self::config()->title_separator . $value->Value; |
||
254 | } else { |
||
255 | $labelvalues[] = $value->Value; |
||
256 | } |
||
257 | } |
||
258 | |||
259 | $title = implode(self::config()->title_glue, $labelvalues); |
||
260 | } |
||
261 | $this->extend('updateTitle', $title); |
||
262 | |||
263 | return $title; |
||
264 | } |
||
265 | |||
266 | public function getCategoryIDs() |
||
267 | { |
||
268 | return $this->Product() ? $this->Product()->getCategoryIDs() : array(); |
||
269 | } |
||
270 | |||
271 | public function getCategories() |
||
272 | { |
||
273 | return $this->Product() ? $this->Product()->getCategories() : ArrayList::create(); |
||
274 | } |
||
275 | |||
276 | public function canPurchase($member = null, $quantity = 1) |
||
277 | { |
||
278 | $allowpurchase = false; |
||
279 | if ($product = $this->Product()) { |
||
280 | $allowpurchase = |
||
281 | ($this->sellingPrice() > 0 || Product::config()->allow_zero_price) && $product->AllowPurchase; |
||
282 | } |
||
283 | |||
284 | $permissions = $this->extend('canPurchase', $member, $quantity); |
||
285 | $permissions[] = $allowpurchase; |
||
286 | return min($permissions); |
||
287 | } |
||
288 | |||
289 | /* |
||
290 | * Returns if the product variation is already in the shopping cart. |
||
291 | * @return boolean |
||
292 | */ |
||
293 | public function IsInCart() |
||
294 | { |
||
295 | return $this->Item() && $this->Item()->Quantity > 0; |
||
296 | } |
||
297 | |||
298 | /* |
||
299 | * Returns the order item which contains the product variation |
||
300 | * @return OrderItem |
||
301 | */ |
||
302 | public function Item() |
||
303 | { |
||
304 | $filter = array(); |
||
305 | $this->extend('updateItemFilter', $filter); |
||
306 | $item = ShoppingCart::singleton()->get($this, $filter); |
||
307 | if (!$item) { |
||
308 | //return dummy item so that we can still make use of Item |
||
309 | $item = $this->createItem(0); |
||
310 | } |
||
311 | $this->extend('updateDummyItem', $item); |
||
312 | return $item; |
||
313 | } |
||
314 | |||
315 | public function addLink() |
||
316 | { |
||
317 | return $this->Item()->addLink($this->ProductID, $this->ID); |
||
318 | } |
||
319 | |||
320 | public function createItem($quantity = 1, $filter = array()) |
||
321 | { |
||
322 | $orderitem = self::config()->order_item; |
||
323 | $item = new $orderitem(); |
||
324 | $item->ProductID = $this->ProductID; |
||
325 | $item->ProductVariationID = $this->ID; |
||
326 | //$item->ProductVariationVersion = $this->Version; |
||
327 | if ($filter) { |
||
328 | //TODO: make this a bit safer, perhaps intersect with allowed fields |
||
329 | $item->update($filter); |
||
330 | } |
||
331 | $item->Quantity = $quantity; |
||
332 | return $item; |
||
333 | } |
||
334 | |||
335 | public function sellingPrice() |
||
336 | { |
||
337 | $price = $this->Price; |
||
338 | $this->extend('updateSellingPrice', $price); |
||
339 | |||
340 | //prevent negative values |
||
341 | $price = $price < 0 ? 0 : $price; |
||
342 | |||
343 | // NOTE: Ideally, this would be dependent on the locale but as of |
||
344 | // now the Silverstripe Currency field type has 2 hardcoded all over |
||
345 | // the place. In the mean time there is an issue where the displayed |
||
346 | // unit price can not exactly equal the multiplied price on an order |
||
347 | // (i.e. if the calculated price is 3.145 it will display as 3.15. |
||
348 | // so if I put 10 of them in my cart I will expect the price to be |
||
349 | // 31.50 not 31.45). |
||
350 | return round($price, Order::config()->rounding_precision); |
||
351 | } |
||
352 | } |
||
353 |
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"]
, you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths