| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- from decimal import Decimal as D
-
- from django.conf import settings
- from django.db import models
- from django.utils.translation import ugettext_lazy as _
- from django.utils.importlib import import_module as django_import_module
-
- from oscar.core.loading import get_class
- from oscar.apps.partner.exceptions import InvalidStockAdjustment
- DefaultWrapper = get_class('partner.wrappers', 'DefaultWrapper')
-
-
- # Cache the partners for quicklookups
- default_wrapper = DefaultWrapper()
- partner_wrappers = {}
- for partner, class_str in settings.OSCAR_PARTNER_WRAPPERS.items():
- bits = class_str.split('.')
- class_name = bits.pop()
- module_str = '.'.join(bits)
- module = django_import_module(module_str)
- partner_wrappers[partner] = getattr(module, class_name)()
-
-
- def get_partner_wrapper(partner_name):
- """
- Returns the appropriate partner wrapper given the partner name
- """
- return partner_wrappers.get(partner_name, default_wrapper)
-
-
- class AbstractPartner(models.Model):
- """
- Fulfillment partner
- """
- name = models.CharField(_("Name"), max_length=128, unique=True)
-
- # A partner can have users assigned to it. These can be used
- # to provide authentication for webservices etc.
- users = models.ManyToManyField('auth.User', related_name="partners", blank=True, null=True,
- verbose_name=_("Users"))
-
- class Meta:
- verbose_name = _('Fulfillment Partner')
- verbose_name_plural = _('Fulfillment Partners')
- abstract = True
- permissions = (
- ("can_edit_stock_records", _("Can edit stock records")),
- ("can_view_stock_records", _("Can view stock records")),
- ("can_edit_product_range", _("Can edit product range")),
- ("can_view_product_range", _("Can view product range")),
- ("can_edit_order_lines", _("Can edit order lines")),
- ("can_view_order_lines", _("Can view order lines"))
- )
-
- def __unicode__(self):
- return self.name
-
-
- class AbstractStockRecord(models.Model):
- """
- A basic stock record.
-
- This links a product to a partner, together with price and availability
- information. Most projects will need to subclass this object to add custom
- fields such as lead_time, report_code, min_quantity.
-
- We deliberately don't store tax information to allow each project
- to subclass this model and put its own fields for convey tax.
- """
- product = models.OneToOneField('catalogue.Product', related_name="stockrecord", verbose_name=_("Product"))
- partner = models.ForeignKey('partner.Partner', verbose_name=_("Partner"))
-
- # The fulfilment partner will often have their own SKU for a product, which
- # we store here.
- partner_sku = models.CharField(_("Partner SKU"), max_length=128)
-
- # Price info:
- price_currency = models.CharField(_("Currency"), max_length=12, default=settings.OSCAR_DEFAULT_CURRENCY)
-
- # This is the base price for calculations - tax should be applied
- # by the appropriate method. We don't store it here as its calculation is
- # highly domain-specific. It is NULLable because some items don't have a fixed
- # price.
- price_excl_tax = models.DecimalField(_("Price (excl. tax)"), decimal_places=2, max_digits=12, blank=True, null=True)
-
- # Retail price for this item
- price_retail = models.DecimalField(_("Price (retail)"), decimal_places=2, max_digits=12, blank=True, null=True)
-
- # Cost price is optional as not all partners supply it
- cost_price = models.DecimalField(_("Cost Price"), decimal_places=2, max_digits=12, blank=True, null=True)
-
- # Stock level information
- num_in_stock = models.PositiveIntegerField(_("Number in stock"), default=0, blank=True, null=True)
-
- # Threshold for low-stock alerts
- low_stock_threshold = models.PositiveIntegerField(_("Low Stock Threshold"), blank=True, null=True)
-
- # The amount of stock allocated to orders but not fed back to the master
- # stock system. A typical stock update process will set the num_in_stock
- # variable to a new value and reset num_allocated to zero
- num_allocated = models.IntegerField(_("Number of Allocated"), default=0, blank=True, null=True)
-
- # Date information
- date_created = models.DateTimeField(_("Date Created"), auto_now_add=True)
- date_updated = models.DateTimeField(_("Date Updated"), auto_now=True, db_index=True)
-
- class Meta:
- abstract = True
- unique_together = ('partner', 'partner_sku')
- verbose_name = _("Stock Record")
- verbose_name_plural = _("Stock Records")
-
- # 2-stage stock management model
-
- def allocate(self, quantity):
- """
- Record a stock allocation.
-
- This normally happens when a product is bought at checkout. When the
- product is actually shipped, then we 'consume' the allocation.
- """
- if self.num_allocated is None:
- self.num_allocated = 0
- self.num_allocated += quantity
- self.save()
-
- def is_allocation_consumption_possible(self, quantity):
- return quantity <= min(self.num_allocated, self.num_in_stock)
-
- def consume_allocation(self, quantity):
- """
- Consume a previous allocation
-
- This is used when an item is shipped. We remove the original allocation
- and adjust the number in stock accordingly
- """
- if not self.is_allocation_consumption_possible(quantity):
- raise InvalidStockAdjustment(_('Invalid stock consumption request'))
- self.num_allocated -= quantity
- self.num_in_stock -= quantity
- self.save()
-
- def cancel_allocation(self, quantity):
- # We ignore requests that request a cancellation of more than the amount already
- # allocated.
- self.num_allocated -= min(self.num_allocated, quantity)
- self.save()
-
- @property
- def net_stock_level(self):
- """
- Return the effective number in stock. This is correct property to show
- the customer, not the num_in_stock field as that doesn't account for
- allocations. This can be negative in some unusual circumstances
- """
- if self.num_in_stock is None:
- return 0
- if self.num_allocated is None:
- return self.num_in_stock
- return self.num_in_stock - self.num_allocated
-
- def set_discount_price(self, price):
- """
- A setter method for setting a new price.
-
- This is called from within the "discount" app, which is responsible
- for applying fixed-discount offers to products. We use a setter method
- so that this behaviour can be customised in projects.
- """
- self.price_excl_tax = price
- self.save()
-
- # Price retrieval methods - these default to no tax being applicable
- # These are intended to be overridden.
-
- @property
- def is_available_to_buy(self):
- """
- Return whether this stockrecord allows the product to be purchased
- """
- return get_partner_wrapper(self.partner.name).is_available_to_buy(self)
-
- def is_purchase_permitted(self, user=None, quantity=1):
- """
- Return whether this stockrecord allows the product to be purchased by a
- specific user and quantity
- """
- return get_partner_wrapper(self.partner.name).is_purchase_permitted(self, user, quantity)
-
- @property
- def is_below_threshold(self):
- if self.low_stock_threshold is None:
- return False
- return self.net_stock_level < self.low_stock_threshold
-
- @property
- def availability_code(self):
- """
- Return an product's availability as a code for use in CSS to add icons
- to the overall availability mark-up. For example, "instock",
- "unavailable".
- """
- return get_partner_wrapper(self.partner.name).availability_code(self)
-
- @property
- def availability(self):
- """
- Return a product's availability as a string that can be displayed to the
- user. For example, "In stock", "Unavailabl".
- """
- return get_partner_wrapper(self.partner.name).availability(self)
-
- def max_purchase_quantity(self, user=None):
- """
- Return an item's availability as a string
-
- :param user: (optional) The user who wants to purchase
- """
- return get_partner_wrapper(self.partner.name).max_purchase_quantity(self, user)
-
- @property
- def dispatch_date(self):
- """
- Return the estimated dispatch date for a line
- """
- return get_partner_wrapper(self.partner.name).dispatch_date(self)
-
- @property
- def lead_time(self):
- return get_partner_wrapper(self.partner.name).lead_time(self)
-
- @property
- def price_incl_tax(self):
- """
- Return a product's price including tax.
-
- This defaults to the price_excl_tax as tax calculations are
- domain specific. This class needs to be subclassed and tax logic
- added to this method.
- """
- if self.price_excl_tax is None:
- return D('0.00')
- return self.price_excl_tax + self.price_tax
-
- @property
- def price_tax(self):
- """
- Return a product's tax value
- """
- return get_partner_wrapper(self.partner.name).calculate_tax(self)
-
- def __unicode__(self):
- if self.partner_sku:
- return "%s (%s): %s" % (self.partner.name, self.partner_sku, self.product.title)
- else:
- return "%s: %s" % (self.partner.name, self.product.title)
-
-
- class AbstractStockAlert(models.Model):
- stockrecord = models.ForeignKey('partner.StockRecord', related_name='alerts', verbose_name=_("Stock Record"))
- threshold = models.PositiveIntegerField(_("Threshold"))
- OPEN, CLOSED = "Open", "Closed"
- status_choices = (
- (OPEN, _("Open")),
- (CLOSED, _("Closed")),
- )
- status = models.CharField(_("Status"), max_length=128, default=OPEN, choices=status_choices)
- date_created = models.DateTimeField(_("Date Created"), auto_now_add=True)
- date_closed = models.DateTimeField(_("Date Closed"), blank=True, null=True)
-
- def close(self):
- self.status = self.CLOSED
- self.save()
-
- def __unicode__(self):
- return _('<stockalert for "%(stock)s" status %(status)s>') % {'stock': self.stockrecord, 'status': self.status}
-
- class Meta:
- ordering = ('-date_created',)
- verbose_name = _('Stock Alert')
- verbose_name_plural = _('Stock Alerts')
|