Coverage for /home/chris/workspace/django-shop/shop/models/defaults/bases: 98.07%

# Coverage for /home/chris/workspace/django-shop/shop/models/defaults/bases : 98.07%

## 183 statements 182 run 1 missing 0 excluded 3 partial

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 | # -*- coding: utf-8 -*- from decimal import Decimal from distutils.version import LooseVersion from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.db import models from django.db.models.aggregates import Sum from django.utils.translation import ugettext_lazy as _ from polymorphic.polymorphic_model import PolymorphicModel from shop.cart.modifiers_pool import cart_modifiers_pool from shop.util.fields import CurrencyField from shop.util.loader import get_model_string import django #============================================================================== # Product #============================================================================== class BaseProduct(PolymorphicModel): """ A basic product for the shop Most of the already existing fields here should be generic enough to reside on the "base model" and not on an added property """ name = models.CharField(max_length=255, verbose_name=_('Name')) slug = models.SlugField(verbose_name=_('Slug')) active = models.BooleanField(default=False, verbose_name=_('Active')) date_added = models.DateTimeField(auto_now_add=True, verbose_name=_('Date added')) last_modified = models.DateTimeField(auto_now=True, verbose_name=_('Last modified')) unit_price = CurrencyField(verbose_name=_('Unit price')) class Meta(object): abstract = True app_label = 'shop' verbose_name = _('Product') verbose_name_plural = _('Products') def __unicode__(self): return self.name def get_absolute_url(self): return reverse('product_detail', args=[self.slug]) def get_price(self): """ Return the price for this item (provided for extensibility) """ return self.unit_price def get_name(self): """ Return the name of this Product (provided for extensibility) """ return self.name #============================================================================== # Carts #============================================================================== class BaseCart(models.Model): """ This should be a rather simple list of items. Ideally it should be bound to a session and not to a User is we want to let people buy from our shop without having to register with us. """ # If the user is null, that means this is used for a session user = models.OneToOneField(User, null=True, blank=True) date_created = models.DateTimeField(auto_now_add=True) last_updated = models.DateTimeField(auto_now=True) class Meta(object): abstract = True app_label = 'shop' verbose_name = _('Cart') verbose_name_plural = _('Carts') def __init__(self, *args, **kwargs): super(BaseCart, self).__init__(*args,**kwargs) # That will hold things like tax totals or total discount self.subtotal_price = Decimal('0.0') self.total_price = Decimal('0.0') self.current_total = Decimal('0.0') # used by cart modifiers self.extra_price_fields = [] # List of tuples (label, value) def add_product(self, product, quantity=1, merge=True, queryset=None): """ Adds a (new) product to the cart. The parameter `merge`, controls wheter we should merge the added CartItem with another already existing sharing the same product_id. This is useful when you have products with variations (for example), and you don't want to have your products merge (to loose their specific variations, for example). A drawback is, that generally setting `merge` to ``False`` for products with variations can be a problem if users can buy thousands of products at a time (that would mean we would create thousands of CartItems as well which all have the same variation). The parameter `queryset` can be used to override the standard queryset that is being used to find the CartItem that should be merged into. If you use variations, just finding the first CartItem that belongs to this cart and the given product is not sufficient. You will want to find the CartItem that already has the same variations that the user chose for this request. Example with merge = True: >>> self.items[0] = CartItem.objects.create(..., product=MyProduct()) >>> self.add_product(MyProduct()) >>> self.items[0].quantity 2 Example with merge=False: >>> self.items[0] = CartItem.objects.create(..., product=MyProduct()) >>> self.add_product(MyProduct()) >>> self.items[0].quantity 1 >>> self.items[1].quantity 1 """ from shop.models import CartItem if queryset == None: queryset = CartItem.objects.filter(cart=self, product=product) item = queryset # Let's see if we already have an Item with the same product ID if item.exists() and merge: cart_item = item[0] cart_item.quantity = cart_item.quantity + int(quantity) cart_item.save() else: cart_item = CartItem.objects.create( cart=self, quantity=quantity, product=product) cart_item.save() self.save() # to get the last updated timestamp return cart_item def update_quantity(self, cart_item_id, quantity): """ Updates the quantity for given cart item or deletes it if its quantity reaches `0` """ cart_item = self.items.get(pk=cart_item_id) if quantity == 0: cart_item.delete() else: cart_item.quantity = quantity cart_item.save() self.save() def delete_item(self, cart_item_id): """ A simple convenience method to delete one of the cart's items. This allows to implicitely check for "access rights" since we insure the cartitem is actually in the user's cart """ cart_item = self.items.get(pk=cart_item_id) cart_item.delete() self.save() def update(self): """ This should be called whenever anything is changed in the cart (added or removed). It will loop on all line items in the cart, and call all the price modifiers on each row. After doing this, it will compute and update the order's total and subtotal fields, along with any payment field added along the way by modifiers. Note that theses added fields are not stored - we actually want to reflect rebate and tax changes on the *cart* items, but we don't want that for the order items (since they are legally binding after the "purchase" button was pressed) """ from shop.models import CartItem items = CartItem.objects.filter(cart=self) self.extra_price_fields = [] # Reset the price fields self.subtotal_price = Decimal('0.0') # Reset the subtotal for item in items: # For each OrderItem (order line)... self.subtotal_price = self.subtotal_price + item.update() item.save() self.current_total = self.subtotal_price # Now we have to iterate over the registered modifiers again (unfortunately) # to pass them the whole Order this time for modifier in cart_modifiers_pool.get_modifiers_list(): modifier.process_cart(self) self.total_price = self.current_total def empty(self): """ Remove all cart items """ self.items.all().delete() self.delete() @property def total_quantity(self): """ Returns the total quantity of all items in the cart """ return sum([ci.quantity for ci in self.items.all()]) class BaseCartItem(models.Model): """ This is a holder for the quantity of items in the cart and, obviously, a pointer to the actual Product being purchased :) """ cart = models.ForeignKey(get_model_string('Cart'), related_name="items") quantity = models.IntegerField() product = models.ForeignKey(get_model_string('Product')) class Meta(object): abstract = True app_label = 'shop' verbose_name = _('Cart item') verbose_name_plural = _('Cart items') def __init__(self, *args, **kwargs): # That will hold extra fields to display to the user # (ex. taxes, discount) super(BaseCartItem, self).__init__(*args,**kwargs) self.extra_price_fields = [] # list of tuples (label, value) # These must not be stored, since their components can be changed between # sessions / logins etc... self.line_subtotal = Decimal('0.0') self.line_total = Decimal('0.0') self.current_total = Decimal('0.0') # Used by cart modifiers def update(self): self.extra_price_fields = [] # Reset the price fields self.line_subtotal = self.product.get_price() * self.quantity self.current_total = self.line_subtotal for modifier in cart_modifiers_pool.get_modifiers_list(): # We now loop over every registered price modifier, # most of them will simply add a field to extra_payment_fields modifier.process_cart_item(self) self.line_total = self.current_total return self.line_total #============================================================================== # Orders #============================================================================== class BaseOrder(models.Model): """ A model representing an Order. An order is the "in process" counterpart of the shopping cart, which holds stuff like the shipping and billing addresses (copied from the User profile) when the Order is first created), list of items, and holds stuff like the status, shipping costs, taxes, etc... """ PROCESSING = 1 # New order, no shipping/payment backend chosen yet PAYMENT = 2 # The user is filling in payment information CONFIRMED = 3 # Chosen shipping/payment backend, processing payment COMPLETED = 4 # Successful payment confirmed by payment backend SHIPPED = 5 # successful order shipped to client CANCELLED = 6 # order has been cancelled STATUS_CODES = ( (PROCESSING, 'Processing'), (PAYMENT, 'Selecting payment'), (CONFIRMED, 'Confirmed'), (COMPLETED, 'Completed'), (SHIPPED, 'Shipped'), (CANCELLED, 'Cancelled'), ) # If the user is null, the order was created with a session user = models.ForeignKey(User, blank=True, null=True, verbose_name=_('User')) status = models.IntegerField(choices=STATUS_CODES, default=PROCESSING, verbose_name=_('Status')) order_subtotal = CurrencyField(verbose_name=_('Order subtotal')) order_total = CurrencyField(verbose_name='Order total') shipping_address_text = models.TextField(_('Shipping address'), blank=True, null=True) billing_address_text = models.TextField(_('Billing address'), blank=True, null=True) created = models.DateTimeField(auto_now_add=True, verbose_name=_('Created')) modified = models.DateTimeField(auto_now=True, verbose_name=_('Updated')) class Meta(object): abstract = True app_label = 'shop' verbose_name = _('Order') verbose_name_plural = _('Orders') def __unicode__(self): return _('Order ID: %(id)s') % {'id': self.id} def get_absolute_url(self): return reverse('order_detail', kwargs={'pk': self.pk }) def is_payed(self): """Has this order been integrally payed for?""" return self.amount_payed == self.order_total def is_completed(self): return self.status == self.COMPLETED @property def amount_payed(self): """ The amount payed is the sum of related orderpayments """ from shop.models import OrderPayment sum = OrderPayment.objects.filter(order=self).aggregate(sum=Sum('amount')) result = sum.get('sum') if not result: result = Decimal('-1') return result @property def shipping_costs(self): from shop.models import ExtraOrderPriceField sum = Decimal('0.0') cost_list = ExtraOrderPriceField.objects.filter(order=self).filter(is_shipping=True) for cost in cost_list: sum = sum + cost.value return sum def set_billing_address(self, billing_address): """ Process billing_address trying to get as_text method from address and copying. You can override this method to process address more granulary e.g. you can copy address instance and save FK to it in your order class """ exit if hasattr(billing_address, 'as_text'): self.billing_address_text = billing_address.as_text() self.save() def set_shipping_address(self, shipping_address): """ Process shipping_address trying to get as_text method from address and copying. You can override this method to process address more granulary e.g. you can copy address instance and save FK to it in your order class """ exit if hasattr(shipping_address, 'as_text'): self.shipping_address_text = shipping_address.as_text() self.save() # We need some magic to support django < 1.3 that has no support models.on_delete option f_kwargs = {} 376if LooseVersion(django.get_version()) >= LooseVersion('1.3'): f_kwargs['on_delete'] = models.SET_NULL class BaseOrderItem(models.Model): """ A line Item for an order. """ order = models.ForeignKey(get_model_string('Order'), related_name='items', verbose_name=_('Order')) product_reference = models.CharField(max_length=255, verbose_name=_('Product reference')) product_name = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Product name')) product = models.ForeignKey(get_model_string('Product'), verbose_name=_('Product'), null=True, blank=True, **f_kwargs) unit_price = CurrencyField(verbose_name=_('Unit price')) quantity = models.IntegerField(verbose_name=_('Quantity')) line_subtotal = CurrencyField(verbose_name=_('Line subtotal')) line_total = CurrencyField(verbose_name=_('Line total')) class Meta(object): abstract = True app_label = 'shop' verbose_name = _('Order item') verbose_name_plural = _('Order items') def save(self, *args, **kwargs): if self.product: self.product_name = self.product.name super(BaseOrderItem, self).save(*args, **kwargs)