root/trunk/satchmo/product/models.py

Revision 207 (by chris, 01/10/07 20:48:07)

Making changes to support internationalization

"""
Base model used for products.  Stores hierarchical categories
as well as individual product level information which includes
options.
"""

from django.db import models
from django.core import validators
from sets import Set
from satchmo.thumbnail.field import ImageWithThumbnailField
from django.conf import settings
from satchmo.tax.models import TaxClass
import os
from decimal import Decimal
from django.utils.translation import gettext_lazy as _

# Create your models here.

class Category(models.Model):
    """
    Basic hierarchical category model for storing products
    """
    name = models.CharField(_("Name"), core=True, maxlength=200)
    slug = models.SlugField(prepopulate_from=('name',),help_text=_("Used for URLs"))
    parent = models.ForeignKey('self', blank=True, null=True, related_name='child')
    meta = models.TextField(_("Meta Description"), blank=True, null=True, help_text=_("Meta description for this category"))
    description = models.TextField(_("Description"), blank=True,help_text="Optional")
        
    def _recurse_for_parents_slug(self, cat_obj):
        #This is used for the urls
        p_list = []
        if cat_obj.parent_id:
            p = cat_obj.parent
            p_list.append(p.slug)
            more = self._recurse_for_parents_slug(p)
            p_list.extend(more)
        if cat_obj == self and p_list:
            p_list.reverse()
        return p_list

    def get_absolute_url(self):
        p_list = self._recurse_for_parents_slug(self)
        p_list.append(self.slug)
        baseurl = settings.SHOP_BASE + "/category/"
        return baseurl + "/".join(p_list)        
                
    def _recurse_for_parents_name(self, cat_obj):
        #This is used for the visual display & save validation
        p_list = []
        if cat_obj.parent_id:
            p = cat_obj.parent
            p_list.append(p.name)
            more = self._recurse_for_parents_name(p)
            p_list.extend(more)
        if cat_obj == self and p_list:
            p_list.reverse()
        return p_list
                
    def get_separator(self):
        return ' :: '
        
    def _parents_repr(self):
        p_list = self._recurse_for_parents_name(self)
        return self.get_separator().join(p_list)
    _parents_repr.short_description = "Category parents"
    
    def _recurse_for_parents_name_url(self, cat_obj):
        #Get all the absolute urls and names (for use in site navigation)
        p_list = []
        url_list = []
        if cat_obj.parent_id:
            p = cat_obj.parent
            p_list.append(p.name)
            url_list.append(p.get_absolute_url())
            more, url = self._recurse_for_parents_name_url(p)
            p_list.extend(more)
            url_list.extend(url)
        if cat_obj == self and p_list:
            p_list.reverse()
            url_list.reverse()
        return p_list, url_list

    def get_url_name(self):
        #Get a list of the url to display and the actual urls
        p_list, url_list = self._recurse_for_parents_name_url(self)
        p_list.append(self.name)
        url_list.append(self.get_absolute_url())
        return zip(p_list, url_list)
    
    def __str__(self):
        p_list = self._recurse_for_parents_name(self)
        p_list.append(self.name)
        return self.get_separator().join(p_list)
        
    def save(self):
        p_list = self._recurse_for_parents_name(self)
        if self.name in p_list:
            raise validators.ValidationError(_("You must not save a category in itself!"))
        super(Category, self).save()
        
    def _flatten(self, L):
        """
        Taken from a python newsgroup post
        """
        if type(L) != type([]): return [L]
        if L == []: return L
        return self._flatten(L[0]) + self._flatten(L[1:])
            
    def _recurse_for_children(self, node):
        children = []
        children.append(node)
        for child in node.child.all():
            children_list = self._recurse_for_children(child)
            children.append(children_list)
        return(children)

    def get_all_children(self):
        """
        Gets a list of all of the children categories.
        """
        children_list = self._recurse_for_children(self)
        flat_list = self._flatten(children_list[1:])
        return(flat_list)
    
    class Admin:
        list_display = ('name', '_parents_repr')
        ordering = ['name']
        
    class Meta:
        verbose_name = _("Category")
        verbose_name_plural = _("Categories")

class OptionGroup(models.Model):
    """
    A set of options that can be applied to an item.
    Examples - Size, Color, Shape, etc
    """
    name = models.CharField(_("Name of Option Group"),maxlength = 50, core=True, help_text=_('This will be the text displayed on the product page'),)
    description = models.CharField(_("Detailed Description"),maxlength = 100, blank=True, help_text=_('Further description of this group i.e. shirt size vs shoe size'),)
    sort_order = models.IntegerField(_("Sort Order"), help_text=_("The order they will be displayed on the screen"))
    
    def __str__(self):
        if self.description:
            return ("%s - %s" % (self.name, self.description))
        else:
            return self.name
    
    class Admin:
        pass
        
    class Meta:
        ordering = ['sort_order']
        verbose_name = _("Option Group")
        verbose_name_plural = _("Option Groups")
        
class Item(models.Model):
    """
    The basic product being sold in the store.  This is what the customer sees.
    """
    category = models.ManyToManyField(Category, filter_interface=True)
    verbose_name = models.CharField(_("Full Name"), maxlength=255)
    short_name = models.SlugField(_("Slug Name"), prepopulate_from=("verbose_name",), unique=True, help_text=_("This is a short, descriptive name of the shirt that will be used in the URL link to this item"))
    description = models.TextField(_("Description of product"), help_text=_("This field can contain HTML and should be a few paragraphs explaining the background of the product, and anything that would help the potential customer make their purchase."))
    meta = models.TextField(maxlength=200, blank=True, null=True, help_text=_("Meta description for this item"))
    date_added = models.DateField(null=True, blank=True, auto_now_add=True)
    active = models.BooleanField(_("Is product active?"), default=True, help_text=_("This will determine whether or not this product will appear on the site"))
    featured = models.BooleanField(_("Featured Item"), default=False, help_text=_("Featured items will show on the front page"))
    option_group = models.ManyToManyField(OptionGroup, filter_interface=True, blank=True)
    base_price = models.FloatField(_("Base Price"), max_digits=6, decimal_places=2)
    weight = models.FloatField(_("Weight"), max_digits=6, decimal_places=2, null=True, blank=True)
    length = models.FloatField(_("Length"), max_digits=6, decimal_places=2, null=True, blank=True)
    width = models.FloatField(_("Width"), max_digits=6, decimal_places=2, null=True, blank=True)
    height = models.FloatField(_("Height"), max_digits=6, decimal_places=2, null=True, blank=True)
    create_subs = models.BooleanField(_("Create Sub Items"), default=False, help_text =_("Create new sub-items"))
    relatedItems = models.ManyToManyField('self', blank=True, null=True, related_name='related')
    alsoPurchased = models.ManyToManyField('self', blank=True, null=True, related_name='previouslyPurchased')
    taxable = models.BooleanField(default=False)
    taxClass = models.ForeignKey(TaxClass, blank=True, null=True, help_text=_("If it is taxable, what kind of tax?"))    
    
    def __str__(self):
        return self.short_name 
    
    def _get_price(self):
        # On some systems, the price was not getting set as a decimal type.  This ensures that it does.
        return Decimal(self.base_price)
    price = property(_get_price)
    
    def _get_mainImage(self):
        if self.itemimage_set.count() > 0:
            return(self.itemimage_set.order_by('sort')[0])
        else:
            return(False)
    main_image = property(_get_mainImage)
    
    def _cross_list(self, sequences):
        """
        Code taken from the Python cookbook v.2 (19.9 - Looping through the cross-product of multiple iterators)
        This is used to create all the sub items associated with an item
        """
        result =[[]]
        for seq in sequences:
            result = [sublist+[item] for sublist in result for item in seq]
        return result
    
    def create_subitems(self):
        """
        Get a list of all the optiongroups applied to this object
        Create all combinations of the options and create subitems
        """
        sublist = []
        masterlist = []
        #Create a list of all the options & create all combos of the options
        for opt in self.option_group.all():
            for value in opt.optionitem_set.all():
                sublist.append(value)
            masterlist.append(sublist)
            sublist = []
        combinedlist = self._cross_list(masterlist)
        #Create new sub_items for each combo
        for options in combinedlist:
            price_delta = 0
            sub = SubItem(item=self, items_in_stock=0)
            sub.save()
            s1 = Set()
            for option in options:
                sub.options.add(option)
                optionValue = "%s-%s" % (option.optionGroup.id, option.value)
                s1.add(optionValue)
                sub.save()
            #If the option already exists, lets make sure there are no dupes
            #TODO: Check before we create the item
            if self.get_sub_item_count(s1) > 1:              
                sub.delete()
        return(True)
    
    def get_sub_item(self, optionSet):
        for sub in self.subitem_set.all():
            if sub.option_values == optionSet:
                return(sub)
        return(None)
    
    def get_sub_item_count(self, optionSet):
        count = 0
        for sub in self.subitem_set.all():
            if sub.option_values == optionSet:
                count+=1
        return count
    
    def save(self):
        '''
        Right now this only works if you save the suboptions, then go back and choose to create the subitems
        '''
        #super(Item,self).save()
        if self.create_subs:
            self.create_subitems()
            self.create_subs = False
        super(Item, self).save()
    
    def get_absolute_url(self):
        return "%s/product/%s" % (settings.SHOP_BASE,self.short_name)

    
    class Admin: 
        list_display = ('verbose_name', 'active')
        fields = (
        (None, {'fields': ('category','verbose_name','short_name','description','date_added','active','featured','base_price',)}),
        ('Meta Data', {'fields': ('meta',), 'classes': 'collapse'}),
        ('Item Dimensions', {'fields': (('length', 'width','height',),'weight'), 'classes': 'collapse'}),
        ('Options', {'fields': ('option_group','create_subs',),}), 
        ('Tax', {'fields':('taxable', 'taxClass'), 'classes': 'collapse'}),
        ('Related Products', {'fields':('relatedItems','alsoPurchased'),'classes':'collapse'}), 
        )
        list_filter = ('category',)
        
    class Meta:
        verbose_name = _("Master Product")
        verbose_name_plural = _("Master Products")
        
class ItemImage(models.Model):
    """
    A picture of an item.  Can have many pictures associated with an item.
    Thumbnails are automatically created.
    """
    item = models.ForeignKey(Item, edit_inline=models.TABULAR, num_in_admin=3)
    picture = ImageWithThumbnailField(upload_to="./images") #Media root is automatically appended
    caption = models.CharField(_("Optional caption"),maxlength=100,null=True, blank=True)
    sort = models.IntegerField(_("Sort Order"), help_text=_("Leave blank to delete"), core=True)
    
    def __str__(self):
        return "Picture of %s" % self.item.short_name
        
    class Meta:
        ordering = ['sort']
        verbose_name = _("Product Image")
        verbose_name_plural = _("Product Images")
        
class OptionItem(models.Model):
    """
    These are the actual items in and OptionGroup.  If the OptionGroup is Size, then an OptionItem
    would be Small.
    """
    optionGroup = models.ForeignKey(OptionGroup, edit_inline=models.TABULAR, num_in_admin=5)
    name = models.CharField(_("Display value"), maxlength = 50, core=True)
    value = models.CharField(_("Stored value"), prepopulate_from=("name",), maxlength = 50)
    price_change = models.FloatField(_("Price Change"), null=True, blank=True, 
                                    help_text=_("This is the price differential for this option"), max_digits=4, decimal_places=2)
    displayOrder = models.IntegerField(_("Display Order"))
  
    def __str__(self):
        return self.name
        
    class Meta:
        ordering = ['displayOrder']
        verbose_name = _("Option Item")
        verbose_name_plural = _("Option Items")
        
class SubItem(models.Model):
    """
    The unique inventoriable item.  For instance, if a shirt has a size and color, then
    only 1 SubItem would have Size=Small and Color=Black
    """
    item = models.ForeignKey(Item)
    items_in_stock = models.IntegerField(_("Number in stock"), core=True)
    weight = models.FloatField(_("Weight"), max_digits=6, decimal_places=2, null=True, blank=True)
    length = models.FloatField(_("Length"), max_digits=6, decimal_places=2, null=True, blank=True)
    width = models.FloatField(_("Width"), max_digits=6, decimal_places=2, null=True, blank=True)
    height = models.FloatField(_("Height"), max_digits=6, decimal_places=2, null=True, blank=True)
    options = models.ManyToManyField(OptionItem, filter_interface=True, null=True, blank=True)
    
    def _get_optionName(self):
        "Returns the options in a human readable form"
        if self.options.count() == 0:
            return self.item.verbose_name
        output = self.item.verbose_name + " ( "
        numProcessed = 0
        # We want the options to be sorted in a consistent manner
        optionDict = dict([(sub.optionGroup.sort_order, sub) for sub in self.options.all()])
        for optionNum in sorted(optionDict.keys()):
            numProcessed += 1
            if numProcessed == self.options.count():
                output += optionDict[optionNum].name
            else:
                output += optionDict[optionNum].name + "/"
        output += " )"
        return output
    full_name = property(_get_optionName)
    
    def _get_fullPrice(self):
        price_delta = Decimal("0.0")
        for option in self.options.all():
            if option.price_change:
                price_delta += Decimal(option.price_change)
        return(self.item.price + price_delta)
    unit_price = property(_get_fullPrice)
    
    def _get_optionValues(self):
        """
        Return a set of all the valid options for this sub item.  
        A set makes sure we don't have to worry about ordering
        """
        output = Set()
        for option in self.options.all():
            outvalue = "%s-%s" % (option.optionGroup.id,option.value)
            output.add(outvalue)
        return(output)
    option_values = property(_get_optionValues)
    
    def _check_optionParents(self):
        groupList = []
        for option in self.options.all():
            if option.optionGroup.id in groupList:
                return(True)
            else:
                groupList.append(option.optionGroup.id)
        return(False)
            
    
    def in_stock(self):
        if self.items_in_stock > 0:
            return True
        else:
            return False;

    def __str__(self):
        return self.full_name
    
    def isValidOption(self, field_data, all_data):
        raise validators.ValidationError, _("Two options from the same option group can not be applied to an item.")
    
    #def save(self):
    #    super(Sub_Item, self).save()
    #    if self._check_optionParents():
    #        super(Sub_Item, self).delete()
    #        raise validators.ValidationError, "Two options from the same option group can not be applied to an item."
    #    else:
    #        super(Sub_Item, self).save()
    
    class Admin:
        list_display = ('full_name', 'unit_price', 'items_in_stock')
        list_filter = ('item',)
        fields = (
        (None, {'fields': ('item','items_in_stock',)}),
        ('Item Dimensions', {'fields': (('length', 'width','height',),'weight'), 'classes': 'collapse'}),
        ('Options', {'fields': ('options',),}),       
        )

    class Meta:
        verbose_name = _("Individual Product")
        verbose_name_plural = _("Individual Products")
Note: See TracBrowser for help on using the browser.