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)
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
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()
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
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())
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])) })
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 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''
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
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)
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 ''
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 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
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 }
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 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 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)
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 _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))
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' }
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
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
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 }
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
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 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, }
def items(self): return get_product_model().objects.exclude(category__enabled=False).exclude(draft=True).exclude(category_id__isnull=True)
def get_product_model(self): """ Return the model for a shop product. """ return get_product_model()
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
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