Sasha Vincic avatar Sasha Vincic committed 613475d Merge

merge from tip

Comments (0)

Files changed (96)

 949a4734af33a268c0726789d0ba33ac2f2881b2 0.6.2
 d1663e27f391f22f79d75fb2f03247dc7ef2b013 0.6.3
 00158b8e01bd7c37d0e6551097cc444df6f4784e 0.6.4
+a24a4a078c3b55f52e2db47423d10b4e6912e4a4 0.6.5
+bef2bdb679e11e06f08e6a9a9ce3d64fa987e7fc 0.6.6
+b392197d44095112932a2d1f7c6b39e403141534 0.6.7
 Changes
 =======
 
+0.6.6 (2012-02-09)
+------------------
+
+* Bugfix: fixed url for Pages at breadcrums (Maciej Wiśniowski)
+* Bugfix: display sale price at category products page (Maciej Wiśniowski)
+* Bugfix: fix product pagination (Maciej Wiśniowski)
+* Bugfix: added short_description to Category management UI
+* Bugfix: display category descriptions
+* Bugfix: fixed template selection; issue #134
+* Improvement: allow easy modification of category/product templates (Maciej Wiśniowski)
+* Updated polish translations (Maciej Wiśniowski)
+
+0.6.5 (2012-02-03)
+------------------
+
+* Bugfix: added csrftoken for rating mails (Maciej Wiśniowski)
+* Bugfix: fixed ImageWithThumbsField (Maciej Wiśniowski)
+* Updated romanian translations (olimpiu)
+* Updated polish translations (Maciej Wiśniowski)
+
 0.6.4 (2012-01-08)
 ------------------
 
 =======
 
 For the complete history please look into HISTORY.txt
-=======

docs/developer/api.rst

 API
 ===
 
-Plugins
-=======
+Plug-ins
+========
 
 .. _order_number_generator:
 

docs/developer/howtos/how_to_add_product_pricing.rst

 If you do not know how to do this, please refer to the excellent `Django
 tutorial <http://docs.djangoproject.com/en/dev/intro/tutorial01/>`_.
 
-Implement the pricecalculator class
-===================================
+Implement the price calculator class
+====================================
 
 Within ``__init__.py`` file of your application (or anywhere you choose) create
 a class that inherits from lfs.plugins.PricingCalculator and implement all

docs/developer/howtos/how_to_addresses.rst

 How to use localized addresses
 ==============================
 
-Address localisation is turned on by default in LFS. To turn off Address l10n
+Address localization is turned on by default in LFS. To turn off Address l10n
 in settings.py set::
 
     POSTAL_ADDRESS_L10N = False

docs/developer/howtos/how_to_shipping_price.rst

 
 #. Add your application to the PYTHONPATH.
 
-#. Add the class to the :ref:`LFS_SHIPPING_PRICE_CALCULATORS
+#. Add the class to the :ref:`LFS_SHIPPING_METHOD_PRICE_CALCULATORS
    <settings_lfs_shipping_price_calculators>` setting.
 
 #. If your are using models (which is completely up to you), add the application

docs/developer/settings.rst

 
 .. _settings_plugins:
 
-Plugins
+Plug-ins
 ========
 
 .. _settings_lfs_order_numbers_generators:
 
 .. _settings_lfs_shipping_price_calculators:
 
-LFS_SHIPPING_PRICE_CALCULATORS
+LFS_SHIPPING_METHOD_PRICE_CALCULATORS
     List of list of available shipping method price calculators, whereas the
     first entry is the dotted name to a ShippingMethodPriceCalculator and the
     second entry is the name, which is displayed. These are provided for
     selection within the shipping method. LFS is shipped with following
     entries::
 
-        LFS_SHIPPING_PRICE_CALCULATORS = [
+        LFS_SHIPPING_METHOD_PRICE_CALCULATORS = [
             ["lfs.shipping.GrossShippingMethodPriceCalculator", _(u'Price includes tax')],
             ["lfs.shipping.NetShippingMethodPriceCalculator", _(u'Price excludes tax')],
         ]
     The amount of recent products which are displayed within the recent
     products portlet, e.g. 3.
 
+.. _settings_addresses:
+
+Addresses
+=========
+
+LFS_INVOICE_EMAIL_REQUIRED
+    If True the e-mail address of the invoice address is required.
+
+LFS_INVOICE_PHONE_REQUIRED
+    If True the phone of the invoice address is required.
+
+LFS_INVOICE_COMPANY_NAME_REQUIRED
+    If True the company name of the invoice address is required.
+
+LFS_SHIPPING_PHONE_REQUIRED
+    If True the phone of the shipping address is required.
 
 .. _settings_units:
 
 
 .. _settings_email:
 
-E-Mail
-======
+E-Mails
+=======
 
 LFS_SEND_ORDER_MAIL_ON_CHECKOUT
     If true, an e-mail with the order details is send to the customer after
Add a comment to this file

docs/images/wysiwyg_toolbar.png

Added
New image
    user/concepts/marketing.rst
    user/concepts/reviews.rst
    user/concepts/pages.rst
+   user/concepts/images.rst
 
 .. _index_users_management_interface:
 

docs/spelling_wordlist.txt

 django
 english
 github
+gros
 gz
 hg
 html
 Indices
 init
+internet
 JMeter
 JMeter
 jQuery
 paypal
 percentaged
 plugin
+plugins
 Portlet
 portlet
 Portlets

docs/user/concepts/images.rst

+.. index:: Images, Global images
+
+.. _images_concepts:
+
+======
+Images
+======
+
+This section describes the concepts of images.
+
+.. _images_concepts_global_images:
+
+Overview
+========
+
+LFS provides two kinds of images: product images and global images.
+
+Product images
+==============
+
+Product images managed within the :ref:`images tab <products_management_images>`
+of a product and are attached to a single product. They are displayed
+automatically within the detail view of the product and the first image also
+within the category overview view.
+
+Global images
+==============
+
+Global images are managed on a :doc:`central place
+</user/management/shop/images>` and can be easily embedded within any :term:`WYSIWYG`
+text field within LFS (e.g. within a :doc:`page </user/concepts/pages>`).
+
+To do that one just  have to click on the image icon of the :term:`WYSIWYG`
+editor (see below), select the image in question, adapt the class and size of
+the image and click on the ``insert image`` button.
+
+.. image:: /images/wysiwyg_toolbar.*
+
+See also
+========
+
+* :doc:`Global image management </user/management/shop/images>`
+* :ref:`Product images management <products_management_images>`

docs/user/management/catalog/products.rst

 Site Actions
 ============
 
-Add Product
+Add product
     Adds a product to the catalog.
 
-Delete Product
+Delete product
     Deletes the current displayed product.
 
-View Product
+View product
     Opens the product in a pop-up window to quickly check the appearance of the
     product without leaving the :term:`LMI`.
 
-Goto Product
+Goto product
     Leaves the :term:`LMI` and goes to customer view of the current displayed
     product.
 
-Product Type
+Product type
     Select box to change the :ref:`type of the product <product-types-label>`.
 
 .. _products_management_tabs:
 Slug
     The last part of the product's URL. This must be unique.
 
-Redirect To
+Redirect to
     If the product is not active and this field is filled the user is redirected
     to the given URL. This might be useful if your product has been indexed by
     search engines but is not available any more. Note: super users are not
 SKU
     Your unique product id.
 
-SKU Manufacturer
+SKU manufacturer
     The unique product id of the manufacturer (external SKU).
 
 Price
     dependents on the selected price calculator for this product (see
     below).
 
+Price unit
+    The price unit of the product. This is displayed after the price of the
+    product within the product detail view and the category products view.
+
 Tax
     The tax rate of the product. Whether this is included or excluded
     dependents on the selected price calculator for this product (see
     below).
 
-Price Calculator
+Price calculator
     Determines how the product price is calculated using the product price and
     tax stored in the database. If you leave this field blank, your pricing
     calculator will default to the shop :ref:`price calculator
     means the product price in the database includes tax and ``Price Excludes
     Tax``, which means the product price in the database excludes tax.
 
-For Sale
-    If the check box is activated the entered for sale price is active. On all
+For sale
+    If the checkbox is activated the entered for sale price is active. On all
     views the standard price is displayed stroked and the for sale price is
     displayed emphasized.
 
-Unit
-    The unit of the product. This is displayed after the price of the product.
+    For variants following is true:
 
-Price Unit
-    The price unit of the product. This is displayed before the quantity field
-    of the product.
+    Standard
+        Inherits the ``for sale`` state of the base article.
 
-Type of Quantity Field
+    Yes
+        Variant is for sale.
+
+    No
+        Variant is not for sale.
+
+Quantity field unit
+    This is displayed before the quantity field of the product within the
+    product detail view and after the product amount within cart and order
+    items.
+
+Type of quantity field
     There are three types of quantity fields: ``Integer``, which means the
     quantity must be an integer and all decimal places are ignored. ``Decimal
-    0.1``, which means the quantity must be a decimal number with one decimal
-    place, more decimal places are ignored. ``Decimal 0.01``, which means the
-    quantity must be a decimal number with two decimal places, more decimal
-    places are ignored. If no type is given the default type is taken, which is
-    ``Integer``.
+    0.1``, which means the quantity must be a decimal number with one place and
+    more decimal places are ignored. ``Decimal 0.01``, which means the quantity
+    must be a decimal number with two places andMore decimal places are ignored.
 
 Active base price
     If this is activated the base price of the product is displayed within
-    product detail view and category overview view.
+    product detail view and category products view.
+
+    For variants following is true:
+
+    Standard
+        Inherits the activate base price from the bae article. Values for ``base
+        price unit`` and ``base price amount`` are taken from the base article.
+
+    Yes
+        Base price is activated. Values for ``base price unit`` and ``base price
+        amount`` are taken from the variant.
+
+    No
+        Base price is deactivated.
 
 Base price unit
-    The unit of the base price. This is displayed after the base price of the
-    product.
+    This unit is displayed after the base price of the product.
 
 Base price amount
     The amount, which is used to calculate the base price of the product. The
 
          base price = price / base price amount
 
-Short Description
+Short description
     A short description of the product. This is displayed within overviews
     like categories or the search result page.
 
 
 .. index:: Static Block
 
-Static Block
+Static block
     An optional static block which displayed on top of the product view.
 
 .. index:: Template
 
-Product Template
+Product template
     The selected product template decides how the content of the product is
     structured.
 
     You can also :doc:`assign products to categories
     </user/management/catalog/categories>`.
 
-.. _product-images-label:
+.. _products_management_images:
 
 Images
 ------
 is the default image of the product and is also displayed on overviews like
 the category detail view or search results view.
 
-Add Images
+Add images
     Click on the ``Select images`` button and select as many images as you want
     within your browsers pop-up window. You can use shift click to select a
     range of images at once and ctrl (cmd for apple users) click to select
     more images. Now click on open to start the upload process. You will now
     see a progress bar meanwhile your images are being uploaded.
 
-Update Images
+Update images
     To update the images just change the Title and the position of all products
     you want to change and click on the ``Update`` button.
 
-Move Images
+Move images
     To move images just click on the up or down arrow beside the image.
 
 Delete Images
-    To delete images select the check boxes beside all images you want to delete
+    To delete images select the checkboxes beside all images you want to delete
     and click the ``Delete`` button.
 
 .. _products_management_attachments:
     more images. Click on select to start the upload process. You will now
     see a progress indicator meanwhile your images are being uploaded.
 
-Update Attachments
+Update attachments
     To update the images just change the Title and/or the position of all
     products you want to change and click on the ``Update`` button.
 
-Move Attachments
+Move attachments
     To move attachments you just click on the up or down arrows beside the
     attachment.
 
-Delete Attachments
-    To delete attachments select the check boxes beside all images you want to
+Delete attachments
+    To delete attachments select the checkboxes beside all images you want to
     delete and click the ``Delete`` button.
 
 .. _products_management_accessories:
 
 Within this tab you can manage the accessories of this product.
 
-Add Accessories
+Add accessories
 ^^^^^^^^^^^^^^^
 
-Within the ``Selectable Products`` section select all check box beside the
+Within the ``Selectable Products`` section select all checkbox beside the
 product you want to add as accessory to the product and click on ``Add To
 Accessories``.
 
     You can filter the selectable products by name and category with the input
     fields on top of the ``Selectable Products`` section.
 
-Update Accessories
+Update accessories
 ^^^^^^^^^^^^^^^^^^
 
 Within the ``Selected Products`` section change the values you want and click
     The entered quantity is displayed next to the accessory. The shop customer
     can only add the given quantity to the cart.
 
-Remove Accessories
+Remove accessories
 ^^^^^^^^^^^^^^^^^^
 
-Within the ``Selected Products`` section select all check boxes beside the
+Within the ``Selected Products`` section select all checkboxes beside the
 products you want to remove from the product and click on ``Remove From
 Accessories``.
 
 .. _products_management_related_products:
 
-Related Products
+Related products
 ----------------
 
 Within this tab you can manage the related products of this product.
 
-Add Related Products
+Add related products
 ^^^^^^^^^^^^^^^^^^^^
 
-Within the ``Selectable Products`` section select all check box beside the
+Within the ``Selectable Products`` section select all checkbox beside the
 product you want to add as related products to the product and click on
 ``Add To Related Products``.
 
     You can filter the selectable products by name and category with the input
     fields on top of the ``Selectable Products`` section.
 
-Remove Related Products
+Remove related products
 ^^^^^^^^^^^^^^^^^^^^^^^
 
-Within the ``Selected Products`` section select all check boxes beside the
+Within the ``Selected Products`` section select all checkboxes beside the
 products you want to remove from the product and click on ``Remove From Related
 Prouducts``.
 
 Length
     The length of the product.
 
-Stock Data
+Stock data
 ^^^^^^^^^^
 
 Deliverable
     customer sees the product but he is not able to add the product to the
     cart.
 
-Manual Delivery Time
+Manual delivery time
     By default the delivery time is calculated automatically by the currently
     valid shipping method for this product. With this field the shop owner can
     overwrite this behavior and can put in a manual delivery time.
 
-Manage Stock Amount
+Manage stock amount
     If this is checked the stock amount will be decreased when the product
     has been bought. Additionally the maximum amount which can be bought is
     the number in ``Stock amount`` (see below).
 
-Stock Amount
+Stock amount
     The available amount of the product in stock.
 
-Order Time
+Order time
     Duration from ordering the product to being in stock again (when it is out
     of stock).
 
-Ordered At
+Ordered at
     The date when the **shop owner** has ordered the product.
 
 .. note::
     If ``Order time`` and ``Order at`` is given the total ``delivery time`` is
     calculated based on this two fields and the default ``Delivery time``.
 
-Packaging Unit
-^^^^^^^^^^^^^^
+Packing
+^^^^^^^
 
-Active Packing Unit
-    If this is checked the product can only be sold in packages.
+Active packing
+    If this is checked the product can only be sold in packings.
 
-Packing Unit
-    Amount of products per package.
+    For variants following is true:
 
-Unit:
-    The unit of the package, for instance ``package`` or ``set``.
+    Standard
+        Inherits the packing state from the base article. Values for ``packing
+        amount`` and ``packing unit`` are taken from the base article.
+
+    Yes
+        Packing is activated. Values for ``packing amount`` and ``packing unit``
+        are taken from the variant.
+
+    No
+        Packing is deactivated.
+
+Packing amount
+    Amount of products per packing.
+
+Packing unit:
+    The unit of the packing. This is displayed after the packing amount.
 
 .. index:: SEO
 
 for all usual HTML meta data fields. However LFS provides some reasonable
 default values for all fields.
 
-Meta Title
+Meta title
     This is displayed within the meta title tag of the product's HTML tags. By
     default the name of the product is used.
 
-Meta Keywords
+Meta keywords
     This is displayed within the meta keywords tag of the product's detail view.
     By default the short description of the product is used.
 
-Meta Description
+Meta description
     This is displayed within the meta description tag of the product's  detail
     view. By default the short description of the product is used.
 
 
 This tab is used to assign :term:`portlets` to the product.
 
-Blocked Parent Slots
+Blocked parent slots
     By default portlets are inherited from the current category. To block
     portlets check the regarding slots and click on the ``Save blocked parent
     slots`` button.
     click on the red cross beside the portlet. You can also change the position
     of the portlets by clicking on the up and down arrows beside the portlets.
 
-Add new Portlet
+Add new portlet
     In order to add a portlet to the product select the type of portlet and
     click on ``Add portlet``.
 

docs/user/management/shop/images.rst

+.. index:: Images, Global images
+
+.. _global_images_management:
+
+======
+Images
+======
+
+This section describes the management interface for :ref:`global images
+<images_concepts_global_images>`.
+
+Data
+====
+
+Add images
+    Click on the ``Select images`` button and select as many images as you want
+    within your browsers pop-up window. You can use shift click to select a
+    range of images at once and ctrl (cmd for apple users) click to select
+    more images. Now click on open to start the upload process. You will now
+    see a progress bar meanwhile your images are being uploaded.
+
+Delete Images
+    To delete images select the checkboxes above all images you want to delete
+    and click the ``Delete`` button. Alternative you can click on the ``Toggle
+    selection`` button in order to inverse the current selection.
+
+See also
+========
+
+* :doc:`Images concept </user/concepts/images>`

docs/user/management/shop/index.rst

    shipping_methods.rst
    product_taxes.rst
    customer_taxes.rst
+   images.rst

File contents unchanged.

lfs/cart/tests.py

         request.session = self.session
         request.user = self.user
 
-        # This need to result in a message to the customer
-        result = add_to_cart(request)
-        self.failIf(result.cookies.get("message").__str__().find("Sorry%2C%20but%20%27Product%201%27%20is%20not%20available%20anymore.") == -1)
+        self.assertRaises(Http404, add_to_cart, request)
 
         # But no message if product is ordered ...
         self.p1.order_time = self.dt

lfs/cart/views.py

     product = lfs_get_object_or_404(Product, pk=product_id)
 
     # Only active and deliverable products can be added to the cart.
-    if (product.is_active() and product.deliverable) == False:
+    if (product.is_active() and product.is_deliverable()) == False:
         raise Http404()
 
     try:
                     continue
 
                 if property.is_number_field:
-                    value = locale.atof(value)
+                    try:
+                        value = locale.atof(value)
+                    except ValueError:
+                        value = locale.atof("0.0")
 
                 properties_dict[property_id] = unicode(value)
 

lfs/catalog/models.py

 import lfs.catalog.utils
 from lfs.core.fields.thumbs import ImageWithThumbsField
 from lfs.core.managers import ActiveManager
-from lfs.catalog.settings import ACTIVE_FOR_SALE_CHOICES, CONTENT_CATEGORIES
-from lfs.catalog.settings import ACTIVE_FOR_SALE_STANDARD
-from lfs.catalog.settings import ACTIVE_FOR_SALE_YES
+from lfs.catalog.settings import CHOICES, CONTENT_CATEGORIES
+from lfs.catalog.settings import CHOICES_STANDARD
+from lfs.catalog.settings import CHOICES_YES
 from lfs.catalog.settings import PRODUCT_TYPE_CHOICES
 from lfs.catalog.settings import CONFIGURABLE_PRODUCT
 from lfs.catalog.settings import STANDARD_PRODUCT
 from lfs.catalog.settings import QUANTITY_FIELD_DECIMAL_2
 from lfs.catalog.settings import THUMBNAIL_SIZES
 from lfs.catalog.settings import VARIANTS_DISPLAY_TYPE_CHOICES
-from lfs.catalog.settings import CATEGORY_VARIANT_CHEAPEST
+from lfs.catalog.settings import CATEGORY_VARIANT_CHEAPEST_PRICE
+from lfs.catalog.settings import CATEGORY_VARIANT_CHEAPEST_BASE_PRICE
+from lfs.catalog.settings import CATEGORY_VARIANT_CHEAPEST_PRICES
 from lfs.catalog.settings import CATEGORY_VARIANT_DEFAULT
 
 from lfs.tax.models import Tax
                                         max_length=255)
     effective_price = models.FloatField(_(u"Price"), blank=True)
     price_unit = models.CharField(_(u"Price unit"), blank=True, max_length=20, choices=LFS_PRICE_UNITS)
-    unit = models.CharField(_(u"Unit"), blank=True, max_length=20, choices=LFS_UNITS)
+    unit = models.CharField(_(u"Quanity field unit"), blank=True, max_length=20, choices=LFS_UNITS)
     short_description = models.TextField(_(u"Short description"), blank=True)
     description = models.TextField(_(u"Description"), blank=True)
     images = generic.GenericRelation("Image", verbose_name=_(u"Images"),
     manage_stock_amount = models.BooleanField(_(u"Manage stock amount"), default=False)
     stock_amount = models.FloatField(_(u"Stock amount"), default=0)
 
-    active_packing_unit = models.PositiveSmallIntegerField(_(u"Active packing unit"), default=0)
-    packing_unit = models.FloatField(_(u"Packing unit"), blank=True, null=True)
-    packing_unit_unit = models.CharField(_(u"Unit"), blank=True, max_length=30, choices=LFS_PACKING_UNITS)
+    active_packing_unit = models.PositiveSmallIntegerField(_(u"Active packing"), default=0)
+    packing_unit = models.FloatField(_(u"Amount per packing"), blank=True, null=True)
+    packing_unit_unit = models.CharField(_(u"Packing unit"), blank=True, max_length=30, choices=LFS_PACKING_UNITS)
 
     static_block = models.ForeignKey("StaticBlock", verbose_name=_(u"Static block"), blank=True, null=True, related_name="products")
 
     active_static_block = models.BooleanField(_(u"Active static bock"), default=False)
     active_description = models.BooleanField(_(u"Active description"), default=False)
     active_price = models.BooleanField(_(u"Active price"), default=False)
-    active_for_sale = models.PositiveSmallIntegerField(_("Active for sale"), choices=ACTIVE_FOR_SALE_CHOICES, default=ACTIVE_FOR_SALE_STANDARD)
+    active_for_sale = models.PositiveSmallIntegerField(_("Active for sale"), choices=CHOICES, default=CHOICES_STANDARD)
     active_for_sale_price = models.BooleanField(_(u"Active for sale price"), default=False)
     active_images = models.BooleanField(_(u"Active Images"), default=False)
     active_related_products = models.BooleanField(_(u"Active related products"), default=False)
         return description
 
     def get_base_price_amount(self):
-        if self.is_variant() and self.active_base_price == ACTIVE_FOR_SALE_STANDARD:
+        if self.is_variant() and self.active_base_price == CHOICES_STANDARD:
             return self.parent.base_price_amount
         else:
             return self.base_price_amount
 
     def get_base_price_unit(self):
-        if self.is_variant() and self.active_base_price == ACTIVE_FOR_SALE_STANDARD:
+        if self.is_variant() and self.active_base_price == CHOICES_STANDARD:
             return self.parent.base_price_unit
         else:
             return self.base_price_unit
         whether the product is a variant.
         """
         if self.is_variant():
-            if self.active_base_price == ACTIVE_FOR_SALE_STANDARD:
+            if self.active_base_price == CHOICES_STANDARD:
                 return self.parent.active_base_price
             else:
-                return self.active_base_price == ACTIVE_FOR_SALE_YES
+                return self.active_base_price == CHOICES_YES
         else:
             return self.active_base_price
 
         product is a variant.
         """
         if self.is_variant():
-            if self.active_for_sale == ACTIVE_FOR_SALE_STANDARD:
+            if self.active_for_sale == CHOICES_STANDARD:
                 return self.parent.for_sale
-            elif self.active_for_sale == ACTIVE_FOR_SALE_YES:
+            elif self.active_for_sale == CHOICES_YES:
                 return True
             else:
                 return False
         This is either the cheapest variant, the default variant, an explicitly
         selected one or None.
         """
-        if self.category_variant == CATEGORY_VARIANT_CHEAPEST:
+        if self.category_variant == CATEGORY_VARIANT_CHEAPEST_PRICE:
             return self.get_cheapest_variant(request)
+        elif self.category_variant == CATEGORY_VARIANT_CHEAPEST_BASE_PRICE:
+            return self.get_cheapest_variant_by_base_price(request)
+        elif self.category_variant == CATEGORY_VARIANT_CHEAPEST_PRICES:
+            return self.get_default_variant()
         elif self.category_variant == CATEGORY_VARIANT_DEFAULT:
             return self.get_default_variant()
         else:
 
         return cheapest_variant
 
-    def display_cheapest_variant_for_category(self):
-        return self.category_variant == CATEGORY_VARIANT_CHEAPEST
+    def get_cheapest_variant_by_base_price(self, request):
+        """
+        Returns the cheapest variant by base gross price.
+        """
+        cheapest_variant = None
+        min_price = None
+        for variant in Product.objects.filter(parent=self):
+            price = variant.get_base_price_gross(request)
+            if price == 0:
+                continue
+            if (min_price is None) or (price < min_price):
+                cheapest_variant = variant
+                min_price = price
+
+        return cheapest_variant
+
+    def get_cheapest_for_sale_price_gross(self, request):
+        """
+        Returns the min price and min base price as dict.
+        """
+        if self.is_variant():
+            product = self.parent
+        else:
+            product = self
+
+        prices = []
+        for variant in Product.objects.filter(parent=product, active=True):
+            price = variant.get_for_sale_price_gross(request)
+            if price not in prices:
+                prices.append(price)
+
+            return {
+            "price": min(prices),
+            "starting_from": len(prices) > 1,
+        }
+
+    def get_cheapest_standard_price_gross(self, request):
+        """
+        Returns the min price and min base price as dict.
+        """
+        prices = []
+        for variant in Product.objects.filter(parent=self, active=True):
+            price = variant.get_standard_price_gross(request)
+            if price not in prices:
+                prices.append(price)
+
+        return {
+            "price": min(prices),
+            "starting_from": len(prices) > 1,
+        }
+
+    def get_cheapest_price_gross(self, request):
+        """
+        Returns the min price and min base price as dict.
+        """
+        prices = []
+        for variant in Product.objects.filter(parent=self, active=True):
+            price = variant.get_price_gross(request)
+            if price not in prices:
+                prices.append(price)
+
+        return {
+            "price": min(prices),
+            "starting_from": len(prices) > 1,
+        }
+
+    def get_cheapest_base_price_gross(self, request):
+        """
+        Returns the min price and min base price as dict.
+        """
+        prices = []
+        for variant in Product.objects.filter(parent=self, active=True):
+            price = float("%.2f" % variant.get_base_price_gross(request))
+            if price not in prices:
+                prices.append(price)
+
+        return {
+            "price": min(prices),
+            "starting_from": len(prices) > 1,
+        }
 
     def get_static_block(self):
         """Returns the static block of the product. Takes care whether the
         options.sort()
         options = "".join(options)
         for variant in self.variants.filter(active=True):
-            temp = variant.property_values.all()
+            temp = variant.property_values.filter(type=PROPERTY_VALUE_TYPE_VARIANT)
             temp = ["%s|%s" % (x.property.id, x.value) for x in temp]
             temp.sort()
             temp = "".join(temp)
         Returns True if the packing unit is active. Takes variant into accounts.
         """
         if self.is_variant():
-            if self.active_packing_unit == ACTIVE_FOR_SALE_STANDARD:
+            if self.active_packing_unit == CHOICES_STANDARD:
                 return self.parent.active_packing_unit
             else:
-                return self.active_packing_unit == ACTIVE_FOR_SALE_YES
+                return self.active_packing_unit == CHOICES_YES
         else:
             return self.active_packing_unit
 
         Returns the packing info of the product as list. Takes variants into
         account.
         """
-        if self.is_variant() and self.active_packing_unit == ACTIVE_FOR_SALE_STANDARD:
+        if self.is_variant() and self.active_packing_unit == CHOICES_STANDARD:
             obj = self.parent
         else:
             obj = self
         if self.manage_stock_amount and self.stock_amount <= 0 and not self.order_time:
             return False
         else:
-            return self.deliverable
+            if self.is_variant():
+                return self.deliverable and self.parent.deliverable
+            else:
+                return self.deliverable
+
+    def get_manual_delivery_time(self):
+        """Returns the manual delivery time of a product or None.
+        """
+        if self.manual_delivery_time:
+            return self.delivery_time
+
+        if self.is_variant() and self.parent.manual_delivery_time:
+            return self.parent.delivery_time
+
+        return None
 
     def get_clean_quantity(self, quantity=1):
         """Returns the correct formatted quantity based on the product's type
     position = models.IntegerField(_(u"Position"), blank=True, null=True)
     unit = models.CharField(_(u"Unit"), blank=True, max_length=15)
     display_on_product = models.BooleanField(_(u"Display on product"), default=True)
-    local = models.BooleanField(default=False)
-    filterable = models.BooleanField(default=True)
+    local = models.BooleanField(_(u"Local"), default=False)
+    filterable = models.BooleanField(_(u"Filterable"), default=True)
     display_no_results = models.BooleanField(_(u"Display no results"), default=False)
-    configurable = models.BooleanField(default=False)
+    configurable = models.BooleanField(_(u"Configurable"), default=False)
     type = models.PositiveSmallIntegerField(_(u"Type"), choices=PROPERTY_FIELD_CHOICES, default=PROPERTY_TEXT_FIELD)
     price = models.FloatField(_(u"Price"), blank=True, null=True)
     display_price = models.BooleanField(_(u"Display price"), default=True)
     unit_step = models.FloatField(_(u"Step"), blank=True, null=True)
     decimal_places = models.PositiveSmallIntegerField(_(u"Decimal places"), default=0)
 
-    required = models.BooleanField(default=False)
+    required = models.BooleanField(_(u"Required"), default=False)
 
     step_type = models.PositiveSmallIntegerField(_(u"Step type"), choices=PROPERTY_STEP_TYPE_CHOICES, default=PROPERTY_STEP_TYPE_AUTOMATIC)
     step = models.IntegerField(_(u"Step"), blank=True, null=True)

lfs/catalog/settings.py

     (QUANTITY_FIELD_DECIMAL_2, _(u"Decimal 0.01")),
 ]
 
-ACTIVE_FOR_SALE_STANDARD = 0
-ACTIVE_FOR_SALE_YES = 2
-ACTIVE_FOR_SALE_NO = 3
-ACTIVE_FOR_SALE_CHOICES = [
-    (ACTIVE_FOR_SALE_STANDARD, _(u"Standard")),
-    (ACTIVE_FOR_SALE_YES, _(u"Yes")),
-    (ACTIVE_FOR_SALE_NO, _(u"No")),
+CHOICES_STANDARD = 0
+CHOICES_YES = 2
+CHOICES_NO = 3
+CHOICES = [
+    (CHOICES_STANDARD, _(u"Standard")),
+    (CHOICES_YES, _(u"Yes")),
+    (CHOICES_NO, _(u"No")),
 ]
 
 STANDARD_PRODUCT = "0"
 ]
 
 CATEGORY_VARIANT_DEFAULT = -1
-CATEGORY_VARIANT_CHEAPEST = -2
+CATEGORY_VARIANT_CHEAPEST_PRICE = -2
+CATEGORY_VARIANT_CHEAPEST_BASE_PRICE = -3
+CATEGORY_VARIANT_CHEAPEST_PRICES = -4
 CATEGORY_VARIANT_CHOICES = [
     (CATEGORY_VARIANT_DEFAULT, _(u"Default")),
-    (CATEGORY_VARIANT_CHEAPEST, _(u"Cheapest")),
+    (CATEGORY_VARIANT_CHEAPEST_PRICE, _(u"Cheapest price")),
+    (CATEGORY_VARIANT_CHEAPEST_BASE_PRICE, _(u"Cheapest base price")),
+    (CATEGORY_VARIANT_CHEAPEST_PRICES, _(u"Cheapest prices")),
 ]
 
 LIST = 0
         "name": _(u"Category with subcategories"),
     }),
 )
+CATEGORY_TEMPLATES = getattr(settings, 'CATEGORY_TEMPLATES', CATEGORY_TEMPLATES)
 
 # Template configuration for product display
 PRODUCT_TEMPLATES = (
         "name": _(u"Default")
     },),
 )
+PRODUCT_TEMPLATES = getattr(settings, 'PRODUCT_TEMPLATES', PRODUCT_TEMPLATES)
 
 THUMBNAIL_SIZES = getattr(settings, 'LFS_THUMBNAIL_SIZES', ((60, 60), (100, 100), (200, 200), (300, 300), (400, 400)))

lfs/catalog/tests.py

 from lfs.caching.utils import lfs_get_object_or_404
 import lfs.catalog.utils
 from lfs.core.signals import property_type_changed
-from lfs.catalog.settings import ACTIVE_FOR_SALE_YES
-from lfs.catalog.settings import ACTIVE_FOR_SALE_STANDARD
-from lfs.catalog.settings import ACTIVE_FOR_SALE_NO
+from lfs.catalog.settings import CHOICES_YES
+from lfs.catalog.settings import CHOICES_STANDARD
+from lfs.catalog.settings import CHOICES_NO
 from lfs.catalog.settings import CONTENT_CATEGORIES
 from lfs.catalog.settings import PRODUCT_WITH_VARIANTS, VARIANT
 from lfs.catalog.settings import DELIVERY_TIME_UNIT_HOURS
     def test_calculate_packing(self):
         from lfs.catalog.views import calculate_packing
         request = RequestFactory().post("/")
+        request.session = SessionStore()
+        request.user = AnonymousUser()
 
         self.p1.packing_unit = 2
         self.p1.save()
         self.assertEqual(result.status_code, 200)
 
         request = RequestFactory().post("/", {"quantity": "3"})
+        request.session = SessionStore()
+        request.user = AnonymousUser()
         result = calculate_packing(request, id=1)
         self.assertEqual(result.status_code, 200)
 
         request = RequestFactory().post("/", {"quantity": "3"})
+        request.session = SessionStore()
+        request.user = AnonymousUser()
         result = calculate_packing(request, id=1, as_string=True)
         self.failUnless(isinstance(result, unicode))
 
         self.p3 = Product.objects.create(name=u"Product 3", slug=u"product-3", active=True)
 
         # Create a size property with two options
-        self.size = size = Property.objects.create(name="Size")
+        self.size = size = Property.objects.create(name="Size", type=PROPERTY_SELECT_FIELD)
         self.l = l = PropertyOption.objects.create(name="L", property=size)
         self.m = m = PropertyOption.objects.create(name="M", property=size)
 
         # Create a color property with two options
-        self.color = color = Property.objects.create(name="Color")
+        self.color = color = Property.objects.create(name="Color", type=PROPERTY_SELECT_FIELD)
         self.red = red = PropertyOption.objects.create(name="Red", property=color)
         self.green = green = PropertyOption.objects.create(name="Green", property=color)
 
             active=True,
         )
 
-        self.ppv_color_red = ProductPropertyValue.objects.create(product=self.v1, property=self.color, value=self.red.id, type=PROPERTY_VALUE_TYPE_FILTER)
-        self.ppv_size_m = ProductPropertyValue.objects.create(product=self.v1, property=self.size, value=self.m.id, type=PROPERTY_VALUE_TYPE_FILTER)
+        self.ppv_color_red = ProductPropertyValue.objects.create(product=self.v1, property=self.color, value=self.red.id, type=PROPERTY_VALUE_TYPE_VARIANT)
+        self.ppv_size_m = ProductPropertyValue.objects.create(product=self.v1, property=self.size, value=self.m.id, type=PROPERTY_VALUE_TYPE_VARIANT)
 
         # Add a variant with color = green, size = l
         self.v2 = Product.objects.create(name="Variant 2", slug="variant-2", sub_type=VARIANT, parent=self.p1, active=True)
-        self.ppv_color_green = ProductPropertyValue.objects.create(product=self.v2, property=color, value=self.green.id, type=PROPERTY_VALUE_TYPE_FILTER)
-        self.ppv_size_l = ProductPropertyValue.objects.create(product=self.v2, property=size, value=self.l.id, type=PROPERTY_VALUE_TYPE_FILTER)
+        self.ppv_color_green = ProductPropertyValue.objects.create(product=self.v2, property=color, value=self.green.id, type=PROPERTY_VALUE_TYPE_VARIANT)
+        self.ppv_size_l = ProductPropertyValue.objects.create(product=self.v2, property=size, value=self.l.id, type=PROPERTY_VALUE_TYPE_VARIANT)
 
         # Add related products to p1
         self.p1.related_products.add(self.p2, self.p3)
     def test_get_variant_properties(self):
         """
         """
-        # First add some variant property values
-        ppv_color_red = ProductPropertyValue.objects.create(product=self.v1, property=self.color, value=self.red.id, type=PROPERTY_VALUE_TYPE_VARIANT)
-        ppv_size_m = ProductPropertyValue.objects.create(product=self.v1, property=self.size, value=self.m.id, type=PROPERTY_VALUE_TYPE_VARIANT)
-        ppv_color_green = ProductPropertyValue.objects.create(product=self.v2, property=self.color, value=self.green.id, type=PROPERTY_VALUE_TYPE_VARIANT)
-        ppv_size_l = ProductPropertyValue.objects.create(product=self.v2, property=self.size, value=self.l.id, type=PROPERTY_VALUE_TYPE_VARIANT)
+        self.color.type = PROPERTY_TEXT_FIELD
+        self.color.save()
+
+        self.size.type = PROPERTY_TEXT_FIELD
+        self.size.save()
 
         options = [p["value"] for p in self.v1.get_variant_properties()]
-        self.failIf(str(ppv_color_red.value) not in options)
-        self.failIf(str(ppv_size_m.value) not in options)
+        self.failIf(str(self.ppv_color_red.value) not in options)
+        self.failIf(str(self.ppv_size_m.value) not in options)
 
         options = [p["value"] for p in self.v2.get_variant_properties()]
         self.failIf(str(self.ppv_color_green.value) not in options)
     def test_get_displayed_properties(self):
         """
         """
+        self.color.type = PROPERTY_TEXT_FIELD
+        self.color.save()
+
+        self.size.type = PROPERTY_TEXT_FIELD
+        self.size.save()
+
         # First add some variant property values
         ppv_color_red = ProductPropertyValue.objects.create(product=self.p1, property=self.color, value=self.red.id, type=PROPERTY_VALUE_TYPE_DISPLAY)
         ppv_size_m = ProductPropertyValue.objects.create(product=self.p1, property=self.size, value=self.m.id, type=PROPERTY_VALUE_TYPE_DISPLAY)
         self.failIf(str(ppv_size_m.value) not in options)
 
         options = [p["value"] for p in self.p2.get_displayed_properties()]
-        self.failIf(str(self.ppv_color_green.value) not in options)
-        self.failIf(str(self.ppv_size_l.value) not in options)
+        self.failIf(str(ppv_color_green.value) not in options)
+        self.failIf(str(ppv_size_l.value) not in options)
 
     def test_has_option(self):
         """
         self.p1.for_sale = True
         self.p1.save()
 
-        self.v1.active_for_sale = ACTIVE_FOR_SALE_STANDARD
+        self.v1.active_for_sale = CHOICES_STANDARD
         self.v1.save()
 
         self.assertEqual(self.v1.get_for_sale(), True)
 
-        self.v1.active_for_sale = ACTIVE_FOR_SALE_YES
+        self.v1.active_for_sale = CHOICES_YES
         self.v1.save()
 
         self.assertEqual(self.v1.get_for_sale(), True)
 
-        self.v1.active_for_sale = ACTIVE_FOR_SALE_NO
+        self.v1.active_for_sale = CHOICES_NO
         self.v1.save()
 
         self.assertEqual(self.v1.get_for_sale(), False)
         self.p1.for_sale = False
         self.p1.save()
 
-        self.v1.active_for_sale = ACTIVE_FOR_SALE_STANDARD
+        self.v1.active_for_sale = CHOICES_STANDARD
         self.v1.save()
 
         self.assertEqual(self.v1.get_for_sale(), False)
 
-        self.v1.active_for_sale = ACTIVE_FOR_SALE_YES
+        self.v1.active_for_sale = CHOICES_YES
         self.v1.save()
 
         self.assertEqual(self.v1.get_for_sale(), True)
 
-        self.v1.active_for_sale = ACTIVE_FOR_SALE_NO
+        self.v1.active_for_sale = CHOICES_NO
         self.v1.save()
 
         self.assertEqual(self.v1.get_for_sale(), False)
             'slug': 'variant-slug',
             'name': 'variant',
             'price': 10.00,
+            'property_%s' % self.size.id: self.m.id,
         }
 
         # set up a user with permission to access the manage interface
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(len(Product.objects.all()), 6)
-        variant = Product.objects.get(slug="product-1-variant-slug")
+        variant = Product.objects.get(slug="product-1-variant-slug-m")
         self.assertEqual(variant.name, 'variant')
         self.assertEqual(variant.price, 10.00)
         self.assertEqual(variant.parent, product)

lfs/catalog/views.py

 from lfs.catalog.models import ProductPropertyValue
 from lfs.catalog.models import PropertyOption
 from lfs.catalog.models import ProductAttachment
+from lfs.catalog.settings import CATEGORY_VARIANT_CHEAPEST_PRICES
+from lfs.catalog.settings import CONTENT_PRODUCTS
 from lfs.catalog.settings import PRODUCT_WITH_VARIANTS
-from lfs.catalog.settings import VARIANT
 from lfs.catalog.settings import PROPERTY_VALUE_TYPE_DEFAULT
 from lfs.catalog.settings import SELECT
-from lfs.catalog.settings import CONTENT_PRODUCTS
+from lfs.catalog.settings import VARIANT
 from lfs.core.utils import LazyEncoder
 from lfs.core.templatetags import lfs_tags
 from lfs.utils import misc as lfs_utils
     changed.
     """
     product_id = request.POST.get("product_id")
-    print product_id
 
     try:
         variant = Product.objects.get(pk=product_id)
             default_variant = product.get_variant_for_category(request)
             if default_variant:
                 product = default_variant
+
         image = None
         product_image = product.get_image()
         if product_image:
             "slug": product.slug,
             "name": product.get_name(),
             "image": image,
-            "price": product.get_price(request),
-            "standard_price": product.get_standard_price(request),
             "price_unit": product.price_unit,
             "price_includes_tax": product.price_includes_tax(request),
         })

lfs/checkout/forms.py

     """
     invoice_firstname = forms.CharField(label=_(u"First Name"), max_length=50)
     invoice_lastname = forms.CharField(label=_(u"Last Name"), max_length=50)
-    invoice_phone = forms.CharField(label=_(u"Invoice Phone"), required=getattr(settings, "INVOICE_PHONE_REQUIRED", False), max_length=20)
-    invoice_email = forms.EmailField(label=_(u"Invoice E-mail"), required=getattr(settings, "INVOICE_EMAIL_REQUIRED", True), max_length=50)
-    invoice_company_name = forms.CharField(label=_(u"Company name"), required=getattr(settings, "INVOICE_COMPANY_NAME_REQUIRED", False), max_length=50)
+    invoice_phone = forms.CharField(label=_(u"Invoice Phone"), required=getattr(settings, "LFS_INVOICE_PHONE_REQUIRED", False), max_length=20)
+    invoice_email = forms.EmailField(label=_(u"Invoice E-mail"), required=getattr(settings, "LFS_INVOICE_EMAIL_REQUIRED", True), max_length=50)
+    invoice_company_name = forms.CharField(label=_(u"Company name"), required=getattr(settings, "LFS_INVOICE_COMPANY_NAME_REQUIRED", False), max_length=50)
 
     shipping_firstname = forms.CharField(label=_(u"First Name"), required=False, max_length=50)
     shipping_lastname = forms.CharField(label=_(u"Last Name"), required=False, max_length=50)
             if self.cleaned_data.get("shipping_lastname", "") == "":
                 self._errors["shipping_lastname"] = ErrorList([msg])
 
-            if getattr(settings, "SHIPPING_PHONE_REQUIRED"):
+            if getattr(settings, "LFS_SHIPPING_PHONE_REQUIRED"):
                 if self.cleaned_data.get("shipping_phone", "") == "":
                     self._errors["shipping_phone"] = ErrorList([msg])
 

lfs/core/fields/thumbs.py

 
     """
     def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, sizes=None, **kwargs):
-        self.verbose_name = verbose_name
-        self.name = name
-        self.width_field = width_field
-        self.height_field = height_field
+        super(ImageWithThumbsField, self).__init__(verbose_name=verbose_name,
+                                                   name=name,
+                                                   width_field=width_field,
+                                                   height_field=height_field,
+                                                   **kwargs)
         self.sizes = sizes
-        super(ImageField, self).__init__(**kwargs)
+

lfs/core/management/commands/lfs_init.py

         PortletAssignment.objects.create(slot=left_slot, content=shop, portlet=pages_portlet)
 
         # Payment methods
-        PaymentMethod.objects.create(pk=1, name="Direct debit", priority=1, active=1, deletable=0, type=PM_BANK)
-        PaymentMethod.objects.create(pk=2, name="Cash on delivery", priority=2, active=1, deletable=0)
-        PaymentMethod.objects.create(pk=3, name="PayPal", priority=3, active=1, deletable=0)
-        PaymentMethod.objects.create(pk=4, name="Prepayment", priority=4, active=1, deletable=0)
+        pm = PaymentMethod.objects.create(name="Direct debit", priority=1, active=1, deletable=0, type=PM_BANK)
+        pm.id=1; pm.save()
+        pm = PaymentMethod.objects.create(name="Cash on delivery", priority=2, active=1, deletable=0)
+        pm.id=2; pm.save()
+        pm = PaymentMethod.objects.create(name="PayPal", priority=3, active=1, deletable=0)
+        pm.id=3; pm.save()
+        pm = PaymentMethod.objects.create(name="Prepayment", priority=4, active=1, deletable=0)
+        pm.id=4; pm.save()
 
         # Shipping methods
         ShippingMethod.objects.create(name="Standard", priority=1, active=1)
 
         # Pages
-        Page.objects.create(id=1, title="Root", slug="", active=1, exclude_from_navigation=1)
-        Page.objects.create(title="Terms and Conditions", slug="terms-and-conditions", active=1, body="Enter your terms and conditions here.")
-        Page.objects.create(title="Imprint", slug="imprint", active=1, body="Enter your imprint here.")
+        p = Page.objects.create(title="Root", slug="", active=1, exclude_from_navigation=1)
+        p.id = 1; p.save()
+        p = Page.objects.create(title="Terms and Conditions", slug="terms-and-conditions", active=1, body="Enter your terms and conditions here.")
+        p.id = 2; p.save()
+        p = Page.objects.create(title="Imprint", slug="imprint", active=1, body="Enter your imprint here.")
+        p.id = 3; p.save()
 
         # Order Numbers
         ong = import_symbol(settings.LFS_ORDER_NUMBER_GENERATOR)

lfs/core/management/commands/lfs_migrate.py

             print "You are up-to-date"
 
     def migrate_to_07(self, application, version):
+        from lfs.catalog.models import Product
+        from lfs.catalog.settings import VARIANT
         from lfs.core.utils import get_default_shop
         from lfs.page.models import Page
         from lfs.shipping.models import ShippingMethod
         from lfs.catalog.settings import QUANTITY_FIELD_TYPES
 
         db.add_column("catalog_product", "type_of_quantity_field", models.PositiveSmallIntegerField(_(u"Type of quantity field"), null=True, blank=True, choices=QUANTITY_FIELD_TYPES))
+        db.add_column("catalog_product", "category_variant", models.SmallIntegerField(_(u"Category variant"), blank=True, null=True))
+        db.add_column("catalog_product", "active_base_price", models.PositiveSmallIntegerField(_(u"Active base price"), default=0))
+        db.add_column("catalog_product", "base_price_unit", models.CharField(_(u"Base price unit"), blank=True, null=True, max_length=30, choices=settings.LFS_BASE_PRICE_UNITS))
+        db.add_column("catalog_product", "base_price_amount", models.FloatField(_(u"Base price amount"), default=0.0, blank=True, null=True))
+
+        if db.backend_name == "postgres":
+            db.execute('ALTER TABLE catalog_product ALTER active_packing_unit TYPE smallint USING CASE WHEN active_packing_unit=FALSE THEN 0 ELSE 1 END;')
+        else:
+            db.alter_column('catalog_product', 'active_packing_unit', models.PositiveSmallIntegerField(_(u"Active packing"), default=0))
+            for product in Product.objects.all():
+                if product.active_packing_unit != 0:
+                    product.active_packing_unit = 1
+                    product.save()
 
         # Pages
         print "Migrating to 0.7"
         db.add_column("order_order", "shipping_company_name", models.CharField(null=True, blank=True, max_length=100))
 
         # Shipping Method
-        db.add_column("shipping_shippingmethod", "price_calculator", models.CharField(max_length=200, choices=settings.LFS_SHIPPING_PRICE_CALCULATORS, default=settings.LFS_SHIPPING_PRICE_CALCULATORS[0][0]))
+        db.add_column("shipping_shippingmethod", "price_calculator", models.CharField(max_length=200, choices=settings.LFS_SHIPPING_METHOD_PRICE_CALCULATORS, default=settings.LFS_SHIPPING_METHOD_PRICE_CALCULATORS[0][0]))
         for shipping_method in ShippingMethod.objects.all():
-            shipping_method.price_calculator = settings.LFS_SHIPPING_PRICE_CALCULATORS[0][0]
+            shipping_method.price_calculator = settings.LFS_SHIPPING_METHOD_PRICE_CALCULATORS[0][0]
             shipping_method.save()
 
         # Static Block

File contents unchanged.

lfs/core/signals.py

 featured_changed = django.dispatch.Signal()
 
 # Order
+order_created = django.dispatch.Signal()
+order_paid = django.dispatch.Signal()
+order_sent = django.dispatch.Signal()
 order_submitted = django.dispatch.Signal()
-order_sent = django.dispatch.Signal()
 
 # Property
 property_type_changed = django.dispatch.Signal()

lfs/core/templatetags/lfs_tags.py

 from django.core.cache import cache
 from django.core.exceptions import ObjectDoesNotExist
 from django.core.urlresolvers import reverse
-from django.template import Node, TemplateSyntaxError, Variable
+from django.template import Node, TemplateSyntaxError
+from django.utils.safestring import mark_safe
 from django.utils.translation import ugettext_lazy as _
 
 # lfs imports
 import lfs.utils.misc
 from lfs.caching.utils import lfs_get_object_or_404
 from lfs.catalog.models import Category
+from lfs.catalog.settings import CONFIGURABLE_PRODUCT
+from lfs.catalog.settings import CATEGORY_VARIANT_CHEAPEST_PRICES
 from lfs.catalog.settings import PRODUCT_WITH_VARIANTS
 from lfs.catalog.settings import STANDARD_PRODUCT
-from lfs.catalog.settings import CONFIGURABLE_PRODUCT
 from lfs.catalog.models import Product
 from lfs.catalog.models import PropertyOption
 from lfs.catalog.settings import PRODUCT_TYPE_LOOKUP
             products = Product.objects.filter(
                 categories__in=categories,
                 sub_type__in=(STANDARD_PRODUCT, PRODUCT_WITH_VARIANTS, CONFIGURABLE_PRODUCT),
-            ).order_by(sorting)
+            ).distinct().order_by(sorting)
         else:
             products = Product.objects.filter(
                 categories__in=categories,
                 sub_type__in=(STANDARD_PRODUCT, PRODUCT_WITH_VARIANTS, CONFIGURABLE_PRODUCT),
                 active=True,
-            ).order_by(sorting)
+            ).distinct().order_by(sorting)
 
         product_slugs = [p.slug for p in products]
         product_index = product_slugs.index(slug)
         if result[-1] == '-':
             length = len(locale.nl_langinfo(locale.CRNCYSTR))
             result = '%s-%s' % (result[0:length], result[length:-1])
-        return '<span class="negative">%s</span>' % result
+        return mark_safe('<span class="negative">%s</span>' % result)
     return result
 
 
 @register.filter
-def decimal_l10n(value):
+def decimal_l10n(value, digits=2):
     """Returns the decimal value of value based on current locale.
     """
-    return locale.format_string("%.2f", value)
+    try:
+        value = float(value)
+    except ValueError:
+        pass
+
+    format_str = "%%.%sf" % digits
+    return locale.format_string(format_str, value)
 
 
 @register.filter
     """Returns the packages based on product's package unit and cart items
     amount.
     """
-    return int(math.ceil(cart_item.amount / cart_item.product.packing_unit))
+    cart_item = cart_item.get("obj")
+    if cart_item.product.packing_unit:
+        return int(math.ceil(cart_item.amount / cart_item.product.packing_unit))
+    return 0
+
+
+class CategoryProductPricesGrossNode(Node):
+    """
+    Node to calculate needed gross prices of a product for the category view.
+    """
+    def __init__(self, product_id):
+        self.product_id = template.Variable(product_id)
+
+    def render(self, context):
+        request = context.get("request")
+
+        product_id = self.product_id.resolve(context)
+        product = Product.objects.get(pk=product_id)
+
+        if product.is_variant():
+            parent = product.parent
+        else:
+            parent = product
+
+        if parent.category_variant == CATEGORY_VARIANT_CHEAPEST_PRICES:
+            if product.get_for_sale():
+                info = parent.get_cheapest_standard_price_gross(request)
+                context["standard_price"] = info["price"]
+                context["standard_price_starting_from"] = info["starting_from"]
+
+            info = parent.get_cheapest_price_gross(request)
+            context["price"] = info["price"]
+            context["price_starting_from"] = info["starting_from"]
+
+            info = parent.get_cheapest_base_price_gross(request)
+            context["base_price"] = info["price"]
+            context["base_price_starting_from"] = info["starting_from"]
+        else:
+            if product.get_for_sale():
+                context["standard_price"] = product.get_standard_price(request)
+            context["price"] = product.get_price_gross(request)
+            context["price_starting_from"] = False
+
+            context["base_price"] = product.get_base_price_gross(request)
+            context["base_price_starting_from"] = False
+
+        if product.get_active_packing_unit():
+            context["base_packing_price_gross"] = product.get_base_packing_price_gross(request)
+
+        return ""
+
+def do_category_product_prices_gross(parser, token):
+    """
+    Calculates needed gross price of a product for the category view.
+    """
+    bits = token.contents.split()
+    if len(bits) != 2:
+        raise TemplateSyntaxError('%s tag needs product id as argument' % bits[0])
+    return CategoryProductPricesGrossNode(bits[1])
+register.tag('category_product_prices_gross', do_category_product_prices_gross)
 
 
 @register.filter(name='get_price')
Add a comment to this file

lfs/core/widgets/file.py

File contents unchanged.

lfs/criteria/models/criteria.py

     price = models.FloatField(_(u"Price"), default=0.0)
 
     def __unicode__(self):
-        return "Cart Price %s %s" % (self.get_operator_display(), self.price)
+        return _("Cart Price %(operator)s %(price)s") % {'operator': self.get_operator_display(), 'price': self.price}
 
     @property
     def content_type(self):

lfs/discounts/models.py

         Criteria which must all valid to make the discount happen.
 
     """
-    name = models.CharField(max_length=100)
+    name = models.CharField(_(u"Name"), max_length=100)
     value = models.FloatField(_(u"Value"))
     type = models.PositiveSmallIntegerField(_(u"Type"), choices=DISCOUNT_TYPE_CHOICES, default=DISCOUNT_TYPE_ABSOLUTE)
     tax = models.ForeignKey(Tax, verbose_name=_(u"Tax"), blank=True, null=True)
-    sku = models.CharField(blank=True, max_length=50)
+    sku = models.CharField(_(u"SKU"), blank=True, max_length=50)
     criteria_objects = generic.GenericRelation(CriteriaObjects,
         object_id_field="content_id", content_type_field="content_type")
 

lfs/gross_price/tests.py

 import lfs.catalog.utils
 import lfs.core.settings as lfs_settings
 from lfs.core.signals import property_type_changed
-from lfs.catalog.settings import ACTIVE_FOR_SALE_YES
-from lfs.catalog.settings import ACTIVE_FOR_SALE_STANDARD
-from lfs.catalog.settings import ACTIVE_FOR_SALE_NO
+from lfs.catalog.settings import CHOICES_YES
+from lfs.catalog.settings import CHOICES_STANDARD
+from lfs.catalog.settings import CHOICES_NO
 from lfs.catalog.settings import CONTENT_CATEGORIES
 from lfs.catalog.settings import PRODUCT_WITH_VARIANTS, VARIANT
 from lfs.catalog.settings import DELIVERY_TIME_UNIT_HOURS
         self.p1.for_sale = True
         self.p1.save()
 
-        self.v1.active_for_sale = ACTIVE_FOR_SALE_STANDARD
+        self.v1.active_for_sale = CHOICES_STANDARD
         self.v1.save()
 
         self.assertEqual(self.v1.get_for_sale(), True)
 
-        self.v1.active_for_sale = ACTIVE_FOR_SALE_YES
+        self.v1.active_for_sale = CHOICES_YES
         self.v1.save()
 
         self.assertEqual(self.v1.get_for_sale(), True)
 
-        self.v1.active_for_sale = ACTIVE_FOR_SALE_NO
+        self.v1.active_for_sale = CHOICES_NO
         self.v1.save()
 
         self.assertEqual(self.v1.get_for_sale(), False)
         self.p1.for_sale = False
         self.p1.save()
 
-        self.v1.active_for_sale = ACTIVE_FOR_SALE_STANDARD
+        self.v1.active_for_sale = CHOICES_STANDARD
         self.v1.save()
 
         self.assertEqual(self.v1.get_for_sale(), False)
 
-        self.v1.active_for_sale = ACTIVE_FOR_SALE_YES
+        self.v1.active_for_sale = CHOICES_YES
         self.v1.save()
 
         self.assertEqual(self.v1.get_for_sale(), True)
 
-        self.v1.active_for_sale = ACTIVE_FOR_SALE_NO
+        self.v1.active_for_sale = CHOICES_NO
         self.v1.save()
 
         self.assertEqual(self.v1.get_for_sale(), False)
Add a comment to this file

lfs/locale/de/LC_MESSAGES/django.mo

Binary file modified.

lfs/locale/de/LC_MESSAGES/django.po

 msgstr ""
 "Project-Id-Version: LFS\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2012-01-26 08:03+0100\n"
+"POT-Creation-Date: 2012-01-28 19:40+0100\n"
 "PO-Revision-Date: 2009-02-22 07:20+0100\n"
 "Last-Translator: Kai Diefenbach <kai.diefenbach@iqpp.de>\n"
 "Language-Team: Kai Diefenbach <kai.diefenbach@iqpp.de>\n"
 msgid "Session"
 msgstr "Session"
 
-#: cart/models.py:50 cart/models.py:238 catalog/models.py:580
+#: cart/models.py:50 cart/models.py:238 catalog/models.py:596
 #: templates/manage/cart/cart_inline.html:58
 #: templates/manage/cart/cart_inline.html:97
 #: templates/manage/cart/carts_inline.html:10
 msgid "Cart"
 msgstr "Warenkorb"
 
-#: cart/models.py:236 catalog/models.py:1614 catalog/models.py:1868
-#: catalog/models.py:1934 catalog/models.py:2272 marketing/models.py:13
+#: cart/models.py:236 catalog/models.py:1630 catalog/models.py:1884
+#: catalog/models.py:1950 catalog/models.py:2288 marketing/models.py:13
 #: marketing/models.py:26 marketing/models.py:33
 #: templates/manage/cart/cart_inline.html:100
 #: templates/manage/reviews/review_inline.html:78
 msgid "Product"
 msgstr "Produkt"
 
-#: cart/models.py:237 catalog/models.py:1617
+#: cart/models.py:237 catalog/models.py:1633
 #: templates/manage/product/accessories_inline.html:116
 msgid "Quantity"
 msgstr "Menge"
 msgid "Cart item"
 msgstr "Artikel"
 
-#: cart/models.py:394 catalog/models.py:1821 catalog/models.py:1846
-#: catalog/models.py:1869 catalog/models.py:1894 catalog/models.py:1936
+#: cart/models.py:394 catalog/models.py:1837 catalog/models.py:1862
+#: catalog/models.py:1885 catalog/models.py:1910 catalog/models.py:1952
 #: order/models.py:211 templates/manage/properties/property.html:32
 msgid "Property"
 msgstr "Eigenschaften"
 msgid "Sorry, but '%(product)s' is only %(amount)s times available."
 msgstr "Es tut uns leid, '%(product)s' ist nur noch %(amount)s Mal im Lager."
 
-#: catalog/models.py:142 catalog/models.py:551 catalog/models.py:1740
-#: catalog/models.py:1896 catalog/models.py:2060 core/models.py:19
+#: catalog/models.py:158 catalog/models.py:567 catalog/models.py:1756
+#: catalog/models.py:1912 catalog/models.py:2076 core/models.py:19
 #: core/models.py:154 export/models.py:16 manufacturer/models.py:10
 #: payment/models.py:73 shipping/models.py:71
 #: templates/manage/category/products_inline.html:47
 msgid "Name"
 msgstr "Name"
 
-#: catalog/models.py:143 catalog/models.py:552 export/models.py:17
+#: catalog/models.py:159 catalog/models.py:568 export/models.py:17
 #: page/models.py:17 supplier/models.py:14
 #: templates/manage/product/products_inline.html:20
 #: templates/manage/product/variants.html:104
 msgid "Slug"
 msgstr "Slug"
 
-#: catalog/models.py:144 catalog/models.py:618 catalog/models.py:1935
+#: catalog/models.py:160 catalog/models.py:634 catalog/models.py:1951
 #: core/models.py:80
 msgid "Parent"
 msgstr "Kategorie"
 
-#: catalog/models.py:148
+#: catalog/models.py:164
 msgid "Show all products"
 msgstr "Alle Produkte anzeigen"
 
-#: catalog/models.py:150 catalog/models.py:1647 catalog/models.py:1743
+#: catalog/models.py:166 catalog/models.py:1663 catalog/models.py:1759
 #: catalog/settings.py:66 export/models.py:18
 #: templates/manage/manage_base.html:86
 #: templates/manage/cart/carts_inline.html:25
 msgid "Products"
 msgstr "Produkte"
 
-#: catalog/models.py:151 catalog/models.py:561
+#: catalog/models.py:167 catalog/models.py:577
 msgid "Short description"
 msgstr "Kurzbeschreibung"
 
-#: catalog/models.py:152 catalog/models.py:562 catalog/models.py:2089
-#: catalog/models.py:2270 core/models.py:159 customer_tax/models.py:24
+#: catalog/models.py:168 catalog/models.py:578 catalog/models.py:2105
+#: catalog/models.py:2286 core/models.py:159 customer_tax/models.py:24
 #: payment/models.py:74 shipping/models.py:72 tax/models.py:18
 #: templates/manage/product/attachments.html:21
 msgid "Description"
 msgstr "Beschreibung"
 
-#: catalog/models.py:153 catalog/models.py:1985 core/models.py:160
+#: catalog/models.py:169 catalog/models.py:2001 core/models.py:160
 #: payment/models.py:76 shipping/models.py:74
 #: templates/manage/category/data.html:68
 #: templates/manage/product/images.html:33
 msgid "Image"
 msgstr "Bild"
 
-#: catalog/models.py:154 catalog/models.py:1616 catalog/models.py:1744
-#: catalog/models.py:1847 catalog/models.py:1870 catalog/models.py:1898
-#: catalog/models.py:1986 catalog/models.py:2064 catalog/models.py:2273
+#: catalog/models.py:170 catalog/models.py:1632 catalog/models.py:1760
+#: catalog/models.py:1863 catalog/models.py:1886 catalog/models.py:1914
+#: catalog/models.py:2002 catalog/models.py:2080 catalog/models.py:2289
 #: core/models.py:79 core/management/commands/lfs_migrate.py:107
 #: criteria/models/criteria_objects.py:24 marketing/models.py:14
 #: marketing/models.py:34 page/models.py:18
 msgid "Position"
 msgstr "Position"
 
-#: catalog/models.py:155 page/models.py:19
+#: catalog/models.py:171 page/models.py:19
 msgid "Exclude from navigation"
 msgstr "Von Navigation ausschließen"
 
-#: catalog/models.py:157 catalog/models.py:596 core/models.py:161
+#: catalog/models.py:173 catalog/models.py:612 core/models.py:161
 msgid "Static block"
 msgstr "Statischer Block"
 
-#: catalog/models.py:158
+#: catalog/models.py:174
 msgid "Category template"
 msgstr "Kategorie-Schablone"
 
-#: catalog/models.py:159
+#: catalog/models.py:175
 msgid "Active formats"
 msgstr "Formate aktivieren"
 
-#: catalog/models.py:161 core/models.py:164
+#: catalog/models.py:177 core/models.py:164
 msgid "Product rows"
 msgstr "Produktzeilen"
 
-#: catalog/models.py:162 core/models.py:163
+#: catalog/models.py:178 core/models.py:163
 msgid "Product cols"
 msgstr "Produktspalten"
 
-#: catalog/models.py:163 core/models.py:165
+#: catalog/models.py:179 core/models.py:165
 msgid "Category cols"
 msgstr "Kategoriespalten"
 
-#: catalog/models.py:165 catalog/models.py:566 core/models.py:182
+#: catalog/models.py:181 catalog/models.py:582 core/models.py:182
 #: core/management/commands/lfs_migrate.py:59
 #: core/management/commands/lfs_migrate.py:83 page/models.py:24
 msgid "Meta title"
 msgstr "Meta Title"
 
-#: catalog/models.py:166 catalog/models.py:567 core/models.py:183
+#: catalog/models.py:182 catalog/models.py:583 core/models.py:183
 #: core/management/commands/lfs_migrate.py:60
 #: core/management/commands/lfs_migrate.py:84 page/models.py:25
 msgid "Meta keywords"
 msgstr "Meta Keywords"
 
-#: catalog/models.py:167 catalog/models.py:568 core/models.py:184
+#: catalog/models.py:183 catalog/models.py:584 core/models.py:184
 #: core/management/commands/lfs_migrate.py:61
 #: core/management/commands/lfs_migrate.py:85 page/models.py:26
 msgid "Meta description"
 msgstr "Meta Description"
 
-#: catalog/models.py:551
+#: catalog/models.py:567
 msgid "The name of the product."
 msgstr "Der name des Produkts."
 
-#: catalog/models.py:552
+#: catalog/models.py:568
 msgid "The unique last part of the Product's URL."
 msgstr "Der eindeutige letzte Teil der Produkt-URL."
 
-#: catalog/models.py:553 templates/manage/category/products_inline.html:50
+#: catalog/models.py:569 templates/manage/category/products_inline.html:50
 #: templates/manage/category/selected_products.html:45
 #: templates/manage/marketing/featured_inline.html:47
 #: templates/manage/marketing/featured_inline.html:105
 msgid "SKU"
 msgstr "Artikelnummer"
 
-#: catalog/models.py:553
+#: catalog/models.py:569
 msgid "Your unique article number of the product."
 msgstr "Ihre eindeutige Artikelnummer des Produkts."
 
-#: catalog/models.py:554 catalog/models.py:558 catalog/models.py:1752
-#: catalog/models.py:1897 criteria/models/criteria.py:52 order/models.py:56
+#: catalog/models.py:570 catalog/models.py:574 catalog/models.py:1768
+#: catalog/models.py:1913 criteria/models/criteria.py:52 order/models.py:56
 #: payment/models.py:78 payment/models.py:122 shipping/models.py:76
 #: shipping/models.py:154 templates/manage/cart/cart_inline.html:103
 #: templates/manage/criteria/combinedlengthandgirth_criterion.html:14
 msgid "Price"
 msgstr "Preis"
 
-#: catalog/models.py:555
+#: catalog/models.py:571
 msgid "Price calculator"
 msgstr "Preiskalkulator"
 
-#: catalog/models.py:559
+#: catalog/models.py:575
 msgid "Price unit"
 msgstr "Preiseinheit"
 
-#: catalog/models.py:560 catalog/models.py:594 catalog/models.py:1745
-#: catalog/models.py:2088
-msgid "Unit"
-msgstr "Einheit"
-
-#: catalog/models.py:563 templates/manage/product/images.html:3
+#: catalog/models.py:576
+msgid "Quanity field unit"
+msgstr " Einheit Anzahlfeld"
+
+#: catalog/models.py:579 templates/manage/product/images.html:3
 #: templates/manage/product/product.html:95
 msgid "Images"
 msgstr "Bilder"
 
-#: catalog/models.py:570 templates/manage/product/related_products.html:4
+#: catalog/models.py:586 templates/manage/product/related_products.html:4
 msgid "Related products"
 msgstr "Zugehörige Produkte"
 
-#: catalog/models.py:573
+#: catalog/models.py:589
 msgid "Acessories"
 msgstr "Zubehör"
 
-#: catalog/models.py:577
+#: catalog/models.py:593
 #: templates/manage/product/product_filters_inline.html:17
 msgid "For sale"
 msgstr "Im Angebot"
 
-#: catalog/models.py:578
+#: catalog/models.py:594
 msgid "For sale price"
 msgstr "Angebotspreis"
 
-#: catalog/models.py:579 core/models.py:75 marketing/models.py:35
+#: catalog/models.py:595 core/models.py:75 marketing/models.py:35
 #: page/models.py:15 payment/models.py:71 payment/models.py:124
 #: shipping/models.py:69 shipping/models.py:156
 #: templates/manage/category/products_inline.html:53
 msgid "Active"
 msgstr "Aktiv"
 
-#: catalog/models.py:584
+#: catalog/models.py:600
 msgid "Deliverable"
 msgstr "Lieferbar"
 
-#: catalog/models.py:585
+#: catalog/models.py:601
 msgid "Manual delivery time"
 msgstr "Manuelle Lieferzeit"
 
-#: catalog/models.py:586 shipping/models.py:77
+#: catalog/models.py:602 shipping/models.py:77
 msgid "Delivery time"
 msgstr "Lieferzeit"
 
-#: catalog/models.py:587
+#: catalog/models.py:603
 msgid "Order time"
 msgstr "Dauer Bestellung"
 
-#: catalog/models.py:588
+#: catalog/models.py:604
 msgid "Ordered at"
 msgstr "Bestellt am"
 
-#: catalog/models.py:589
+#: catalog/models.py:605
 msgid "Manage stock amount"
 msgstr "Verwaltung des Lagerbestandes"
 
-#: catalog/models.py:590
+#: catalog/models.py:606
 msgid "Stock amount"
 msgstr "Lagerbestand"
 
-#: catalog/models.py:592
-msgid "Active packing unit"
+#: catalog/models.py:608
+msgid "Active packing"
 msgstr "Verpackungseinheit aktivieren"
 
-#: catalog/models.py:593
+#: catalog/models.py:609
+msgid "Amount per packing"
+msgstr "Anzahl pro Verpackungseinheit"
+
+#: catalog/models.py:610
 msgid "Packing unit"