Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

abstract_models.py 9.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. from django.db import models
  2. from django.conf import settings
  3. from django.utils.encoding import python_2_unicode_compatible
  4. from django.utils.translation import ugettext_lazy as _, pgettext_lazy
  5. from oscar.core.compat import AUTH_USER_MODEL
  6. from oscar.models.fields import AutoSlugField
  7. from oscar.apps.partner.exceptions import InvalidStockAdjustment
  8. @python_2_unicode_compatible
  9. class AbstractPartner(models.Model):
  10. """
  11. A fulfillment partner. An individual or company who can fulfil products.
  12. E.g. for physical goods, somebody with a warehouse and means of delivery.
  13. Creating one or more instances of the Partner model is a required step in
  14. setting up an Oscar deployment. Many Oscar deployments will only have one
  15. fulfillment partner.
  16. """
  17. code = AutoSlugField(_("Code"), max_length=128, unique=True,
  18. populate_from='name')
  19. name = models.CharField(
  20. pgettext_lazy(u"Partner's name", u"Name"), max_length=128, blank=True)
  21. #: A partner can have users assigned to it. This is used
  22. #: for access modelling in the permission-based dashboard
  23. users = models.ManyToManyField(
  24. AUTH_USER_MODEL, related_name="partners",
  25. blank=True, null=True, verbose_name=_("Users"))
  26. @property
  27. def display_name(self):
  28. return self.name or self.code
  29. @property
  30. def primary_address(self):
  31. """
  32. Returns a partners primary address. Usually that will be the
  33. headquarters or similar.
  34. This is a rudimentary implementation that raises an error if there's
  35. more than one address. If you actually want to support multiple
  36. addresses, you will likely need to extend PartnerAddress to have some
  37. field or flag to base your decision on.
  38. """
  39. addresses = self.addresses.all()
  40. if len(addresses) == 0: # intentionally using len() to save queries
  41. return None
  42. elif len(addresses) == 1:
  43. return addresses[0]
  44. else:
  45. raise NotImplementedError(
  46. "Oscar's default implementation of primary_address only "
  47. "supports one PartnerAddress. You need to override the "
  48. "primary_address to look up the right address")
  49. def get_address_for_stockrecord(self, stockrecord):
  50. """
  51. Stock might be coming from different warehouses. Overriding this
  52. function allows selecting the correct PartnerAddress for the record.
  53. That can be useful when determining tax.
  54. """
  55. return self.primary_address
  56. class Meta:
  57. abstract = True
  58. app_label = 'partner'
  59. permissions = (('dashboard_access', 'Can access dashboard'), )
  60. verbose_name = _('Fulfillment partner')
  61. verbose_name_plural = _('Fulfillment partners')
  62. def __str__(self):
  63. return self.display_name
  64. @python_2_unicode_compatible
  65. class AbstractStockRecord(models.Model):
  66. """
  67. A stock record.
  68. This records information about a product from a fulfilment partner, such as
  69. their SKU, the number they have in stock and price information.
  70. Stockrecords are used by 'strategies' to determine availability and pricing
  71. information for the customer.
  72. """
  73. product = models.ForeignKey(
  74. 'catalogue.Product', related_name="stockrecords",
  75. verbose_name=_("Product"))
  76. partner = models.ForeignKey(
  77. 'partner.Partner', verbose_name=_("Partner"),
  78. related_name='stockrecords')
  79. #: The fulfilment partner will often have their own SKU for a product,
  80. #: which we store here. This will sometimes be the same the product's UPC
  81. #: but not always. It should be unique per partner.
  82. #: See also http://en.wikipedia.org/wiki/Stock-keeping_unit
  83. partner_sku = models.CharField(_("Partner SKU"), max_length=128)
  84. # Price info:
  85. price_currency = models.CharField(
  86. _("Currency"), max_length=12, default=settings.OSCAR_DEFAULT_CURRENCY)
  87. # This is the base price for calculations - tax should be applied by the
  88. # appropriate method. We don't store tax here as its calculation is highly
  89. # domain-specific. It is NULLable because some items don't have a fixed
  90. # price but require a runtime calculation (possible from an external
  91. # service).
  92. price_excl_tax = models.DecimalField(
  93. _("Price (excl. tax)"), decimal_places=2, max_digits=12,
  94. blank=True, null=True)
  95. #: Retail price for this item. This is simply the recommended price from
  96. #: the manufacturer. If this is used, it is for display purposes only.
  97. #: This prices is the NOT the price charged to the customer.
  98. price_retail = models.DecimalField(
  99. _("Price (retail)"), decimal_places=2, max_digits=12,
  100. blank=True, null=True)
  101. #: Cost price is the price charged by the fulfilment partner. It is not
  102. #: used (by default) in any price calculations but is often used in
  103. #: reporting so merchants can report on their profit margin.
  104. cost_price = models.DecimalField(
  105. _("Cost Price"), decimal_places=2, max_digits=12,
  106. blank=True, null=True)
  107. #: Number of items in stock
  108. num_in_stock = models.PositiveIntegerField(
  109. _("Number in stock"), blank=True, null=True)
  110. #: The amount of stock allocated to orders but not fed back to the master
  111. #: stock system. A typical stock update process will set the num_in_stock
  112. #: variable to a new value and reset num_allocated to zero
  113. num_allocated = models.IntegerField(
  114. _("Number allocated"), blank=True, null=True)
  115. #: Threshold for low-stock alerts. When stock goes beneath this threshold,
  116. #: an alert is triggered so warehouse managers can order more.
  117. low_stock_threshold = models.PositiveIntegerField(
  118. _("Low Stock Threshold"), blank=True, null=True)
  119. # Date information
  120. date_created = models.DateTimeField(_("Date created"), auto_now_add=True)
  121. date_updated = models.DateTimeField(_("Date updated"), auto_now=True,
  122. db_index=True)
  123. def __str__(self):
  124. msg = u"Partner: %s, product: %s" % (
  125. self.partner.display_name, self.product,)
  126. if self.partner_sku:
  127. msg = u"%s (%s)" % (msg, self.partner_sku)
  128. return msg
  129. class Meta:
  130. abstract = True
  131. app_label = 'partner'
  132. unique_together = ('partner', 'partner_sku')
  133. verbose_name = _("Stock record")
  134. verbose_name_plural = _("Stock records")
  135. @property
  136. def net_stock_level(self):
  137. """
  138. The effective number in stock (eg available to buy).
  139. This is correct property to show the customer, not the num_in_stock
  140. field as that doesn't account for allocations. This can be negative in
  141. some unusual circumstances
  142. """
  143. if self.num_in_stock is None:
  144. return 0
  145. if self.num_allocated is None:
  146. return self.num_in_stock
  147. return self.num_in_stock - self.num_allocated
  148. # 2-stage stock management model
  149. def allocate(self, quantity):
  150. """
  151. Record a stock allocation.
  152. This normally happens when a product is bought at checkout. When the
  153. product is actually shipped, then we 'consume' the allocation.
  154. """
  155. if self.num_allocated is None:
  156. self.num_allocated = 0
  157. self.num_allocated += quantity
  158. self.save()
  159. allocate.alters_data = True
  160. def is_allocation_consumption_possible(self, quantity):
  161. """
  162. Test if a proposed stock consumption is permitted
  163. """
  164. return quantity <= min(self.num_allocated, self.num_in_stock)
  165. def consume_allocation(self, quantity):
  166. """
  167. Consume a previous allocation
  168. This is used when an item is shipped. We remove the original
  169. allocation and adjust the number in stock accordingly
  170. """
  171. if not self.is_allocation_consumption_possible(quantity):
  172. raise InvalidStockAdjustment(
  173. _('Invalid stock consumption request'))
  174. self.num_allocated -= quantity
  175. self.num_in_stock -= quantity
  176. self.save()
  177. consume_allocation.alters_data = True
  178. def cancel_allocation(self, quantity):
  179. # We ignore requests that request a cancellation of more than the
  180. # amount already allocated.
  181. self.num_allocated -= min(self.num_allocated, quantity)
  182. self.save()
  183. cancel_allocation.alters_data = True
  184. @property
  185. def is_below_threshold(self):
  186. if self.low_stock_threshold is None:
  187. return False
  188. return self.net_stock_level < self.low_stock_threshold
  189. @python_2_unicode_compatible
  190. class AbstractStockAlert(models.Model):
  191. """
  192. A stock alert. E.g. used to notify users when a product is 'back in stock'.
  193. """
  194. stockrecord = models.ForeignKey(
  195. 'partner.StockRecord', related_name='alerts',
  196. verbose_name=_("Stock Record"))
  197. threshold = models.PositiveIntegerField(_("Threshold"))
  198. OPEN, CLOSED = "Open", "Closed"
  199. status_choices = (
  200. (OPEN, _("Open")),
  201. (CLOSED, _("Closed")),
  202. )
  203. status = models.CharField(_("Status"), max_length=128, default=OPEN,
  204. choices=status_choices)
  205. date_created = models.DateTimeField(_("Date Created"), auto_now_add=True)
  206. date_closed = models.DateTimeField(_("Date Closed"), blank=True, null=True)
  207. def close(self):
  208. self.status = self.CLOSED
  209. self.save()
  210. close.alters_data = True
  211. def __str__(self):
  212. return _('<stockalert for "%(stock)s" status %(status)s>') \
  213. % {'stock': self.stockrecord, 'status': self.status}
  214. class Meta:
  215. abstract = True
  216. app_label = 'partner'
  217. ordering = ('-date_created',)
  218. verbose_name = _('Stock alert')
  219. verbose_name_plural = _('Stock alerts')