• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hivesolutions / budy / 960

pending completion
960

push

travis-ci

joamag
conditional sku field support

2 of 2 new or added lines in 1 file covered. (100.0%)

3722 of 5112 relevant lines covered (72.81%)

18.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

50.52
/src/budy/models/product.py
1
#!/usr/bin/python
2
# -*- coding: utf-8 -*-
3

4
# Hive Budy
5
# Copyright (c) 2008-2018 Hive Solutions Lda.
6
#
7
# This file is part of Hive Budy.
8
#
9
# Hive Budy is free software: you can redistribute it and/or modify
10
# it under the terms of the Apache License as published by the Apache
11
# Foundation, either version 2.0 of the License, or (at your option) any
12
# later version.
13
#
14
# Hive Budy is distributed in the hope that it will be useful,
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
# Apache License for more details.
18
#
19
# You should have received a copy of the Apache License along with
20
# Hive Budy. If not, see <http://www.apache.org/licenses/>.
21

22
__author__ = "João Magalhães <joamag@hive.pt>"
26✔
23
""" The author(s) of the module """
24

25
__version__ = "1.0.0"
26✔
26
""" The version of the module """
27

28
__revision__ = "$LastChangedRevision$"
26✔
29
""" The revision number of the module """
30

31
__date__ = "$LastChangedDate$"
26✔
32
""" The last change date of the module """
33

34
__copyright__ = "Copyright (c) 2008-2018 Hive Solutions Lda."
26✔
35
""" The copyright for the module """
36

37
__license__ = "Apache License, Version 2.0"
26✔
38
""" The license for the module """
39

40
import json
26✔
41
import commons
26✔
42

43
import appier
26✔
44
import appier_extras
26✔
45

46
from . import base
26✔
47
from . import bundle
26✔
48

49
class Product(base.BudyBase):
26✔
50

51
    GENDER_S = {
26✔
52
        "Male" : "Male",
53
        "Female" : "Female",
54
        "Both" : "Both"
55
    }
56
    """ The dictionary that maps the multiple gender
57
    enumeration values with their string representation """
58

59
    short_description = appier.field(
26✔
60
        index = "hashed",
61
        default = True
62
    )
63

64
    product_id = appier.field(
26✔
65
        index = "hashed",
66
        description = "Product ID",
67
        observations = """The primary identifier of the
68
        product, should be globally unique"""
69
    )
70

71
    supplier_code = appier.field(
26✔
72
        index = "hashed",
73
        observations = """The identifier used by the
74
        (back) supplier of the product, should be considered
75
        a more unique way of identifying a product"""
76
    )
77

78
    sku = appier.field(
26✔
79
        index = "hashed",
80
        description = "SKU",
81
        observations = """The main identifier to be used for
82
        the keeping of the internal reference SKU (Stock Keeping
83
        Unit), should be considered the public way of representing
84
        the product"""
85
    )
86

87
    upc = appier.field(
26✔
88
        index = "hashed",
89
        description = "UPC",
90
        observations = """The standard Universal Product Code
91
        (UPC) value for this product"""
92
    )
93

94
    ean = appier.field(
26✔
95
        index = "hashed",
96
        description = "EAN",
97
        observations = """The standard European Article Number
98
        (EAN) value for this product"""
99
    )
100

101
    gender = appier.field(
26✔
102
        index = "hashed",
103
        meta = "enum",
104
        enum = GENDER_S
105
    )
106

107
    quantity_hand = appier.field(
26✔
108
        type = commons.Decimal,
109
        index = True
110
    )
111

112
    quantity_reserved = appier.field(
26✔
113
        type = commons.Decimal,
114
        index = True
115
    )
116

117
    price = appier.field(
26✔
118
        type = commons.Decimal,
119
        index = True,
120
        initial = commons.Decimal(0.0),
121
        observations = """Main retail price to be used for
122
        a possible sale transaction of the product (includes taxes)"""
123
    )
124

125
    price_compare = appier.field(
26✔
126
        type = commons.Decimal,
127
        index = True,
128
        initial = commons.Decimal(0.0),
129
        observations = """The price that is going to be used
130
        as the base for discount calculation purposes"""
131
    )
132

133
    taxes = appier.field(
26✔
134
        type = commons.Decimal,
135
        index = True,
136
        initial = commons.Decimal(0.0)
137
    )
138

139
    currency = appier.field(
26✔
140
        index = "hashed"
141
    )
142

143
    order = appier.field(
26✔
144
        type = int,
145
        index = "hashed"
146
    )
147

148
    tag = appier.field()
26✔
149

150
    tag_description = appier.field()
26✔
151

152
    price_provider = appier.field(
26✔
153
        index = True
154
    )
155

156
    price_url = appier.field(
26✔
157
        index = "hashed",
158
        meta = "url",
159
        description = "Price URL"
160
    )
161

162
    farfetch_url = appier.field(
26✔
163
        index = "hashed",
164
        meta = "url",
165
        description = "Farfetch URL"
166
    )
167

168
    farfetch_male_url = appier.field(
26✔
169
        index = "hashed",
170
        meta = "url",
171
        description = "Farfetch Male URL"
172
    )
173

174
    farfetch_female_url = appier.field(
26✔
175
        index = "hashed",
176
        meta = "url",
177
        description = "Farfetch Female URL"
178
    )
179

180
    image_url = appier.field(
26✔
181
        index = "hashed",
182
        meta = "image_url",
183
        description = "Image URL"
184
    )
185

186
    thumbnail_url = appier.field(
26✔
187
        index = "hashed",
188
        meta = "image_url",
189
        description = "Thumbnail URL"
190
    )
191

192
    characteristics = appier.field(
26✔
193
        type = list
194
    )
195

196
    labels = appier.field(
26✔
197
        type = list
198
    )
199

200
    brand_s = appier.field(
26✔
201
        index = "hashed"
202
    )
203

204
    season_s = appier.field(
26✔
205
        index = "hashed"
206
    )
207

208
    color_s = appier.field(
26✔
209
        index = "hashed"
210
    )
211

212
    category_s = appier.field(
26✔
213
        index = "hashed"
214
    )
215

216
    collection_s = appier.field(
26✔
217
        index = "hashed"
218
    )
219

220
    colors = appier.field(
26✔
221
        type = appier.references(
222
            "Color",
223
            name = "id"
224
        )
225
    )
226

227
    categories = appier.field(
26✔
228
        type = appier.references(
229
            "Category",
230
            name = "id"
231
        )
232
    )
233

234
    collections = appier.field(
26✔
235
        type = appier.references(
236
            "Collection",
237
            name = "id"
238
        )
239
    )
240

241
    variants = appier.field(
26✔
242
        type = appier.references(
243
            "Product",
244
            name = "id"
245
        )
246
    )
247

248
    images = appier.field(
26✔
249
        type = appier.references(
250
            "Media",
251
            name = "id"
252
        )
253
    )
254

255
    brand = appier.field(
26✔
256
        type = appier.reference(
257
            "Brand",
258
            name = "id"
259
        )
260
    )
261

262
    season = appier.field(
26✔
263
        type = appier.reference(
264
            "Season",
265
            name = "id"
266
        )
267
    )
268

269
    measurements = appier.field(
26✔
270
        type = appier.references(
271
            "Measurement",
272
            name = "id"
273
        )
274
    )
275

276
    compositions = appier.field(
26✔
277
        type = appier.references(
278
            "Composition",
279
            name = "id"
280
        )
281
    )
282

283
    live_model = appier.field(
26✔
284
        type = appier.references(
285
            "LiveModel",
286
            name = "id"
287
        )
288
    )
289

290
    @classmethod
26✔
291
    def validate(cls):
292
        return super(Product, cls).validate() + [
26✔
293
            appier.not_null("short_description"),
294
            appier.not_empty("short_description"),
295

296
            appier.not_null("gender"),
297
            appier.not_empty("gender"),
298

299
            appier.not_null("price"),
300
            appier.gte("price", 0.0),
301

302
            appier.not_null("taxes"),
303
            appier.gte("taxes", 0.0),
304

305
            appier.not_empty("brand_s"),
306

307
            appier.not_empty("season_s"),
308

309
            appier.not_empty("color_s"),
310

311
            appier.not_empty("category_s"),
312

313
            appier.not_empty("collection_s")
314
        ]
315

316
    @classmethod
26✔
317
    def list_names(cls):
318
        return ["id", "product_id", "short_description", "enabled", "gender", "tag", "image_url"]
×
319

320
    @classmethod
26✔
321
    def order_name(cls):
322
        return ["id", -1]
×
323

324
    @classmethod
26✔
325
    def index_names(cls):
326
        return super(Product, cls).index_names() + ["product_id"]
26✔
327

328
    @classmethod
26✔
329
    def token_names(cls):
330
        return super(Product, cls).token_names() + [
26✔
331
            ("short_description", True),
332
            ("product_id", False),
333
            ("supplier_code", False),
334
            ("sku", False),
335
            ("upc", False),
336
            ("ean", False),
337
            ("brand.name", True),
338
            ("season.name", True),
339
            ("characteristics", False),
340
            ("colors.name", True),
341
            ("categories.name", True),
342
            ("collections.name", True),
343
            ("compositions.name", True)
344
        ]
345

346
    @classmethod
26✔
347
    def from_omni(
26✔
348
        cls,
349
        merchandise,
350
        inventory_line = None,
351
        gender = "Both",
352
        currency = "EUR",
353
        force = False
354
    ):
355
        from . import brand
×
356
        from . import color
×
357
        from . import season
×
358
        from . import category
×
359
        from . import collection
×
360
        object_id = merchandise["object_id"]
×
361
        modify_date = merchandise["modify_date"]
×
362
        company_product_code = merchandise["company_product_code"]
×
363
        upc = merchandise["upc"]
×
364
        metadata = merchandise["metadata"] or dict()
×
365
        price_compare = metadata.get("compare_price") or None
×
366
        _color = metadata.get("material") or []
×
367
        _category = metadata.get("category") or []
×
368
        _collection = metadata.get("collection") or []
×
369
        _brand = metadata.get("brand")
×
370
        _season = metadata.get("season")
×
371
        gender = metadata.get("gender") or gender
×
372
        order = metadata.get("order")
×
373
        sku_field = metadata.get("sku_field")
×
374

375
        # verifies if an inventory line has been provided, if that's the case
376
        # it's possible to determine a proper modification date for the product
377
        # taking into account also the modification date of its inventory line
378
        if inventory_line:
×
379
            modify_date_line = inventory_line["modify_date"]
×
380
            if modify_date_line > modify_date: modify_date = modify_date_line
×
381

382
        colors = _color if isinstance(_color, list) else [_color]
×
383
        categories = _category if isinstance(_category, list) else [_category]
×
384
        collections = _collection if isinstance(_collection, list) else [_collection]
×
385
        colors = [color.Color.ensure_s(_color) for _color in colors]
×
386
        categories = [category.Category.ensure_s(_category) for _category in categories]
×
387
        collections = [collection.Collection.ensure_s(_collection) for _collection in collections]
×
388
        if _brand: _brand = brand.Brand.ensure_s(_brand)
×
389
        if _season: _season = season.Season.ensure_s(_season)
×
390
        product = cls.get(product_id = company_product_code, raise_e = False)
×
391
        if not product: product = cls()
×
392
        product.product_id = company_product_code
×
393
        product.supplier_code = upc
×
394
        product.sku = metadata.get(sku_field) or upc or company_product_code
×
395
        product.upc = upc
×
396
        product.short_description = merchandise["name"] or company_product_code
×
397
        product.description = merchandise["description"]
×
398
        product.gender = gender
×
399
        product.price_compare = price_compare
×
400
        product.currency = currency
×
401
        product.order = order
×
402
        product.characteristics = metadata.get("characteristics", [])
×
403
        product.colors = colors
×
404
        product.categories = categories
×
405
        product.collections = collections
×
406
        product.brand = _brand
×
407
        product.season = _season
×
408
        product.meta = dict(
×
409
            object_id = object_id,
410
            modify_date = modify_date
411
        )
412
        if "stock_on_hand" in merchandise or force:
×
413
            product.quantity_hand = merchandise.get("stock_on_hand", 0.0)
×
414
        if "retail_price" in merchandise or force:
×
415
            product.price = merchandise.get("retail_price", 0.0)
×
416
        if "price" in merchandise or force:
×
417
            base_price = product.price if hasattr(product, "price") else 0.0
×
418
            base_price = base_price or 0.0
×
419
            product.taxes = base_price - merchandise.get("price", 0.0)
×
420
        return product
×
421

422
    @classmethod
26✔
423
    @appier.operation(
26✔
424
        name = "Import Omni",
425
        parameters = (
426
            ("Product", "product", "longtext"),
427
        ),
428
        factory = True
429
    )
430
    def import_omni_s(cls, product, safe = True):
26✔
431
        product = json.loads(product)
×
432
        product = cls.from_omni(product)
×
433
        product.save()
×
434
        return product
×
435

436
    @classmethod
26✔
437
    @appier.operation(
26✔
438
        name = "Import CSV",
439
        parameters = (
440
            ("CSV File", "file", "file"),
441
            ("Empty source", "empty", bool, False)
442
        )
443
    )
444
    def import_csv_s(cls, file, empty):
445

446
        def callback(line):
×
447
            from . import color
×
448
            from . import brand
×
449
            from . import season
×
450
            from . import category
×
451
            from . import collection
×
452
            from . import composition
×
453
            from . import measurement
×
454

455
            description,\
×
456
            short_description,\
457
            product_id,\
458
            gender,\
459
            price,\
460
            order,\
461
            tag,\
462
            tag_description,\
463
            farfetch_url,\
464
            farfetch_male_url,\
465
            farfetch_female_url,\
466
            colors,\
467
            categories,\
468
            collections, \
469
            variants,\
470
            _brand,\
471
            _season,\
472
            measurements,\
473
            compositions, \
474
            price_provider, \
475
            price_url = line
476

477
            product_id = product_id or None
×
478
            price = float(price) if price else None
×
479
            order = int(order) if order else None
×
480
            tag = tag or None
×
481
            tag_description = tag_description or None
×
482
            farfetch_url = farfetch_url or None
×
483
            farfetch_male_url = farfetch_male_url or None
×
484
            farfetch_female_url = farfetch_female_url or None
×
485
            price_provider = price_provider or None
×
486
            price_url = price_url or None
×
487

488
            colors = colors.split(";") if colors else []
×
489
            colors = color.Color.find(name = {"$in" : colors})
×
490

491
            categories = categories.split(";") if categories else []
×
492
            categories = category.Category.find(name = {"$in" : categories})
×
493

494
            collections = collections.split(";") if collections else []
×
495
            collections = collection.Collection.find(name = {"$in" : collections})
×
496

497
            variants = variants.split(";") if variants else []
×
498
            variants = Product.find(product_id = {"$in" : variants})
×
499

500
            _brand = brand.Brand.find(name = _brand) if _brand else None
×
501
            _season = season.Season.find(name = _season) if _season else None
×
502

503
            measurements = measurements.split(";") if measurements else []
×
504
            measurements = measurement.Measurement.find(name = {"$in" : measurements})
×
505

506
            compositions = compositions.split(";") if compositions else []
×
507
            compositions = composition.Composition.find(name = {"$in" : compositions})
×
508

509
            product = cls(
×
510
                description = description,
511
                short_description = short_description,
512
                product_id = product_id,
513
                gender = gender,
514
                price = price,
515
                order = order,
516
                tag = tag,
517
                tag_description = tag_description,
518
                farfetch_url = farfetch_url,
519
                farfetch_male_url = farfetch_male_url,
520
                farfetch_female_url = farfetch_female_url,
521
                colors = colors,
522
                categories = categories,
523
                collections = collections,
524
                variants = variants,
525
                brand = _brand,
526
                season = _season,
527
                measurements = measurements,
528
                compositions = compositions,
529
                price_provider = price_provider,
530
                price_url = price_url
531
            )
532
            product.save()
×
533

534
        if empty: cls.delete_c()
×
535
        cls._csv_import(file, callback)
×
536

537
    @classmethod
26✔
538
    @appier.link(name = "Export Simple")
26✔
539
    def simple_csv_url(cls, absolute = False):
26✔
540
        return appier.get_app().url_for(
×
541
            "product_api.simple_csv",
542
            absolute = absolute
543
        )
544

545
    @classmethod
26✔
546
    def _build(cls, model, map):
547
        super(Product, cls)._build(model, map)
26✔
548

549
        # retrieves the multiple values from the product model that are
550
        # going to be used in the calculated attributes construction
551
        price = model.get("price", None)
26✔
552
        price_compare = model.get("price_compare", None)
26✔
553
        labels = model.get("labels", ())
26✔
554

555
        # verifies if the current product is considered to be a discounted
556
        # one, that happens when the price compare is greater than price
557
        is_discounted = price and price_compare and price_compare > price
26✔
558

559
        # calculates the shipping costs taking into account if the price
560
        # is currently defined for the product defaulting to none otherwise
561
        if price == None: shipping_cost = None
26✔
562
        else: shipping_cost = bundle.Bundle.eval_shipping(price, 0.0, 1.0, None)
26✔
563

564
        # sets the multiple attributes of the product that describe it through
565
        # calculated attributes (as expected by model retriever)
566
        model["is_discounted"] = is_discounted
26✔
567
        model["is_discounted_s"] = "discounted" if is_discounted else "not-discounted"
26✔
568
        model["shipping_cost"] = shipping_cost
26✔
569
        for label in labels:
26✔
570
            model[label] = label
26✔
571
            model[label + "_s"] = "true"
26✔
572

573
    def pre_validate(self):
26✔
574
        base.BudyBase.pre_validate(self)
26✔
575
        self.build_images()
26✔
576
        self.build_names()
26✔
577
        self.build_labels()
26✔
578

579
    def pre_save(self):
26✔
580
        base.BudyBase.pre_save(self)
26✔
581
        if not self.measurements: return
26✔
582
        quantities_hand = [measurement.quantity_hand or 0.0 for measurement in\
26✔
583
            self.measurements if hasattr(measurement, "quantity_hand") and\
584
            not measurement.quantity_hand == None]
585
        prices = [measurement.price or 0.0 for measurement in\
26✔
586
            self.measurements if hasattr(measurement, "price") and\
587
            not measurement.price == None]
588
        self.quantity_hand = sum(quantities_hand) if quantities_hand else None
26✔
589
        self.price = max(prices) if prices else 0.0
26✔
590

591
    def build_images(self):
26✔
592
        thumbnail = self.get_image(size = "thumbnail", order = 1)
26✔
593
        thumbnail = thumbnail or self.get_image(size = "thumbnail")
26✔
594
        image = self.get_image(size = "large", order = 1)
26✔
595
        image = image or self.get_image(size = "large")
26✔
596
        self.thumbnail_url = thumbnail.get_url() if thumbnail else None
26✔
597
        self.image_url = image.get_url() if image else None
26✔
598

599
    def build_names(self):
26✔
600
        self.brand_s = self.brand.name if self.brand else None
26✔
601
        self.season_s = self.season.name if self.season else None
26✔
602
        self.color_s = self.colors[0].name if self.colors else None
26✔
603
        self.category_s = self.categories[0].name if self.categories else None
26✔
604
        self.collection_s = self.collections[0].name if self.collections else None
26✔
605

606
    def build_labels(self):
26✔
607
        self._reset_labels()
26✔
608
        self._build_labels(self.colors)
26✔
609
        self._build_labels(self.categories)
26✔
610
        self._build_labels(self.collections)
26✔
611

612
    def related(self, limit = 6, available = True, enabled = True):
26✔
613
        cls = self.__class__
×
614
        kwargs = dict()
×
615
        if available: kwargs["quantity_hand"] = {"$gt" : 0}
×
616
        if self.collections: kwargs["collections"] = {"$in" : [self.collections[0].id]}
×
617
        elif self.categories: kwargs["categories"] = {"$in" : [self.categories[0].id]}
×
618
        elif self.colors: kwargs["colors"] = {"$in" : [self.colors[0].id]}
×
619
        kwargs["id"] = {"$nin" : [self.id]}
×
620
        kwargs["sort"] = [("id", 1)]
×
621
        count = cls.count(**kwargs)
×
622
        skip = self._get_offset(count, limit, kwargs = kwargs)
×
623
        delta = skip + limit - count
×
624
        if delta > 0: skip = count - skip - delta
×
625
        if skip < 0: skip = 0
×
626
        find = cls.find_e if enabled else cls.find
×
627
        products = find(
×
628
            eager = ("images",),
629
            skip = skip,
630
            limit = limit,
631
            map = True,
632
            **kwargs
633
        )
634
        return products
×
635

636
    def get_measurement(self, value, name = None):
26✔
637
        for measurement in self.measurements:
26✔
638
            if not measurement: continue
26✔
639
            if not hasattr(measurement, "value"): continue
26✔
640
            if not hasattr(measurement, "name"): continue
26✔
641
            if not measurement.value == value: continue
26✔
642
            if not measurement.name == name: continue
26✔
643
            return measurement
26✔
644
        return None
×
645

646
    def get_price(
26✔
647
        self,
648
        currency = None,
649
        country = None,
650
        attributes = None
651
    ):
652
        if not self.price_provider: return self.price
26✔
653
        method = getattr(self, "get_price_%s" % self.price_provider)
×
654
        return method(
×
655
            currency = currency,
656
            country = country,
657
            attributes = attributes
658
        )
659

660
    def get_taxes(
26✔
661
        self,
662
        currency = None,
663
        country = None,
664
        attributes = None
665
    ):
666
        if not self.price_provider: return self.taxes
26✔
667
        method = getattr(self, "get_taxes_%s" % self.price_provider, None)
×
668
        if not method: return self.taxes
×
669
        return method(
×
670
            currency = currency,
671
            country = country,
672
            attributes = attributes
673
        )
674

675
    def get_price_ripe(
26✔
676
        self,
677
        currency = None,
678
        country = None,
679
        attributes = None
680
    ):
681
        if not self.price_url: return self.price
×
682

683
        result = self.get_availability_ripe(
×
684
            currency = currency,
685
            country = country,
686
            attributes = attributes
687
        )
688
        total = result["total"]
×
689
        return total["price_final"]
×
690

691
    def get_taxes_ripe(
26✔
692
        self,
693
        currency = None,
694
        country = None,
695
        attributes = None
696
    ):
697
        if not self.price_url: return self.price
×
698

699
        result = self.get_availability_ripe(
×
700
            currency = currency,
701
            country = country,
702
            attributes = attributes
703
        )
704
        total = result["total"]
×
705
        return total["ddp"] + total["vat"]
×
706

707
    def get_availability_ripe(
26✔
708
        self,
709
        currency = None,
710
        country = None,
711
        attributes = None
712
    ):
713
        attributes_m = json.loads(attributes)
×
714
        p = []
×
715
        parts = attributes_m.get("parts", {})
×
716
        embossing = attributes_m.get("embossing", None)
×
717
        letters = attributes_m.get("letters", None)
×
718

719
        for key, value in appier.legacy.iteritems(parts):
×
720
            material = value["material"]
×
721
            color = value["color"]
×
722
            triplet = "%s:%s:%s" % (key, material, color)
×
723
            p.append(triplet)
×
724

725
        params = dict(
×
726
            product_id = self.product_id,
727
            p = p
728
        )
729
        if currency: params["currency"] = currency
×
730
        if country: params["country"] = country
×
731
        if embossing: params["embossing"] = embossing
×
732
        if letters: params["letters"] = letters
×
733

734
        result = appier.get(
×
735
            self.price_url,
736
            params = params
737
        )
738
        return result
×
739

740
    def get_currency(self, currency = None):
26✔
741
        if not self.price_provider: return self.currency or currency
26✔
742
        method = getattr(self, "get_currency_%s" % self.price_provider)
×
743
        return method(currency = currency)
×
744

745
    def get_currency_ripe(self, currency = None):
26✔
746
        return self.currency or currency
×
747

748
    def get_image(self, size = None, order = None):
26✔
749
        for image in self.images:
26✔
750
            is_size = size == None or image.size == size
26✔
751
            if not is_size: continue
26✔
752
            is_order = order == None or image.order == order
×
753
            if not is_order: continue
×
754
            return image
×
755
        return None
26✔
756

757
    def get_size(self, currency = None, country = None, attributes = None):
26✔
758
        if not self.price_provider: return None, None
26✔
759
        method = getattr(self, "get_size_%s" % self.price_provider)
×
760
        return method(
×
761
            country = country,
762
            attributes = attributes
763
        )
764

765
    def get_size_ripe(
26✔
766
        self,
767
        currency = None,
768
        country = None,
769
        attributes = None
770
    ):
771
        attributes_m = json.loads(attributes)
×
772
        size = attributes_m["size"]
×
773
        scale = attributes_m["scale"]
×
774
        gender = attributes_m["gender"]
×
775

776
        if gender == "male": converter = lambda native: ((native - 17) / 2) + 36
×
777
        else: converter = lambda native: ((native - 17) / 2) + 34
×
778

779
        return converter(size), scale
×
780

781
    @appier.operation(
26✔
782
        name = "Add Collection",
783
        parameters = (
784
            (
785
                "Collection",
786
                "collection",
787
                appier.reference("Collection", name = "id")
788
            ),
789
        )
790
    )
791
    def add_collection_s(self, collection):
792
        if not collection: return
26✔
793
        if collection in self.collections: return
26✔
794
        self.collections.append(collection)
26✔
795
        self.save()
26✔
796

797
    @appier.operation(
26✔
798
        name = "Remove Collection",
799
        parameters = (
800
            (
801
                "Collection",
802
                "collection",
803
                appier.reference("Collection", name = "id")
804
            ),
805
        )
806
    )
807
    def remove_collection_s(self, collection):
808
        if not collection: return
×
809
        if not collection in self.collections: return
×
810
        self.collections.remove(collection)
×
811
        self.save()
×
812

813
    @appier.operation(
26✔
814
        name = "Add Image",
815
        parameters = (
816
            (
817
                "Image",
818
                "image",
819
                appier.reference("Media", name = "id")
820
            ),
821
        )
822
    )
823
    def add_image_s(self, image):
824
        if not image: return
26✔
825
        if image in self.images: return
26✔
826
        self.images.append(image)
26✔
827
        self.save()
26✔
828

829
    @appier.operation(
26✔
830
        name = "Remove Image",
831
        parameters = (
832
            (
833
                "Image",
834
                "image",
835
                appier.reference("Media", name = "id")
836
            ),
837
        )
838
    )
839
    def remove_image_s(self, image):
840
        if not image: return
26✔
841
        if not image in self.images: return
26✔
842
        self.images.remove(image)
26✔
843
        self.save()
26✔
844

845
    @appier.operation(name = "Fix")
26✔
846
    def fix_s(self):
847
        if not self.exists(): return
×
848
        self.save()
×
849

850
    @appier.operation(name = "Ensure Quantity", level = 2, devel = True)
26✔
851
    def ensure_quantity_s(self, quantity = 1.0):
26✔
852
        if self.quantity_hand: return
×
853
        self.quantity_hand = quantity
×
854
        self.save()
×
855

856
    @appier.operation(name = "Ensure Price", level = 2, devel = True)
26✔
857
    def ensure_price_s(self, price = 100.0):
26✔
858
        if self.price: return
×
859
        self.price = price
×
860
        self.save()
×
861

862
    @appier.operation(
26✔
863
        name = "Notify",
864
        parameters = (("Email", "email", str),)
865
    )
866
    def notify(self, name = None, *args, **kwargs):
26✔
867
        name = name or "product.new"
×
868
        product = self.reload(map = True)
×
869
        receiver = kwargs.get("email", None)
×
870
        appier_extras.admin.Event.notify_g(
×
871
            name,
872
            arguments = dict(
873
                params = dict(
874
                    payload = product,
875
                    product = product,
876
                    receiver = receiver,
877
                    extra = kwargs
878
                )
879
            )
880
        )
881

882
    @appier.operation(
26✔
883
        name = "Share",
884
        parameters = (
885
            ("Email", "email", str),
886
            ("Sender", "sender", appier.legacy.UNICODE)
887
        )
888
    )
889
    def share(self, *args, **kwargs):
890
        self.notify("product.share", *args, **kwargs)
×
891

892
    @property
26✔
893
    def quantity(self):
894
        return self.quantity_hand
×
895

896
    @property
26✔
897
    def discount(self):
898
        if not self.price: return commons.Decimal(0.0)
26✔
899
        if not self.price_compare: return commons.Decimal(0.0)
26✔
900
        return self.price_compare - self.price
26✔
901

902
    @property
26✔
903
    def discount_percent(self):
904
        if not self.discount: return commons.Decimal(0.0)
26✔
905
        return self.discount / self.price_compare * commons.Decimal(100.0)
26✔
906

907
    @property
26✔
908
    def is_parent(self):
909
        if not hasattr(self, "measurements"): return False
26✔
910
        if not self.measurements: return False
26✔
911
        return len(self.measurements) > 0
26✔
912

913
    @property
26✔
914
    def is_discounted(self):
915
        return self.discount > 0.0
26✔
916

917
    @property
26✔
918
    def is_price_provided(self):
919
        return True if self.price_provider else False
26✔
920

921
    def _reset_labels(self):
26✔
922
        self.labels = []
26✔
923

924
    def _build_labels(self, groups):
26✔
925
        for group in groups:
26✔
926
            for label in group.labels:
26✔
927
                if label in self.labels: continue
26✔
928
                self.labels.append(label)
26✔
929

930
    def _get_offset(self, count, limit, kwargs):
26✔
931
        return self._get_offset_offset(count, limit, kwargs)
×
932

933
    def _get_offset_simple(self, count, limit, kwargs):
26✔
934
        return self.id % count
×
935

936
    def _get_offset_offset(self, count, limit, kwargs):
26✔
937
        cls = self.__class__
×
938
        kwargs = dict(kwargs)
×
939
        kwargs["id"] = {"$lt" : self.id}
×
940
        offset = cls.count(**kwargs)
×
941
        return offset
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2024 Coveralls, Inc