Exemple #1
0
 def __init__(self, *args, **kwargs):
     model = get_product_model()
     kwargs['queryset'] = get_product_model().objects.filter(draft=False)
     kwargs['name'] = 'Products'
     kwargs['browse'] = reverse_lazy('cubane.ishop.products.index')
     kwargs['create'] = reverse_lazy('cubane.ishop.products.create')
     super(BrowseProductField, self).__init__(*args, **kwargs)
Exemple #2
0
    def get_product_or_404(self, pk, slug):
        """
        Return the shop product matching given slug/pk or raise 404. If the
        slug does not match, return a redirect response to the correct url.
        """
        try:
            products = self.get_product_base(get_product_model()).filter(draft=False)

            if settings.SHOP_MULTIPLE_CATEGORIES:
                # multiple categories -> at least one category must be enabled
                products = products.filter(categories__enabled=True).prefetch_related(
                    Prefetch('categories', queryset=ProductCategory.objects.select_related('category').order_by('seq'))
                ).distinct()
            else:
                # single category -> category must be enabled
                products = products.filter(category__enabled=True)

            product = products.get(pk=pk)

            if product.slug != slug:
                return HttpResponsePermanentRedirect(reverse('shop.product', args=[product.slug, product.pk]))
        except get_product_model().DoesNotExist:
            raise Http404('Product matching id %s does not exist or product is a draft.' % pk)

        # if not connected to category
        if settings.SHOP_MULTIPLE_CATEGORIES:
            if product.categories.count() == 0:
                raise Http404('Product has not been assigned to any category.')
        else:
            if not product.category:
                raise Http404('Product has not been assigned to a category.')

        return product
Exemple #3
0
    def categories_with_products(self, categories):
        """
        Filters out empty categories given a list of categories.
        """
        # valid products
        products = get_product_model().objects.filter(draft=False)

        # filter out categories without products
        if settings.SHOP_MULTIPLE_CATEGORIES:
            # multiple categories per product
            product_categories = ProductCategory.objects.filter(product__in=products)
            return categories.filter(
                Q(pk__in=product_categories) |
                Q(siblings__pk__in=products) |
                Q(siblings__siblings__pk__in=products) |
                Q(siblings__siblings__siblings__pk__in=products) |
                Q(siblings__siblings__siblings__siblings__pk__in=products)
            ).distinct()
        else:
            # single category per product
            return categories.filter(
                Q(products__in=products) |
                Q(siblings__products__in=products) |
                Q(siblings__siblings__products__in=products) |
                Q(siblings__siblings__siblings__products__in=products) |
                Q(siblings__siblings__siblings__siblings__products__in=products)
            ).distinct()
Exemple #4
0
    def handle(self, *args, **kwargs):
        print 'Installing multi-category support...Please Wait...'

        i = 0
        for product in get_product_model().objects.select_related(
                'category').all():
            # empty category?
            if not product.category:
                continue

            print product.title

            # create multi-category assignment based on existing category
            ProductCategory.objects.filter(product=product).delete()
            assignment = ProductCategory()
            assignment.product = product
            assignment.category = product.category
            assignment.seq = product.seq
            assignment.save()

            # remove existing relationship
            product.category = None
            product.seq = 0
            product.save()

            i += 1

        print '%d products updated.' % i
        print
Exemple #5
0
 def on_object_links(self, links):
     """
     Override: Support for link-able categories and products.
     """
     shop = get_shop()
     links.add(get_category_model(), shop.get_categories())
     links.add(get_product_model(), shop.get_products())
Exemple #6
0
def product_price(request):
    """
    Calculate the total product price for the given product and the given
    varieties.
    """
    # arguments
    product_id = request.GET.get('product', None)
    if not product_id:
        raise Http404('Argument product required.')
    try:
        ids = [int(x) for x in request.GET.getlist('varieties[]')]
    except ValueError:
        raise Http404('Invalid numeric argument for varieties.')
    try:
        quantity = int(request.GET.get('quantity', 1))
    except ValueError:
        raise Http404('Invalid numeric value for quantity argument.')

    # get product
    product = get_object_or_404(get_product_model(), pk=product_id, client=request.client)

    # get variety options
    varieties = list(VarietyAssignment.objects.select_related('variety_option').filter(
        id__in=ids,
        product=product
    ))

    return to_json_response({
        'net': quantity * (product.net_price + sum([option.price_net for option in varieties])),
        'gross': quantity * (product.gross_price + sum([option.price_gross for option in varieties]))
    })
Exemple #7
0
    def _verify_sku(self, product, sku, sku_code_processed, initial):
        """
        Verify that the given SKU does not exist twice in the system.
        """
        is_valid = True

        def add_error(errors, error):
            # already exists?
            for err in errors:
                if err.get('field') == error.get('field') and err.get(
                        'error') == error.get('error'):
                    return
            errors.append(error)

        # empty SKU?
        if not sku.sku:
            return is_valid

        # make sure that the sku number does not conflict with
        # any other product in the system.
        products = get_product_model().objects.filter(sku=sku)
        if products.count() > 0:
            sku.errors.append({
                'field':
                'sku',
                'error':
                'SKU number already in use by product \'%s\'.' %
                products[0].title
            })
            is_valid = False

        # conflicts with any other record we processed so far?
        if sku.sku in sku_code_processed:
            for _, _sku in initial.items():
                if _sku.sku == sku.sku:
                    error = {
                        'field': 'sku',
                        'error': 'SKU number already in use for this product.'
                    }
                    add_error(sku.errors, error)
                    add_error(_sku.errors, error)
            is_valid = False
        sku_code_processed.append(sku.sku)

        # conflict with any other SKU record for any other product?
        product_skus = ProductSKU.objects.exclude(product=product).filter(
            sku=sku.sku)
        if product_skus.count() > 0:
            sku.errors.append({
                'field':
                'sku',
                'error':
                'SKU number already in use by product \'%s\'.' %
                product_skus[0].product.title
            })
            is_valid = False

        return is_valid
Exemple #8
0
    def render(self, context):
        request = context.get('request')
        currency = context.get('CURRENCY')

        product = value_or_literal(self._product, context)
        if product and isinstance(product, get_product_model()):
            return u'%s%.2f' % (currency, product.previous_price)
        else:
            return u''
Exemple #9
0
    def get_sitemaps(self):
        """
        Add shop-specific content to sitemap.
        """
        _sitemaps = super(CMSExtensions, self).get_sitemaps()

        # shop categories and products
        _sitemaps[slugify(get_category_model()._meta.verbose_name)] = ShopCategoriesSitemap(self)
        _sitemaps[slugify(get_product_model()._meta.verbose_name)] = ShopProductsSitemap(self)

        return _sitemaps
Exemple #10
0
def get_product_or_404(request):
    product_id = request.POST.get('product_id', None)
    if product_id == None:
        raise Http404('Missing argument: product_id.')

    try:
        product_id = int(product_id)
    except ValueError:
        raise Http404('Argument product_id is not an integer.')

    return get_object_or_404(get_product_model(), pk=product_id, draft=False)
Exemple #11
0
    def render(self, context):
        request = context.get('request')

        product = value_or_literal(self._product, context)
        if product:
            if isinstance(product, get_product_model()):
                amount = product.price
            else:
                amount = product

            return get_shop_price(amount)
        else:
            return ''
Exemple #12
0
    def clean_slug(self):
        slug = self.cleaned_data.get('slug')

        if slug:
            products = get_product_model().objects.filter(slug=slug)
            if self._edit and self._instance:
                products = products.exclude(pk=self._instance.pk)
            if products.count() > 0:
                raise forms.ValidationError(
                    'This slug is already used. Please choose a different slug.'
                )

        return slug
Exemple #13
0
    def get_legacy_url_models(self):
        """
        Return a list of models to test against for legacy url support.
        """
        models = super(ShopPageContextExtensions, self).get_legacy_url_models()

        from cubane.ishop import get_category_model
        from cubane.ishop import get_product_model

        models.append(get_category_model())
        models.append(get_product_model())

        return models
Exemple #14
0
    def products(self, request):
        variety_id = request.GET.get('pk')
        variety = get_object_or_404(Variety, pk=variety_id)

        assignments = VarietyAssignment.objects.filter(variety_option__variety=variety).values('product_id').distinct()
        product_ids = [a.get('product_id') for a in assignments]

        # get products
        if settings.SHOP_MULTIPLE_CATEGORIES:
            products = get_product_model().objects.prefetch_related(
                Prefetch('categories', queryset=ProductCategory.objects.select_related('category').order_by('seq'))
            ).distinct()
        else:
            products = get_product_model().objects.select_related('category')
        products = products.filter(pk__in=product_ids).order_by('title')

        if request.method == 'POST':
            return self._redirect(request, 'index')

        return {
            'variety': variety,
            'products': products
        }
Exemple #15
0
    def _verify_barcode(self, product, sku, barcodes_processed, initial):
        """
        Verify that any assigned barcode does not exist twice in the system.
        """
        is_valid = True

        # empty barcode?
        if not sku.barcode:
            return is_valid

        # make sure that the barcode does not conflict with any product
        products = get_product_model().objects.filter(barcode=sku.barcode)
        if products.count() > 0:
            sku.errors.append({
                'field':
                'barcode',
                'error':
                'Barcode already in use by product \'%s\'.' % products[0].title
            })
            is_valid = False

        # conflicts with any other record we processed so far?
        if sku.barcode in barcodes_processed:
            for _, _sku in initial.items():
                if _sku.barcode == sku.barcode:
                    error = {
                        'field': 'barcode',
                        'error': 'Barcode already in use for this product.'
                    }
                    sku.errors.append(error)
                    _sku.errors.append(error)
            is_valid = False
        barcodes_processed.append(sku.barcode)

        # conflict with any other SKU record for any other product?
        product_skus = ProductSKU.objects.exclude(product=product).filter(
            barcode=sku.barcode)
        if product_skus.count() > 0:
            sku.errors.append({
                'field':
                'barcode',
                'error':
                'Barcode already in use by product \'%s\'.' %
                product_skus[0].product.title
            })
            is_valid = False

        return is_valid
Exemple #16
0
    def varieties_delete(self, request, product_id, variety_id):
        product = get_object_or_404(get_product_model(), pk=product_id)
        variety = get_object_or_404(Variety, pk=variety_id)
        assignments = VarietyAssignment.objects.filter(
            product=product, variety_option__variety=variety)

        # delete assignments
        for assignment in assignments:
            request.changelog.delete(assignment)
            assignment.delete()

        request.changelog.commit(
            'Variety <em>%s</em> removed.' % variety.title, product)
        return to_json_response({
            'success': True,
        })
Exemple #17
0
    def render(self, context):
        # resolve target (none, category or product)
        if self._target == None:
            target = None
        else:
            target = value_or_literal(self._target, context)
            if target == 'None': target = None

        # resolve sep
        request = value_or_none('request', context)
        sep = value_or_literal(self._sep, context)

        # build list of pages based on given target
        items = [('Home', '/')]
        if target == None:
            pass
        elif isinstance(target, str) or isinstance(target, unicode):
            # static page
            items.append((target, make_absolute_url(request.path)))
        elif isinstance(target, get_category_model()):
            # category page
            items.extend([(c.title, c.get_absolute_url()) for c in target.get_path()])
        elif isinstance(target, get_product_model()):
            # product category first...
            if target.primary_category:
                items.extend([(c.title, c.get_absolute_url()) for c in target.primary_category.get_path()])

            # ...then the product itself
            items.append((target.title, target.get_absolute_url()))
        elif isinstance(target, ChildPage):
            # page/child page
            page = target.page
            items.append((page.title, page.get_absolute_url()))
            items.append((target.title, target.get_absolute_url()))
        elif isinstance(target, PageBase):
            # single cms page
            items.append((target.title, target.get_absolute_url()))
        else:
            raise ValueError('Invalid target argument: Must be instance of Category, Product or None.')

        t = get_template('cubane/ishop/elements/breadcrumbs.html')
        d = {
            'items': items,
            'sep': sep,
        }
        with context.push(**d):
            return t.render(context)
Exemple #18
0
    def varieties(self, request):
        product_id = request.GET.get('pk')
        product = get_object_or_404(get_product_model(), pk=product_id)

        # load variety options
        variety_options = [
            (a.variety_option.variety, a.variety_option)
            for a in VarietyAssignment.objects.select_related(
                'variety_option', 'variety_option__variety').filter(
                    product=product)
        ]
        assigned_variety_ids = [variety.pk for variety, _ in variety_options]

        # load all varieties and split them into assigned and unassigned
        varieties = Variety.objects.prefetch_related('options').exclude(
            options=None).order_by('title')
        if product.sku_enabled:
            varieties = varieties.filter(sku=False)

        # split into assigned and unassigned
        varieties = list(varieties)
        assigned = filter(lambda v: v.id in assigned_variety_ids, varieties)
        unassigned = filter(lambda v: v.id not in assigned_variety_ids,
                            varieties)

        # inject list of assigned variety options for all assigned varieties
        for v in assigned:
            # collect assigned options
            assigned_options = []
            for variety, option in variety_options:
                if v.pk == variety.pk:
                    assigned_options.append(option)

            # sort by title and construct display text
            assigned_options = sorted(assigned_options, key=lambda o: o.title)
            v.assigned_options_display = ', '.join(
                option.title.strip() for option in assigned_options[:5]) + (
                    ', ...' if len(assigned_options) > 5 else '')

        return {
            'product': product,
            'assigned': assigned,
            'unassigned': unassigned,
            'ok_url': self._get_url(request, 'index')
        }
Exemple #19
0
 def _write_data(self, writer):
     """
     Write all shop inventory data rows.
     """
     # process each product
     product_model = get_product_model()
     products = product_model.objects.filter(draft=False)
     for product in products:
         # SKU-enabled?
         if product.sku_enabled:
             # multiple SKU's. Generate one row for each stockable variant
             skus = product.product_sku.filter(sku__isnull=False).exclude(
                 variety_options=None)
             for sku in skus:
                 if sku.sku:
                     self._write_row(writer,
                                     self._sku_to_value_list(product, sku))
         else:
             # single product record without varieties
             if product.sku:
                 self._write_row(writer,
                                 self._product_to_value_list(product))
Exemple #20
0
 class Meta:
     model = get_product_model()
     exclude = ['seq', 'varieties', 'delivery_options', '_related_products']
     widgets = {
         'title':
         forms.TextInput(attrs={
             'class': 'slugify',
             'autocomplete': 'off'
         }),
         'slug':
         forms.TextInput(attrs={
             'class': 'slug',
             'autocomplete': 'off'
         }),
         'price':
         BootstrapTextInput(prepend=settings.CURRENCY,
                            attrs={'class': 'input-medium'}),
         'deposit':
         BootstrapTextInput(prepend=settings.CURRENCY,
                            attrs={'class': 'input-medium'}),
         'rrp':
         BootstrapTextInput(prepend=settings.CURRENCY,
                            attrs={'class': 'input-medium'}),
         'previous_price':
         BootstrapTextInput(prepend=settings.CURRENCY,
                            attrs={'class': 'input-medium'}),
         'finance_options':
         forms.CheckboxSelectMultiple()
     }
     tabs = [{
         'title':
         'Title',
         'fields': [
             'title', 'slug', 'category', 'categories', 'legacy_url',
             'excerpt', '_meta_title', '_meta_description',
             '_meta_keywords', '_meta_preview'
         ]
     }, {
         'title': 'Content',
         'fields': ['_excerpt', 'description']
     }, {
         'title':
         'Price and Availability',
         'fields': [
             'rrp',
             'previous_price',
             'price',
             'draft',
             'non_returnable',
             'collection_only',
             'pre_order',
             'deposit',
             'loan_exempt',
             'finance_options',
             'exempt_from_free_delivery',
             'exempt_from_discount',
             'feed_google',
             'feed_amazon',
             'stock',
             'stocklevel',
             'sku_enabled',
             'sku',
             'barcode_system',
             'barcode',
             'part_number',
         ]
     }, {
         'title': 'Gallery',
         'fields': ['image', '_gallery_images']
     }, {
         'title': 'Related Products',
         'fields': ['_related_products_collection']
     }, {
         'title': 'SKU / Inventory',
         'fields': ['_inventory']
     }]
     sections = {
         'title': 'Product Data',
         '_excerpt': 'Excerpt',
         '_meta_title': 'Meta Data',
         'barcode_system': 'Identification',
         'stock': 'Stock',
         'sku_enabled': 'SKU / Inventory',
         '_meta_preview': 'Search Result Preview',
         'rrp': 'Price',
         'draft': 'Options',
         'pre_order': 'Pre-order',
         'loan_exempt': 'Finance',
         'exempt_from_free_delivery': 'Exemption',
         'feed_google': 'Channel Feeds',
         'image': 'Product Images'
     }
Exemple #21
0
class ProductFormBase(BaseModelForm):
    class Meta:
        model = get_product_model()
        exclude = ['seq', 'varieties', 'delivery_options', '_related_products']
        widgets = {
            'title':
            forms.TextInput(attrs={
                'class': 'slugify',
                'autocomplete': 'off'
            }),
            'slug':
            forms.TextInput(attrs={
                'class': 'slug',
                'autocomplete': 'off'
            }),
            'price':
            BootstrapTextInput(prepend=settings.CURRENCY,
                               attrs={'class': 'input-medium'}),
            'deposit':
            BootstrapTextInput(prepend=settings.CURRENCY,
                               attrs={'class': 'input-medium'}),
            'rrp':
            BootstrapTextInput(prepend=settings.CURRENCY,
                               attrs={'class': 'input-medium'}),
            'previous_price':
            BootstrapTextInput(prepend=settings.CURRENCY,
                               attrs={'class': 'input-medium'}),
            'finance_options':
            forms.CheckboxSelectMultiple()
        }
        tabs = [{
            'title':
            'Title',
            'fields': [
                'title', 'slug', 'category', 'categories', 'legacy_url',
                'excerpt', '_meta_title', '_meta_description',
                '_meta_keywords', '_meta_preview'
            ]
        }, {
            'title': 'Content',
            'fields': ['_excerpt', 'description']
        }, {
            'title':
            'Price and Availability',
            'fields': [
                'rrp',
                'previous_price',
                'price',
                'draft',
                'non_returnable',
                'collection_only',
                'pre_order',
                'deposit',
                'loan_exempt',
                'finance_options',
                'exempt_from_free_delivery',
                'exempt_from_discount',
                'feed_google',
                'feed_amazon',
                'stock',
                'stocklevel',
                'sku_enabled',
                'sku',
                'barcode_system',
                'barcode',
                'part_number',
            ]
        }, {
            'title': 'Gallery',
            'fields': ['image', '_gallery_images']
        }, {
            'title': 'Related Products',
            'fields': ['_related_products_collection']
        }, {
            'title': 'SKU / Inventory',
            'fields': ['_inventory']
        }]
        sections = {
            'title': 'Product Data',
            '_excerpt': 'Excerpt',
            '_meta_title': 'Meta Data',
            'barcode_system': 'Identification',
            'stock': 'Stock',
            'sku_enabled': 'SKU / Inventory',
            '_meta_preview': 'Search Result Preview',
            'rrp': 'Price',
            'draft': 'Options',
            'pre_order': 'Pre-order',
            'loan_exempt': 'Finance',
            'exempt_from_free_delivery': 'Exemption',
            'feed_google': 'Channel Feeds',
            'image': 'Product Images'
        }

    category = BrowseCategoryField(
        required=True, help_text='The category this product is listed under.')

    _meta_preview = fields.Field(
        label=None,
        required=False,
        help_text='This preview is for demonstration purposes only ' +\
            'and the actual search result may differ from the preview.',
    )

    image = BrowseImagesField(
        required=False,
        help_text=
        'Choose the main image for this product that is used on the product listing page.'
    )

    _gallery_images = GalleryField(
        label='Image Gallery',
        required=False,
        queryset=Media.objects.filter(is_image=True),
        help_text=
        'Add an arbitrarily number of images that are presented on the product details page.'
    )

    _related_products_collection = ModelCollectionField(
        label='Related Products',
        required=False,
        queryset=get_product_model().objects.all(),
        url='/admin/products/',
        title='Products',
        model_title='Products',
        help_text=
        'Add an arbitrarily number of related products to this product.')

    categories = ModelCollectionField(
        label='Categories',
        add_label='Add Category',
        required=True,
        queryset=get_category_model().objects.all(),
        url='/admin/categories/',
        title='Categories',
        model_title='Categories',
        viewmode=ModelCollectionField.VIEWMODE_LIST,
        allow_duplicates=False,
        sortable=False,
        help_text=
        'Add an arbitrarily number of categories this product is listed under.'
    )

    _inventory = RelatedListingField(view=InventoryView())

    def configure(self, request, instance, edit):
        super(ProductFormBase, self).configure(request, instance, edit)

        # meta preview control
        self.fields['_meta_preview'].widget = MetaPreviewWidget(
            attrs={
                'class': 'no-label',
                'path': request.path_info,
                'form': self
            })

        # excerpt
        self.fields[
            '_excerpt'].help_text = 'Provide your elevator pitch to the customer (max. %d characters)' % settings.CMS_EXCERPT_LENGTH

        if request.settings.barcode_system and request.settings.sku_is_barcode:
            self.remove_field('sku')
            self.fields['barcode'].label = 'SKU / Barcode'
            self.fields[
                'barcode'].help_text = 'SKU / Barcode (%s)' % request.settings.barcode_system.upper(
                )

        # multiple categories
        if settings.SHOP_MULTIPLE_CATEGORIES:
            self.remove_field('category')
        else:
            self.remove_field('categories')

        # loan applications
        if settings.SHOP_LOAN_ENABLED:
            queryset = FinanceOption.objects.filter(
                enabled=True, per_product=True).order_by('seq')
            self.fields['finance_options'].queryset = queryset

            if queryset.count() == 0:
                self.remove_field('finance_options')
                self.update_sections()
        else:
            self.remove_field('finance_options')
            self.remove_field('loan_exempt')

        # SKU / inventory
        if not (instance and instance.sku_enabled):
            self.remove_tab('SKU / Inventory')

        self.update_sections()

    def clean_slug(self):
        slug = self.cleaned_data.get('slug')

        if slug:
            products = get_product_model().objects.filter(slug=slug)
            if self._edit and self._instance:
                products = products.exclude(pk=self._instance.pk)
            if products.count() > 0:
                raise forms.ValidationError(
                    'This slug is already used. Please choose a different slug.'
                )

        return slug

    def clean_excerpt(self):
        excerpt = self.cleaned_data.get('_excerpt')

        if excerpt:
            if len(excerpt) > settings.CMS_EXCERPT_LENGTH:
                raise forms.ValidationError(
                    'The maximum allowed length is %d characters.' %
                    settings.CMS_EXCERPT_LENGTH)

        return excerpt

    def clean_price(self):
        return clean_price(self, 'price', self.cleaned_data)

    def clean_rrp(self):
        return clean_price(self, 'rrp', self.cleaned_data)

    def clean_previous_price(self):
        return clean_price(self, 'previous_price', self.cleaned_data)

    def clean(self):
        d = super(ProductFormBase, self).clean()

        barcode_system = d.get('barcode_system')
        barcode = d.get('barcode')

        if barcode_system is None:
            barcode_system = self._request.settings.barcode_system

        # verify that the barcode is correct...
        if barcode and barcode_system:
            try:
                d['barcode'] = verify_barcode(barcode_system, barcode)
            except BarcodeError, e:
                self.field_error('barcode', e.msg)

        return d
Exemple #22
0
    def google_products(self, request):
        def prettify_xml(elem):
            """
            Return a pretty-printed XML string for the Element.
            """
            rough_string = tostring(elem)
            reparsed = minidom.parseString(rough_string)
            return reparsed.toprettyxml(indent='\t').encode('utf-8', 'replace')

        products = get_product_model().objects.filter(feed_google=True)
        root = Element('rss')
        root.attrib['xmlns:g'] = 'http://base.google.com/ns/1.0'
        root.attrib['version'] = '2.0'
        channel = SubElement(root, 'channel')
        title = SubElement(channel, 'title')
        title.text = request.settings.name
        link = SubElement(channel, 'link')
        link.text = settings.DOMAIN_NAME
        description = SubElement(channel, 'description')

        for p in products:
            # availability
            if p.is_available and not p.pre_order:
                txt_availability = 'in stock'
            elif p.pre_order:
                txt_availability = 'preorder'
            else:
                txt_availability = 'out of stock'

            # determine delivery charge by placing the product onto the basket
            basket = Basket()
            basket.add_item(p, None, 1)
            delivery_charge = basket.delivery

            # determine feed item attributes
            txt_id = unicode(p.id)
            txt_title = clean_unicode(p.title).strip()
            txt_link = p.get_absolute_url()
            txt_description = text_from_html(p.description, 5000)
            txt_condition = 'new'
            txt_price = '%.2f GBP' % p.price
            txt_google_category = p.category.google_product_category if p.category and p.category.google_product_category else None
            txt_category = p.category.get_taxonomy_path(
            ) if p.category else None
            txt_country = 'GB'
            txt_delivery_price = '%s %s' % (delivery_charge, 'GBP')
            txt_barcode = p.barcode.strip() if p.barcode else None
            txt_part_number = p.part_number.strip() if p.part_number else None
            txt_brand = p.get_brand_title()

            # create item
            item = SubElement(channel, 'item')

            # id
            _id = SubElement(item, 'g:id')
            _id.text = txt_id

            # title
            title = SubElement(item, 'title')
            title.text = txt_title

            # link/url
            link = SubElement(item, 'link')
            link.text = txt_link

            # main text
            description = SubElement(item, 'description')
            description.text = txt_description

            # condition
            condition = SubElement(item, 'g:condition')
            condition.text = txt_condition

            # price
            price = SubElement(item, 'g:price')
            price.text = txt_price

            # availability
            availability = SubElement(item, 'g:availability')
            availability.text = txt_availability

            # google shopping category
            if txt_google_category:
                gcategory = SubElement(item, 'g:google_product_category')
                gcategory.text = txt_google_category

            # product type
            if txt_category:
                category = SubElement(item, 'g:product_type')
                category.text = txt_category

            # shipping
            shipping = SubElement(item, 'g:shipping')

            # country
            country = SubElement(shipping, 'g:country')
            country.text = txt_country

            # delivery price
            delivery_price = SubElement(shipping, 'g:price')
            delivery_price.text = txt_delivery_price

            # barcode, must be a valid UPC-A (GTIN-12), EAN/JAN (GTIN-13)
            # or GTIN-14, so we need to have at least 12 characters.
            if txt_barcode:
                gtin = SubElement(item, 'g:gtin')
                gtin.text = txt_barcode

            # part number
            if txt_part_number:
                _mpn = SubElement(item, 'g:mpn')
                _mpn.text = txt_part_number

            # brand
            if txt_brand:
                brand = SubElement(item, 'g:brand')
                brand.text = txt_brand

            # image
            if p.image:
                image = SubElement(item, 'g:image_link')
                image.text = p.image.large_url

            # additional images
            if len(p.gallery) > 0:
                for m in p.gallery[:10]:
                    additional_image_link = SubElement(
                        item, 'g:additional_image_link')
                    additional_image_link.text = m.large_url

        # get temp. filename
        f = NamedTemporaryFile(delete=False)
        tmp_filename = f.name
        f.close()

        # create tmp file (utf-8)
        f = open(tmp_filename, 'w+b')
        f.write(prettify_xml(root))
        f.seek(0)

        # send response
        filename = 'google_products_%s.xml' % datetime.date.today().strftime(
            '%d_%m_%Y')
        response = HttpResponse(FileWrapper(f), content_type='text/plain')
        response['Content-Disposition'] = 'attachment; filename=%s' % filename
        return response
Exemple #23
0
    def delivery(self, request):
        product_id = request.GET.get('pk')
        product = get_object_or_404(get_product_model(), pk=product_id)

        # get general delivery options that are available
        options = DeliveryOption.objects.filter(enabled=True)

        # get available delivery options
        delivery_options = list(
            ProductDeliveryOption.objects.select_related(
                'delivery_option').filter(product=product))
        delivery_options_ids = [
            option.delivery_option.id for option in delivery_options
        ]

        # add missing options, so that each is convered
        for option in options:
            if option.id not in delivery_options_ids:
                assignment = ProductDeliveryOption()
                assignment.product = product
                assignment.delivery_option = option
                delivery_options.append(assignment)

        # dataset based on available options
        initial = [{
            'option_id': option.delivery_option.id,
            'deliver_uk': option.delivery_option.deliver_uk,
            'deliver_eu': option.delivery_option.deliver_eu,
            'deliver_world': option.delivery_option.deliver_world,
            'title': option.delivery_option.title,
            'uk': option.uk,
            'eu': option.eu,
            'world': option.world
        } for option in delivery_options]

        if request.method == 'POST':
            formset = DeliveryOptionFormset(request.POST, initial=initial)
        else:
            formset = DeliveryOptionFormset(initial=initial)

        if request.method == 'POST':
            if formset.is_valid():
                # delete all existing assignments
                assignments = ProductDeliveryOption.objects.filter(
                    product=product)
                for assignment in assignments:
                    request.changelog.delete(assignment)
                    assignment.delete()

                # create new assignments
                for form in formset.forms:
                    d = form.cleaned_data
                    for option in options:
                        if option.id == d.get('option_id'):
                            assignment = ProductDeliveryOption()
                            assignment.product = product
                            assignment.delivery_option = option
                            assignment.uk = d.get('uk')
                            assignment.eu = d.get('eu')
                            assignment.world = d.get('world')
                            assignment.save()
                            request.changelog.create(assignment)
                            break

                # commit, message and redirect
                request.changelog.commit(
                    'Delivery options for product <em>%s</em> updated.' %
                    product.title,
                    product,
                    flash=True)
                return self.redirect_to_index_or(request, 'delivery', product)
            else:
                print formset.errors

        return {
            'product': product,
            'delivery_options': delivery_options,
            'form': formset
        }
Exemple #24
0
class ProductView(ModelView):
    """
    Editing categories (tree)
    """
    template_path = 'cubane/ishop/merchant/products/'
    model = get_product_model()

    def __init__(self, namespace, with_folders):
        self.namespace = namespace
        self.with_folders = with_folders

        if with_folders:
            self.folder_model = get_category_model()

        self.sortable = self.with_folders

        # multiple categories
        if settings.SHOP_MULTIPLE_CATEGORIES:
            self.exclude_columns = ['category']
            self.multiple_folders = True
        else:
            self.exclude_columns = ['categories_display']
            self.multiple_folders = False

        super(ProductView, self).__init__()

    patterns = [
        view_url(r'varieties/', 'varieties', name='varieties'),
        view_url(r'varieties/(?P<product_id>\d+)/edit/(?P<variety_id>\d+)/',
                 'varieties_edit',
                 name='varieties.edit'),
        view_url(r'varieties/(?P<product_id>\d+)/delete/(?P<variety_id>\d+)',
                 'varieties_delete',
                 name='varieties.delete'),
        view_url(r'sku/', 'sku', name='sku'),
        view_url(r'delivery/', 'delivery', name='delivery'),
        view_url(r'google-products-export/',
                 'google_products',
                 name='google_products'),
    ]

    listing_actions = [('[SKUs]', 'sku', 'single'),
                       ('[Varieties]', 'varieties', 'single'),
                       ('[Delivery]', 'delivery', 'single'),
                       ('Export To Google', 'google_products', 'any')]

    shortcut_actions = ['sku', 'varieties', 'delivery']

    def _get_objects(self, request):
        if settings.SHOP_MULTIPLE_CATEGORIES:
            # multiple categories
            return self.model.objects.prefetch_related(
                Prefetch('categories',
                         queryset=ProductCategory.objects.select_related(
                             'category').order_by('seq'))).distinct()
        else:
            # single category
            return self.model.objects.select_related('category').all()

    def _get_folders(self, request, parent):
        folders = self.folder_model.objects.all()

        if parent:
            folders = folders.filter(parent=parent)

        return folders

    def _folder_filter(self, request, objects, category_pks):
        """
        Filter given object queryset by the given folder primary key.
        """
        if category_pks:
            q = Q()
            if settings.SHOP_MULTIPLE_CATEGORIES:
                # multiple categories
                for pk in category_pks:
                    q |= Q(categories__id=pk) | \
                         Q(categories__parent_id=pk) | \
                         Q(categories__parent__parent_id=pk) | \
                         Q(categories__parent__parent__parent_id=pk) | \
                         Q(categories__parent__parent__parent__parent_id=pk) | \
                         Q(categories__parent__parent__parent__parent__parent_id=pk)
            else:
                # single category
                for pk in category_pks:
                    q |= Q(category_id=pk) | \
                         Q(category__parent_id=pk) | \
                         Q(category__parent__parent_id=pk) | \
                         Q(category__parent__parent__parent_id=pk) | \
                         Q(category__parent__parent__parent__parent_id=pk) | \
                         Q(category__parent__parent__parent__parent__parent_id=pk)

            # apply filter
            objects = objects.filter(q)
        return objects

    def _get_folder_assignment_name(self):
        """
        Return the name of the field that is used to assign a folder to.
        """
        if settings.SHOP_MULTIPLE_CATEGORIES:
            return 'categories'
        else:
            return 'category'

    def form_initial(self, request, initial, instance, edit):
        """
        Setup gallery images (initial form data)
        """
        initial['_gallery_images'] = load_media_gallery(
            instance.gallery_images)
        initial['_related_products_collection'] = RelatedModelCollection.load(
            instance, RelatedProducts)

        if settings.SHOP_MULTIPLE_CATEGORIES:
            initial['categories'] = RelatedModelCollection.load(
                instance, ProductCategory, sortable=False)

    def bulk_form_initial(self, request, initial, instance, edit):
        if settings.SHOP_MULTIPLE_CATEGORIES:
            initial['categories'] = RelatedModelCollection.load(
                instance, ProductCategory, sortable=False)

    def before_save(self, request, cleaned_data, instance, edit):
        """
        Maintain SKU based on barcode.
        """
        if request.settings.sku_is_barcode:
            instance.sku = cleaned_data.get('barcode')

    def after_save(self, request, d, instance, edit):
        """
        Save gallery items (in seq.)
        """
        save_media_gallery(request, instance, d.get('_gallery_images'))
        RelatedModelCollection.save(request, instance,
                                    d.get('_related_products_collection'),
                                    RelatedProducts)

        if settings.SHOP_MULTIPLE_CATEGORIES:
            RelatedModelCollection.save(request,
                                        instance,
                                        d.get('categories'),
                                        ProductCategory,
                                        allow_duplicates=False,
                                        sortable=False)

    def after_bulk_save(self, request, d, instance, edit):
        if settings.SHOP_MULTIPLE_CATEGORIES:
            RelatedModelCollection.save(request,
                                        instance,
                                        d.get('categories'),
                                        ProductCategory,
                                        allow_duplicates=False,
                                        sortable=False)

    def varieties(self, request):
        product_id = request.GET.get('pk')
        product = get_object_or_404(get_product_model(), pk=product_id)

        # load variety options
        variety_options = [
            (a.variety_option.variety, a.variety_option)
            for a in VarietyAssignment.objects.select_related(
                'variety_option', 'variety_option__variety').filter(
                    product=product)
        ]
        assigned_variety_ids = [variety.pk for variety, _ in variety_options]

        # load all varieties and split them into assigned and unassigned
        varieties = Variety.objects.prefetch_related('options').exclude(
            options=None).order_by('title')
        if product.sku_enabled:
            varieties = varieties.filter(sku=False)

        # split into assigned and unassigned
        varieties = list(varieties)
        assigned = filter(lambda v: v.id in assigned_variety_ids, varieties)
        unassigned = filter(lambda v: v.id not in assigned_variety_ids,
                            varieties)

        # inject list of assigned variety options for all assigned varieties
        for v in assigned:
            # collect assigned options
            assigned_options = []
            for variety, option in variety_options:
                if v.pk == variety.pk:
                    assigned_options.append(option)

            # sort by title and construct display text
            assigned_options = sorted(assigned_options, key=lambda o: o.title)
            v.assigned_options_display = ', '.join(
                option.title.strip() for option in assigned_options[:5]) + (
                    ', ...' if len(assigned_options) > 5 else '')

        return {
            'product': product,
            'assigned': assigned,
            'unassigned': unassigned,
            'ok_url': self._get_url(request, 'index')
        }

    def varieties_edit(self, request, product_id, variety_id):
        product = get_object_or_404(get_product_model(), pk=product_id)
        variety = get_object_or_404(Variety, pk=variety_id)
        options = list(variety.options.order_by('seq', 'id'))
        assignments = VarietyAssignment.objects.select_related(
            'variety_option').filter(product=product,
                                     variety_option__variety=variety)
        assignment_list = list(assignments)

        # dataset based on available options
        initial = [{
            'option_id': option.id,
            'title': option.title,
            'enabled': False,
            'offset_type': option.default_offset_type,
            'offset_value': option.default_offset_value,
            'text_label': option.text_label,
            'seq': option.seq,
            'option_enabled': option.enabled
        } for option in options]

        # update base dataset based on available assignments...
        for initial_option in initial:
            for assignment in assignment_list:
                if initial_option['option_id'] == assignment.variety_option.id:
                    initial_option['enabled'] = True
                    initial_option['option_enabled'] = True
                    initial_option['offset_type'] = assignment.offset_type
                    initial_option['offset_value'] = assignment.offset_value

        # remove options that are not currently assigned but disabled...
        initial = filter(lambda option: option.get('option_enabled'), initial)

        # sort by enabled state, then seq if we have a lot of varieties
        if len(initial) > 15:
            initial = sorted(initial,
                             key=lambda x: (-x.get('enabled', x.get('seq'))))

        # determine form class
        if variety.is_attribute:
            form_class = VarietyAttributeAssignmentFormset
        else:
            form_class = VarietyAssignmentFormset

        # create form
        if request.method == 'POST':
            formset = form_class(request.POST)
        else:
            formset = form_class(initial=initial)

        # validation
        if formset.is_valid():
            # delete existing assignments
            for assignment in assignments:
                request.changelog.delete(assignment)
                assignment.delete()

            # create new assignments
            for form in formset.forms:
                d = form.cleaned_data
                if d.get('enabled') == True:
                    for option in options:
                        if option.id == d.get('option_id'):
                            assignment = VarietyAssignment()
                            assignment.variety_option = option
                            assignment.product = product

                            if not variety.is_attribute:
                                assignment.offset_type = d.get('offset_type')
                                assignment.offset_value = d.get('offset_value')

                            assignment.save()
                            request.changelog.create(assignment)
                            break

            request.changelog.commit(
                'Variety Options for <em>%s</em> for product <em>%s</em> updated.'
                % (variety.title, product.title),
                product,
                flash=True)

            active_tab = request.POST.get('cubane_save_and_continue', '0')
            if not active_tab == '0':
                return self._redirect(request,
                                      'varieties.edit',
                                      args=[product.id, variety.id])
            else:
                return self._redirect(request, 'varieties', product)

        return {
            'product': product,
            'variety': variety,
            'form': formset,
        }

    @view(require_POST)
    def varieties_delete(self, request, product_id, variety_id):
        product = get_object_or_404(get_product_model(), pk=product_id)
        variety = get_object_or_404(Variety, pk=variety_id)
        assignments = VarietyAssignment.objects.filter(
            product=product, variety_option__variety=variety)

        # delete assignments
        for assignment in assignments:
            request.changelog.delete(assignment)
            assignment.delete()

        request.changelog.commit(
            'Variety <em>%s</em> removed.' % variety.title, product)
        return to_json_response({
            'success': True,
        })

    def sku(self, request):
        # get product
        product_id = request.GET.get('pk')
        product = get_object_or_404(get_product_model(), pk=product_id)

        # get varieties
        _varieties = Variety.objects.prefetch_related(
            Prefetch('options',
                     queryset=VarietyOption.objects.order_by('title'))
        ).filter(sku=True).exclude(options=None).exclude(
            style=Variety.STYLE_ATTRIBUTE).order_by('title').distinct()
        skus = ProductSKU.objects.filter(product=product)
        assigned_option_ids = [
            a.variety_option.id
            for a in VarietyAssignment.objects.select_related('variety_option')
            .filter(product=product, variety_option__variety__sku=True)
        ]

        # initial dataset currently present
        initial = {}
        for sku in skus:
            initial[sku.pk] = sku
            initial[sku.pk].errors = []

        # determine barcode system
        cms_settings = get_cms_settings()
        barcode_system = cms_settings.get_barcode_system(product)

        # create template form
        form_template = ProductSKUForm()
        form_template.configure(request, barcode_system)

        def has_var(prefix, name):
            return 'f-%s-%s' % (prefix, name) in request.POST

        def get_var(prefix, name, default=None):
            return request.POST.get('f-%s-%s' % (prefix, name), default)

        def get_int_var(prefix, name, default=None):
            return parse_int(get_var(prefix, name), default)

        # construct list of variety option names
        varieties = []
        variety_index = {}
        for variety in _varieties:
            variety_index[variety.id] = {
                'id': variety.id,
                'title': variety.title,
                'sku': variety.sku,
                'options': {}
            }

            item = {
                'id': variety.id,
                'title': variety.title,
                'sku': variety.sku,
                'options': [],
                'n_assigned_options': 0
            }
            for option in variety.options.all():
                variety_index[variety.id].get('options')[option.id] = {
                    'id': option.id,
                    'title': option.title,
                    'fullTitle':
                    '%s: <em>%s</em>' % (variety.title, option.title)
                }
                item.get('options').append({
                    'id':
                    option.id,
                    'title':
                    option.title,
                    'assigned':
                    option.id in assigned_option_ids
                })
                if option.pk in assigned_option_ids:
                    item['n_assigned_options'] += 1
            varieties.append(item)

        # sort varieties by number of assigned options, so that varieties that
        # have been assigned are at the top of the list. The rest remains sorted
        # alphabetically...
        varieties.sort(key=lambda x: -x.get('n_assigned_options', 0))

        # validation
        is_valid = True
        if request.method == 'POST':
            # process sku records
            prefixes = request.POST.getlist('skus')
            assigned_option_ids = []
            skus_to_save = []
            sku_code_processed = []
            barcodes_processed = []
            for index, prefix in enumerate(prefixes):
                # extract relevant informatioin from post for
                # individual combination
                _id = get_var(prefix, '_id')
                d = {
                    'enabled': get_var(prefix, 'enabled') == 'on',
                    'sku': get_var(prefix, 'sku'),
                    'barcode': get_var(prefix, 'barcode'),
                    'price': get_var(prefix, 'price'),
                    'stocklevel': get_int_var(prefix, 'stocklevel', 0)
                }

                # parse assigned variety options from request data
                n_variety_option = 1
                d['variety_options'] = []
                while len(d['variety_options']) <= 16:
                    _name = 'vo_%d' % n_variety_option
                    if has_var(prefix, _name):
                        d['variety_options'].append(get_int_var(prefix, _name))
                        n_variety_option += 1
                    else:
                        break

                # make sure that sku, barcode and price are None
                # instead of empty
                if _id == '': _id = None
                if d.get('sku') == '': d['sku'] = None
                if d.get('barcode') == '': d['barcode'] = None
                if d.get('price') == '': d['price'] = None

                # construct form based on this data and validate
                form = ProductSKUForm(d)
                form.configure(request, barcode_system)

                # get variety options
                variety_options = VarietyOption.objects.filter(
                    pk__in=d.get('variety_options'))

                # create or edit?
                sku = initial.get(_id, None)
                if sku is None:
                    sku = ProductSKU.objects.get_by_variety_options(
                        product, variety_options)

                    # still not found? -> create new item
                    if sku is None:
                        sku = ProductSKU()
                        sku.product = product

                # remember the sku record to be saved once we processed
                # everything. We will not save anything until everything
                # is considered to be valid.
                skus_to_save.append(sku)

                # mark any assigned variety options as selected, so that they
                # indeed remain selected, even if they have actually not been
                # properly assigned yet because if from errors for example
                for _variety in varieties:
                    _options = _variety.get('options')
                    for _option in _options:
                        for _assigned_option in variety_options:
                            if _option.get('id') == _assigned_option.pk:
                                _option['assigned'] = True
                                break

                # inject error information and keep track of error states
                sku.errors = []
                if form.is_valid():
                    # update data from from
                    d = form.cleaned_data
                else:
                    for field, error in form.errors.items():
                        sku.errors.append({'field': field, 'error': error[0]})
                    is_valid = False

                # copy original data or cleaned data
                sku.enabled = d.get('enabled', False)
                sku.sku = d.get('sku')
                sku.barcode = d.get('barcode')
                sku.price = d.get('price')
                sku.stocklevel = d.get('stocklevel', 0)

                # keep track of variety options that should be assigned due to
                # SKU's that are enabled
                if sku.enabled:
                    assigned_option_ids.extend(
                        [option.pk for option in variety_options])

                # set variety options (saved later)
                sku._variety_options = variety_options

                # verify uniqueness of the SKU code
                if not self._verify_sku(product, sku, sku_code_processed,
                                        initial):
                    is_valid = False

                # verify uniqueness of barcode
                if not self._verify_barcode(product, sku, barcodes_processed,
                                            initial):
                    is_valid = False

                # maintain changed data in initial data set, so that all
                # changes make theire way back into the view, even through
                # we might not have saved changes due to errors
                _id = ('idx_%d' % index) if sku.pk is None else sku.pk
                initial[_id] = sku

        # process if everything is valid
        if request.method == 'POST' and is_valid:
            # create missing option assignments
            assigned_option_ids = list(
                set(filter(lambda x: x is not None, assigned_option_ids)))
            for option_id in assigned_option_ids:
                try:
                    assignment = VarietyAssignment.objects.get(
                        product=product, variety_option__pk=option_id)
                except VarietyAssignment.DoesNotExist:
                    VarietyAssignment.objects.create(
                        product=product, variety_option_id=option_id)

            # remove deprecated option assignments
            deprecated_assignments = VarietyAssignment.objects.select_related(
                'variety_option').filter(
                    product=product,
                    variety_option__variety__sku=True).exclude(
                        variety_option__pk__in=assigned_option_ids)
            for deprecated_assignment in deprecated_assignments:
                deprecated_assignment.delete()

            # save changes to sku records. Null sku, so that we would not
            # collide when making updates
            sku_ids_saved = []
            for sku in skus_to_save:
                # save product sku itself
                sku._sku = sku.sku
                sku.sku = None
                sku.save()
                sku_ids_saved.append(sku.id)

                # assign and save variety options
                sku.variety_options = sku._variety_options

            # remove all previous SKU
            deprecated_skus = ProductSKU.objects.filter(
                product=product).exclude(pk__in=sku_ids_saved)
            for deprecated_sku in deprecated_skus:
                deprecated_sku.delete()

            # apply new sku names, which is now safe to do
            for sku in skus_to_save:
                if request.settings.sku_is_barcode:
                    sku.sku = sku.barcode
                else:
                    sku.sku = sku._sku
                sku.save()

        # redirect and message
        if request.method == 'POST' and is_valid:
            messages.add_message(
                request, messages.SUCCESS,
                'Product Varieties and SKUs saved for <em>%s</em>.' %
                product.title)

            if request.POST.get('cubane_save_and_continue') is None:
                return self._redirect(request, 'index')
            else:
                return self._redirect(request, 'sku', product)

        # materialise current initial data
        _initial = {}
        for _id, _sku in initial.items():
            _initial[_id] = model_to_dict(_sku)
            if _sku.pk is None:
                _initial[_id]['variety_options'] = [
                    option.pk for option in _sku._variety_options
                ]
            else:
                _initial[_id]['variety_options'] = [
                    option.pk for option in _sku.variety_options.all()
                ]
            _initial[_id]['errors'] = _sku.errors if hasattr(_sku,
                                                             'errors') else []

        # template context
        return {
            'product': product,
            'varieties': varieties,
            'initial': to_json(_initial),
            'variety_index_json': to_json(variety_index),
            'form_template': form_template
        }

    def _verify_sku(self, product, sku, sku_code_processed, initial):
        """
        Verify that the given SKU does not exist twice in the system.
        """
        is_valid = True

        def add_error(errors, error):
            # already exists?
            for err in errors:
                if err.get('field') == error.get('field') and err.get(
                        'error') == error.get('error'):
                    return
            errors.append(error)

        # empty SKU?
        if not sku.sku:
            return is_valid

        # make sure that the sku number does not conflict with
        # any other product in the system.
        products = get_product_model().objects.filter(sku=sku)
        if products.count() > 0:
            sku.errors.append({
                'field':
                'sku',
                'error':
                'SKU number already in use by product \'%s\'.' %
                products[0].title
            })
            is_valid = False

        # conflicts with any other record we processed so far?
        if sku.sku in sku_code_processed:
            for _, _sku in initial.items():
                if _sku.sku == sku.sku:
                    error = {
                        'field': 'sku',
                        'error': 'SKU number already in use for this product.'
                    }
                    add_error(sku.errors, error)
                    add_error(_sku.errors, error)
            is_valid = False
        sku_code_processed.append(sku.sku)

        # conflict with any other SKU record for any other product?
        product_skus = ProductSKU.objects.exclude(product=product).filter(
            sku=sku.sku)
        if product_skus.count() > 0:
            sku.errors.append({
                'field':
                'sku',
                'error':
                'SKU number already in use by product \'%s\'.' %
                product_skus[0].product.title
            })
            is_valid = False

        return is_valid

    def _verify_barcode(self, product, sku, barcodes_processed, initial):
        """
        Verify that any assigned barcode does not exist twice in the system.
        """
        is_valid = True

        # empty barcode?
        if not sku.barcode:
            return is_valid

        # make sure that the barcode does not conflict with any product
        products = get_product_model().objects.filter(barcode=sku.barcode)
        if products.count() > 0:
            sku.errors.append({
                'field':
                'barcode',
                'error':
                'Barcode already in use by product \'%s\'.' % products[0].title
            })
            is_valid = False

        # conflicts with any other record we processed so far?
        if sku.barcode in barcodes_processed:
            for _, _sku in initial.items():
                if _sku.barcode == sku.barcode:
                    error = {
                        'field': 'barcode',
                        'error': 'Barcode already in use for this product.'
                    }
                    sku.errors.append(error)
                    _sku.errors.append(error)
            is_valid = False
        barcodes_processed.append(sku.barcode)

        # conflict with any other SKU record for any other product?
        product_skus = ProductSKU.objects.exclude(product=product).filter(
            barcode=sku.barcode)
        if product_skus.count() > 0:
            sku.errors.append({
                'field':
                'barcode',
                'error':
                'Barcode already in use by product \'%s\'.' %
                product_skus[0].product.title
            })
            is_valid = False

        return is_valid

    def delivery(self, request):
        product_id = request.GET.get('pk')
        product = get_object_or_404(get_product_model(), pk=product_id)

        # get general delivery options that are available
        options = DeliveryOption.objects.filter(enabled=True)

        # get available delivery options
        delivery_options = list(
            ProductDeliveryOption.objects.select_related(
                'delivery_option').filter(product=product))
        delivery_options_ids = [
            option.delivery_option.id for option in delivery_options
        ]

        # add missing options, so that each is convered
        for option in options:
            if option.id not in delivery_options_ids:
                assignment = ProductDeliveryOption()
                assignment.product = product
                assignment.delivery_option = option
                delivery_options.append(assignment)

        # dataset based on available options
        initial = [{
            'option_id': option.delivery_option.id,
            'deliver_uk': option.delivery_option.deliver_uk,
            'deliver_eu': option.delivery_option.deliver_eu,
            'deliver_world': option.delivery_option.deliver_world,
            'title': option.delivery_option.title,
            'uk': option.uk,
            'eu': option.eu,
            'world': option.world
        } for option in delivery_options]

        if request.method == 'POST':
            formset = DeliveryOptionFormset(request.POST, initial=initial)
        else:
            formset = DeliveryOptionFormset(initial=initial)

        if request.method == 'POST':
            if formset.is_valid():
                # delete all existing assignments
                assignments = ProductDeliveryOption.objects.filter(
                    product=product)
                for assignment in assignments:
                    request.changelog.delete(assignment)
                    assignment.delete()

                # create new assignments
                for form in formset.forms:
                    d = form.cleaned_data
                    for option in options:
                        if option.id == d.get('option_id'):
                            assignment = ProductDeliveryOption()
                            assignment.product = product
                            assignment.delivery_option = option
                            assignment.uk = d.get('uk')
                            assignment.eu = d.get('eu')
                            assignment.world = d.get('world')
                            assignment.save()
                            request.changelog.create(assignment)
                            break

                # commit, message and redirect
                request.changelog.commit(
                    'Delivery options for product <em>%s</em> updated.' %
                    product.title,
                    product,
                    flash=True)
                return self.redirect_to_index_or(request, 'delivery', product)
            else:
                print formset.errors

        return {
            'product': product,
            'delivery_options': delivery_options,
            'form': formset
        }

    def google_products(self, request):
        def prettify_xml(elem):
            """
            Return a pretty-printed XML string for the Element.
            """
            rough_string = tostring(elem)
            reparsed = minidom.parseString(rough_string)
            return reparsed.toprettyxml(indent='\t').encode('utf-8', 'replace')

        products = get_product_model().objects.filter(feed_google=True)
        root = Element('rss')
        root.attrib['xmlns:g'] = 'http://base.google.com/ns/1.0'
        root.attrib['version'] = '2.0'
        channel = SubElement(root, 'channel')
        title = SubElement(channel, 'title')
        title.text = request.settings.name
        link = SubElement(channel, 'link')
        link.text = settings.DOMAIN_NAME
        description = SubElement(channel, 'description')

        for p in products:
            # availability
            if p.is_available and not p.pre_order:
                txt_availability = 'in stock'
            elif p.pre_order:
                txt_availability = 'preorder'
            else:
                txt_availability = 'out of stock'

            # determine delivery charge by placing the product onto the basket
            basket = Basket()
            basket.add_item(p, None, 1)
            delivery_charge = basket.delivery

            # determine feed item attributes
            txt_id = unicode(p.id)
            txt_title = clean_unicode(p.title).strip()
            txt_link = p.get_absolute_url()
            txt_description = text_from_html(p.description, 5000)
            txt_condition = 'new'
            txt_price = '%.2f GBP' % p.price
            txt_google_category = p.category.google_product_category if p.category and p.category.google_product_category else None
            txt_category = p.category.get_taxonomy_path(
            ) if p.category else None
            txt_country = 'GB'
            txt_delivery_price = '%s %s' % (delivery_charge, 'GBP')
            txt_barcode = p.barcode.strip() if p.barcode else None
            txt_part_number = p.part_number.strip() if p.part_number else None
            txt_brand = p.get_brand_title()

            # create item
            item = SubElement(channel, 'item')

            # id
            _id = SubElement(item, 'g:id')
            _id.text = txt_id

            # title
            title = SubElement(item, 'title')
            title.text = txt_title

            # link/url
            link = SubElement(item, 'link')
            link.text = txt_link

            # main text
            description = SubElement(item, 'description')
            description.text = txt_description

            # condition
            condition = SubElement(item, 'g:condition')
            condition.text = txt_condition

            # price
            price = SubElement(item, 'g:price')
            price.text = txt_price

            # availability
            availability = SubElement(item, 'g:availability')
            availability.text = txt_availability

            # google shopping category
            if txt_google_category:
                gcategory = SubElement(item, 'g:google_product_category')
                gcategory.text = txt_google_category

            # product type
            if txt_category:
                category = SubElement(item, 'g:product_type')
                category.text = txt_category

            # shipping
            shipping = SubElement(item, 'g:shipping')

            # country
            country = SubElement(shipping, 'g:country')
            country.text = txt_country

            # delivery price
            delivery_price = SubElement(shipping, 'g:price')
            delivery_price.text = txt_delivery_price

            # barcode, must be a valid UPC-A (GTIN-12), EAN/JAN (GTIN-13)
            # or GTIN-14, so we need to have at least 12 characters.
            if txt_barcode:
                gtin = SubElement(item, 'g:gtin')
                gtin.text = txt_barcode

            # part number
            if txt_part_number:
                _mpn = SubElement(item, 'g:mpn')
                _mpn.text = txt_part_number

            # brand
            if txt_brand:
                brand = SubElement(item, 'g:brand')
                brand.text = txt_brand

            # image
            if p.image:
                image = SubElement(item, 'g:image_link')
                image.text = p.image.large_url

            # additional images
            if len(p.gallery) > 0:
                for m in p.gallery[:10]:
                    additional_image_link = SubElement(
                        item, 'g:additional_image_link')
                    additional_image_link.text = m.large_url

        # get temp. filename
        f = NamedTemporaryFile(delete=False)
        tmp_filename = f.name
        f.close()

        # create tmp file (utf-8)
        f = open(tmp_filename, 'w+b')
        f.write(prettify_xml(root))
        f.seek(0)

        # send response
        filename = 'google_products_%s.xml' % datetime.date.today().strftime(
            '%d_%m_%Y')
        response = HttpResponse(FileWrapper(f), content_type='text/plain')
        response['Content-Disposition'] = 'attachment; filename=%s' % filename
        return response
Exemple #25
0
    def sku(self, request):
        # get product
        product_id = request.GET.get('pk')
        product = get_object_or_404(get_product_model(), pk=product_id)

        # get varieties
        _varieties = Variety.objects.prefetch_related(
            Prefetch('options',
                     queryset=VarietyOption.objects.order_by('title'))
        ).filter(sku=True).exclude(options=None).exclude(
            style=Variety.STYLE_ATTRIBUTE).order_by('title').distinct()
        skus = ProductSKU.objects.filter(product=product)
        assigned_option_ids = [
            a.variety_option.id
            for a in VarietyAssignment.objects.select_related('variety_option')
            .filter(product=product, variety_option__variety__sku=True)
        ]

        # initial dataset currently present
        initial = {}
        for sku in skus:
            initial[sku.pk] = sku
            initial[sku.pk].errors = []

        # determine barcode system
        cms_settings = get_cms_settings()
        barcode_system = cms_settings.get_barcode_system(product)

        # create template form
        form_template = ProductSKUForm()
        form_template.configure(request, barcode_system)

        def has_var(prefix, name):
            return 'f-%s-%s' % (prefix, name) in request.POST

        def get_var(prefix, name, default=None):
            return request.POST.get('f-%s-%s' % (prefix, name), default)

        def get_int_var(prefix, name, default=None):
            return parse_int(get_var(prefix, name), default)

        # construct list of variety option names
        varieties = []
        variety_index = {}
        for variety in _varieties:
            variety_index[variety.id] = {
                'id': variety.id,
                'title': variety.title,
                'sku': variety.sku,
                'options': {}
            }

            item = {
                'id': variety.id,
                'title': variety.title,
                'sku': variety.sku,
                'options': [],
                'n_assigned_options': 0
            }
            for option in variety.options.all():
                variety_index[variety.id].get('options')[option.id] = {
                    'id': option.id,
                    'title': option.title,
                    'fullTitle':
                    '%s: <em>%s</em>' % (variety.title, option.title)
                }
                item.get('options').append({
                    'id':
                    option.id,
                    'title':
                    option.title,
                    'assigned':
                    option.id in assigned_option_ids
                })
                if option.pk in assigned_option_ids:
                    item['n_assigned_options'] += 1
            varieties.append(item)

        # sort varieties by number of assigned options, so that varieties that
        # have been assigned are at the top of the list. The rest remains sorted
        # alphabetically...
        varieties.sort(key=lambda x: -x.get('n_assigned_options', 0))

        # validation
        is_valid = True
        if request.method == 'POST':
            # process sku records
            prefixes = request.POST.getlist('skus')
            assigned_option_ids = []
            skus_to_save = []
            sku_code_processed = []
            barcodes_processed = []
            for index, prefix in enumerate(prefixes):
                # extract relevant informatioin from post for
                # individual combination
                _id = get_var(prefix, '_id')
                d = {
                    'enabled': get_var(prefix, 'enabled') == 'on',
                    'sku': get_var(prefix, 'sku'),
                    'barcode': get_var(prefix, 'barcode'),
                    'price': get_var(prefix, 'price'),
                    'stocklevel': get_int_var(prefix, 'stocklevel', 0)
                }

                # parse assigned variety options from request data
                n_variety_option = 1
                d['variety_options'] = []
                while len(d['variety_options']) <= 16:
                    _name = 'vo_%d' % n_variety_option
                    if has_var(prefix, _name):
                        d['variety_options'].append(get_int_var(prefix, _name))
                        n_variety_option += 1
                    else:
                        break

                # make sure that sku, barcode and price are None
                # instead of empty
                if _id == '': _id = None
                if d.get('sku') == '': d['sku'] = None
                if d.get('barcode') == '': d['barcode'] = None
                if d.get('price') == '': d['price'] = None

                # construct form based on this data and validate
                form = ProductSKUForm(d)
                form.configure(request, barcode_system)

                # get variety options
                variety_options = VarietyOption.objects.filter(
                    pk__in=d.get('variety_options'))

                # create or edit?
                sku = initial.get(_id, None)
                if sku is None:
                    sku = ProductSKU.objects.get_by_variety_options(
                        product, variety_options)

                    # still not found? -> create new item
                    if sku is None:
                        sku = ProductSKU()
                        sku.product = product

                # remember the sku record to be saved once we processed
                # everything. We will not save anything until everything
                # is considered to be valid.
                skus_to_save.append(sku)

                # mark any assigned variety options as selected, so that they
                # indeed remain selected, even if they have actually not been
                # properly assigned yet because if from errors for example
                for _variety in varieties:
                    _options = _variety.get('options')
                    for _option in _options:
                        for _assigned_option in variety_options:
                            if _option.get('id') == _assigned_option.pk:
                                _option['assigned'] = True
                                break

                # inject error information and keep track of error states
                sku.errors = []
                if form.is_valid():
                    # update data from from
                    d = form.cleaned_data
                else:
                    for field, error in form.errors.items():
                        sku.errors.append({'field': field, 'error': error[0]})
                    is_valid = False

                # copy original data or cleaned data
                sku.enabled = d.get('enabled', False)
                sku.sku = d.get('sku')
                sku.barcode = d.get('barcode')
                sku.price = d.get('price')
                sku.stocklevel = d.get('stocklevel', 0)

                # keep track of variety options that should be assigned due to
                # SKU's that are enabled
                if sku.enabled:
                    assigned_option_ids.extend(
                        [option.pk for option in variety_options])

                # set variety options (saved later)
                sku._variety_options = variety_options

                # verify uniqueness of the SKU code
                if not self._verify_sku(product, sku, sku_code_processed,
                                        initial):
                    is_valid = False

                # verify uniqueness of barcode
                if not self._verify_barcode(product, sku, barcodes_processed,
                                            initial):
                    is_valid = False

                # maintain changed data in initial data set, so that all
                # changes make theire way back into the view, even through
                # we might not have saved changes due to errors
                _id = ('idx_%d' % index) if sku.pk is None else sku.pk
                initial[_id] = sku

        # process if everything is valid
        if request.method == 'POST' and is_valid:
            # create missing option assignments
            assigned_option_ids = list(
                set(filter(lambda x: x is not None, assigned_option_ids)))
            for option_id in assigned_option_ids:
                try:
                    assignment = VarietyAssignment.objects.get(
                        product=product, variety_option__pk=option_id)
                except VarietyAssignment.DoesNotExist:
                    VarietyAssignment.objects.create(
                        product=product, variety_option_id=option_id)

            # remove deprecated option assignments
            deprecated_assignments = VarietyAssignment.objects.select_related(
                'variety_option').filter(
                    product=product,
                    variety_option__variety__sku=True).exclude(
                        variety_option__pk__in=assigned_option_ids)
            for deprecated_assignment in deprecated_assignments:
                deprecated_assignment.delete()

            # save changes to sku records. Null sku, so that we would not
            # collide when making updates
            sku_ids_saved = []
            for sku in skus_to_save:
                # save product sku itself
                sku._sku = sku.sku
                sku.sku = None
                sku.save()
                sku_ids_saved.append(sku.id)

                # assign and save variety options
                sku.variety_options = sku._variety_options

            # remove all previous SKU
            deprecated_skus = ProductSKU.objects.filter(
                product=product).exclude(pk__in=sku_ids_saved)
            for deprecated_sku in deprecated_skus:
                deprecated_sku.delete()

            # apply new sku names, which is now safe to do
            for sku in skus_to_save:
                if request.settings.sku_is_barcode:
                    sku.sku = sku.barcode
                else:
                    sku.sku = sku._sku
                sku.save()

        # redirect and message
        if request.method == 'POST' and is_valid:
            messages.add_message(
                request, messages.SUCCESS,
                'Product Varieties and SKUs saved for <em>%s</em>.' %
                product.title)

            if request.POST.get('cubane_save_and_continue') is None:
                return self._redirect(request, 'index')
            else:
                return self._redirect(request, 'sku', product)

        # materialise current initial data
        _initial = {}
        for _id, _sku in initial.items():
            _initial[_id] = model_to_dict(_sku)
            if _sku.pk is None:
                _initial[_id]['variety_options'] = [
                    option.pk for option in _sku._variety_options
                ]
            else:
                _initial[_id]['variety_options'] = [
                    option.pk for option in _sku.variety_options.all()
                ]
            _initial[_id]['errors'] = _sku.errors if hasattr(_sku,
                                                             'errors') else []

        # template context
        return {
            'product': product,
            'varieties': varieties,
            'initial': to_json(_initial),
            'variety_index_json': to_json(variety_index),
            'form_template': form_template
        }
Exemple #26
0
    def varieties_edit(self, request, product_id, variety_id):
        product = get_object_or_404(get_product_model(), pk=product_id)
        variety = get_object_or_404(Variety, pk=variety_id)
        options = list(variety.options.order_by('seq', 'id'))
        assignments = VarietyAssignment.objects.select_related(
            'variety_option').filter(product=product,
                                     variety_option__variety=variety)
        assignment_list = list(assignments)

        # dataset based on available options
        initial = [{
            'option_id': option.id,
            'title': option.title,
            'enabled': False,
            'offset_type': option.default_offset_type,
            'offset_value': option.default_offset_value,
            'text_label': option.text_label,
            'seq': option.seq,
            'option_enabled': option.enabled
        } for option in options]

        # update base dataset based on available assignments...
        for initial_option in initial:
            for assignment in assignment_list:
                if initial_option['option_id'] == assignment.variety_option.id:
                    initial_option['enabled'] = True
                    initial_option['option_enabled'] = True
                    initial_option['offset_type'] = assignment.offset_type
                    initial_option['offset_value'] = assignment.offset_value

        # remove options that are not currently assigned but disabled...
        initial = filter(lambda option: option.get('option_enabled'), initial)

        # sort by enabled state, then seq if we have a lot of varieties
        if len(initial) > 15:
            initial = sorted(initial,
                             key=lambda x: (-x.get('enabled', x.get('seq'))))

        # determine form class
        if variety.is_attribute:
            form_class = VarietyAttributeAssignmentFormset
        else:
            form_class = VarietyAssignmentFormset

        # create form
        if request.method == 'POST':
            formset = form_class(request.POST)
        else:
            formset = form_class(initial=initial)

        # validation
        if formset.is_valid():
            # delete existing assignments
            for assignment in assignments:
                request.changelog.delete(assignment)
                assignment.delete()

            # create new assignments
            for form in formset.forms:
                d = form.cleaned_data
                if d.get('enabled') == True:
                    for option in options:
                        if option.id == d.get('option_id'):
                            assignment = VarietyAssignment()
                            assignment.variety_option = option
                            assignment.product = product

                            if not variety.is_attribute:
                                assignment.offset_type = d.get('offset_type')
                                assignment.offset_value = d.get('offset_value')

                            assignment.save()
                            request.changelog.create(assignment)
                            break

            request.changelog.commit(
                'Variety Options for <em>%s</em> for product <em>%s</em> updated.'
                % (variety.title, product.title),
                product,
                flash=True)

            active_tab = request.POST.get('cubane_save_and_continue', '0')
            if not active_tab == '0':
                return self._redirect(request,
                                      'varieties.edit',
                                      args=[product.id, variety.id])
            else:
                return self._redirect(request, 'varieties', product)

        return {
            'product': product,
            'variety': variety,
            'form': formset,
        }
Exemple #27
0
 def items(self):
     return get_product_model().objects.exclude(category__enabled=False).exclude(draft=True).exclude(category_id__isnull=True)
Exemple #28
0
 def get_product_model(self):
     """
     Return the model for a shop product.
     """
     return get_product_model()
Exemple #29
0
    def import_products(self, data):
        """
        Import products (groups).
        """
        # group input data by product sku
        groups = {}
        for row in data:
            product_sku = row.get('product_sku')
            if product_sku not in groups:
                groups[product_sku] = {'rows': []}
            groups[product_sku].get('rows').append(row)

        # combine certain fields per group
        for group in groups.values():
            rows = group.get('rows')
            group['title'] = rows[0].get('title')
            group['category'] = rows[0].get('_category')
            group['images'] = []
            group['price'] = None
            for row in rows:
                try:
                    price = self._parse_decimal(row.get('price'))
                except ValueError:
                    self._field_error(
                        row, 'price',
                        'Unable to parse cell value \'<em>%s</em>\' as a decimal value for determining the lowest product price.'
                        % row.get('price'))
                    price = Decimal('0.00')

                row['price_parsed'] = price

                # lowest price is base price for product
                if row.get('price_parsed') < group.get('price') or group.get(
                        'price') is None:
                    group['price'] = row.get('price_parsed')

                # all image links without duplicates in the order given
                for image in row.get('images'):
                    if image not in group.get('images'):
                        group['images'].append(image)

        # process products (groups).
        product_model = get_product_model()
        for product_sku, group in groups.items():
            row = group.get('rows')[0]

            # title cannot be empty
            if not group.get('title'):
                continue

            try:
                product = product_model.objects.get(sku=product_sku)
            except product_model.DoesNotExist:
                product = product_model()
                product.sku = product_sku

            # title cannot already exist, unless for the product
            # we are working with
            title = group.get('title')
            products_with_same_title = product_model.objects.filter(
                title=title)
            if product.pk:
                products_with_same_title = products_with_same_title.exclude(
                    pk=product.pk)
            if products_with_same_title.count() > 0:
                self._field_error(
                    row, 'title',
                    'The product title \'%s\' is used multiple times. A product with the same title already exists with the SKUs \'%s\'.'
                    % (title, ', '.join([
                        '<em>%s</em>' % p.sku for p in products_with_same_title
                    ])))
                continue

            # slug cannot already exist, unless for the product
            # we are working with
            slug = slugify(title)
            products_with_same_slug = product_model.objects.filter(slug=slug)
            if product.pk:
                products_with_same_slug = products_with_same_slug.exclude(
                    pk=product.pk)
            if products_with_same_slug.count() > 0:
                self._field_error(
                    row, 'title',
                    'The product slug \'%s\', which has been automatically generated from the product title \'%s\' already exists. A product with the same title already exists with the SKUs \'%s\'.'
                    % (slug, title, ', '.join([
                        '<em>%s</em>' % p.sku for p in products_with_same_slug
                    ])))
                continue

            product.title = title
            product.slug = slug
            product.category = group.get('category')
            product.price = group.get('price')
            product.draft = False
            self.on_import_product(product, group)
            product.save()
            group['product'] = product

        return groups
Exemple #30
0
    def clean(self):
        """
        Verify that SKU is unique and valid.
        """
        d = super(InventoryForm, self).clean()

        product_model = get_product_model()

        # SKU
        sku = d.get('sku')
        if sku:
            # SKU must be unique across all product SKUs
            x = ProductSKU.objects.filter(sku=sku)
            if self._edit: x = x.exclude(pk=self._instance.pk)
            if x.count() > 0:
                self.field_error('sku', 'This SKU number already exists.')

            # SKU cannot match any product-specific SKU
            products = product_model.objects.filter(sku=sku)
            if products.count() > 0:
                self.field_error(
                    'sku', 'This SKU number already exists for product: %s' %
                    products[0])

        # barcode
        barcode = d.get('barcode')
        if barcode:
            # barcode must be unique across all product SKUs
            x = ProductSKU.objects.filter(barcode=barcode)
            if self._edit: x = x.exclude(pk=self._instance.pk)
            if x.count() > 0:
                self.field_error('barcode',
                                 'This barcode number already exists.')

            # barcode cannot match any product-specific barcode
            products = product_model.objects.filter(barcode=barcode)
            if products.count() > 0:
                self.field_error(
                    'barcode',
                    'This barcode number already exists for product: %s' %
                    products[0])

        product = d.get('product')
        if product:
            # product must be SKU enabled
            if not product.sku_enabled:
                self.field_error(
                    'product', 'This product is not enabled for SKU numbers.')

            variety_options = d.get('variety_options')
            if variety_options:
                # all varieties must be enabled for SKU usage
                for variety_option in variety_options:
                    if not variety_option.variety.sku:
                        self.field_error(
                            'variety_options',
                            'The variety \'%s\' is not enabled for SKU numbers (\'%s\').'
                            % (variety_option.variety, variety_option))

                # all variety options must have a different variety. We cannot
                # map to multiple options of the same variety...
                variety_ids = [option.variety.pk for option in variety_options]
                if len(set(variety_ids)) != len(variety_ids):
                    self.field_error(
                        'variety_options',
                        'A SKU number must be a unique combination of varieties and cannot map to multiple options of the same variety.'
                    )

                # combination of variety options must match all required
                # varieties
                x = ProductSKU.objects.filter(product=product, enabled=True)
                if self._edit: x = x.exclude(pk=self._instance.pk)
                if x.count() > 0:
                    # all required varieties must be assigned...
                    required_varieties = [
                        variety_option.variety
                        for variety_option in x[0].variety_options.all()
                    ]
                    required_variety_ids = [v.pk for v in required_varieties]
                    varieties = [option.variety for option in variety_options]
                    variety_ids = [v.pk for v in varieties]
                    for required_variety in required_varieties:
                        if required_variety.pk not in variety_ids:
                            self.field_error(
                                'variety_options',
                                'This SKU number must map to the required variety: \'%s\'.'
                                % (required_variety))

                    # we cannot have a variety that is not allowed...
                    for variety_option in variety_options:
                        if variety_option.variety.pk not in required_variety_ids:
                            self.field_error(
                                'variety_options',
                                'Variety option \'%s\' does not belong to \'%s\'.'
                                % (variety_option, ' or '.join(
                                    ['%s' % v for v in required_varieties])))

                # combination of variety options must be unique for this product
                x = ProductSKU.objects.filter(product=product, enabled=True)
                for variety_option in variety_options:
                    x = x.filter(variety_options=variety_option)
                if self._edit: x = x.exclude(pk=self._instance.pk)
                if x.count() > 0:
                    self.field_error(
                        'variety_options',
                        'This combination of variety options already exists.')

        return d