You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import os
  2. from decimal import Decimal as D
  3. from django.db.transaction import commit_on_success
  4. from django.utils.translation import ugettext_lazy as _
  5. from oscar.apps.catalogue.categories import create_from_breadcrumbs
  6. from oscar.apps.dashboard.reports.csv_utils import CsvUnicodeReader
  7. from oscar.core.loading import get_class, get_classes
  8. ImportError = get_class('partner.exceptions', 'ImportError')
  9. Partner, StockRecord = get_classes('partner.models', ('Partner', 'StockRecord'))
  10. ProductClass, Product, Category, ProductCategory = get_classes(
  11. 'catalogue.models', ('ProductClass', 'Product', 'Category',
  12. 'ProductCategory'))
  13. class StockImporter(object):
  14. def __init__(self, logger, partner, delimiter):
  15. self.logger = logger
  16. self._delimiter = delimiter
  17. try:
  18. self._partner = Partner.objects.get(name=partner)
  19. except Partner.DoesNotExist:
  20. name_list = ", ".join([d['name'] for d in Partner.objects.values('name')])
  21. raise ImportError(_("Partner named '%(partner)s' does not exist (existing partners: %(list)s)") % {
  22. 'partner': partner, 'list': name_list})
  23. def handle(self, file_path=None):
  24. u"""Handles the actual import process"""
  25. if not file_path:
  26. raise ImportError(_("No file path supplied"))
  27. Validator().validate(file_path)
  28. self._import(file_path)
  29. def _import(self, file_path):
  30. u"""Imports given file"""
  31. stats = {'updated_items': 0,
  32. 'unchanged_items': 0,
  33. 'unmatched_items': 0}
  34. row_number = 0
  35. for row in CsvUnicodeReader(open(file_path, 'rb'), delimiter=self._delimiter, quotechar='"', escapechar='\\'):
  36. row_number += 1
  37. self._import_row(row_number, row, stats)
  38. msg = "\tUpdated items: %d\n\tUnchanged items: %d\n\tUnmatched items: %d" % (
  39. stats['updated_items'],
  40. stats['unchanged_items'],
  41. stats['unmatched_items'])
  42. self.logger.info(msg)
  43. def _import_row(self, row_number, row, stats):
  44. if len(row) != 3:
  45. self.logger.error("Row number %d has an invalid number of fields, skipping..." % row_number)
  46. else:
  47. self._update_stockrecord(*row[:3], row_number=row_number, stats=stats)
  48. def _update_stockrecord(self, partner_sku, price_excl_tax, num_in_stock, row_number, stats):
  49. try:
  50. stock = StockRecord.objects.get(partner=self._partner, partner_sku=partner_sku)
  51. except StockRecord.DoesNotExist:
  52. stats['unmatched_items'] += 1
  53. self.logger.error("\t - Row %d: StockRecord for partner '%s' and sku '%s' does not exist, skipping..." % (row_number, self._partner, partner_sku))
  54. return
  55. price_changed = False
  56. if stock.price_excl_tax != D(price_excl_tax):
  57. stock.price_excl_tax = D(price_excl_tax)
  58. price_changed = True
  59. stock_changed = False
  60. if stock.num_in_stock != int(num_in_stock):
  61. stock.num_in_stock = num_in_stock
  62. stock_changed = True
  63. if price_changed or stock_changed:
  64. stock.save()
  65. msg = " SKU %s:" % (partner_sku)
  66. if price_changed:
  67. msg += '\n - Price set to %s' % (price_excl_tax)
  68. if stock_changed:
  69. msg += '\n - Stock set to %s' % num_in_stock
  70. self.logger.info(msg)
  71. stats['updated_items'] += 1
  72. else:
  73. stats['unchanged_items'] += 1
  74. # Deprecated
  75. class CatalogueImporter(object):
  76. """
  77. A catalogue importer object
  78. """
  79. _flush = False
  80. def __init__(self, logger, delimiter=",", flush=False):
  81. self.logger = logger
  82. self._delimiter = delimiter
  83. self._flush = flush
  84. def handle(self, file_path=None):
  85. u"""Handles the actual import process"""
  86. if not file_path:
  87. raise ImportError(_("No file path supplied"))
  88. Validator().validate(file_path)
  89. if self._flush is True:
  90. self.logger.info(" - Flushing product data before import")
  91. self._flush_product_data()
  92. self._import(file_path)
  93. def _flush_product_data(self):
  94. u"""Flush out product and stock models"""
  95. ProductClass.objects.all().delete()
  96. Product.objects.all().delete()
  97. Partner.objects.all().delete()
  98. StockRecord.objects.all().delete()
  99. @commit_on_success
  100. def _import(self, file_path):
  101. u"""Imports given file"""
  102. stats = {'new_items': 0,
  103. 'updated_items': 0}
  104. row_number = 0
  105. for row in CsvUnicodeReader(open(file_path,'rb'), delimiter=self._delimiter, quotechar='"', escapechar='\\'):
  106. row_number += 1
  107. self._import_row(row_number, row, stats)
  108. msg = "New items: %d, updated items: %d" % (stats['new_items'], stats['updated_items'])
  109. self.logger.info(msg)
  110. def _import_row(self, row_number, row, stats):
  111. if len(row) != 5 and len(row) != 9:
  112. self.logger.error("Row number %d has an invalid number of fields (%d), skipping..." % (row_number, len(row)))
  113. return
  114. item = self._create_item(*row[:5], stats=stats)
  115. if len(row) == 9:
  116. # With stock data
  117. self._create_stockrecord(item, *row[5:9], stats=stats)
  118. def _create_item(self, product_class, category_str, upc, title, description, stats):
  119. # Ignore any entries that are NULL
  120. if description == 'NULL':
  121. description = ''
  122. # Create item class and item
  123. product_class, __ = ProductClass.objects.get_or_create(name=product_class)
  124. try:
  125. item = Product.objects.get(upc=upc)
  126. stats['updated_items'] += 1
  127. except Product.DoesNotExist:
  128. item = Product()
  129. stats['new_items'] += 1
  130. item.upc = upc
  131. item.title = title
  132. item.description = description
  133. item.product_class = product_class
  134. item.save()
  135. # Category
  136. cat = create_from_breadcrumbs(category_str)
  137. ProductCategory.objects.create(product=item, category=cat)
  138. return item
  139. def _create_stockrecord(self, item, partner_name, partner_sku,
  140. price_excl_tax, num_in_stock, stats):
  141. # Create partner and stock record
  142. partner, _ = Partner.objects.get_or_create(
  143. name=partner_name)
  144. try:
  145. stock = StockRecord.objects.get(partner_sku=partner_sku)
  146. except StockRecord.DoesNotExist:
  147. stock = StockRecord()
  148. stock.product = item
  149. stock.partner = partner
  150. stock.partner_sku = partner_sku
  151. stock.price_excl_tax = D(price_excl_tax)
  152. stock.num_in_stock = num_in_stock
  153. stock.save()
  154. class Validator(object):
  155. def validate(self, file_path):
  156. self._exists(file_path)
  157. self._is_file(file_path)
  158. self._is_readable(file_path)
  159. def _exists(self, file_path):
  160. u"""Check whether a file exists"""
  161. if not os.path.exists(file_path):
  162. raise ImportError(_("%s does not exist") % (file_path))
  163. def _is_file(self, file_path):
  164. u"""Check whether file is actually a file type"""
  165. if not os.path.isfile(file_path):
  166. raise ImportError(_("%s is not a file") % (file_path))
  167. def _is_readable(self, file_path):
  168. u"""Check file is readable"""
  169. try:
  170. f = open(file_path, 'r')
  171. f.close()
  172. except:
  173. raise ImportError(_("%s is not readable") % (file_path))