PageRenderTime 50ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/scylla_spree/records.py

https://gitlab.com/5stones/scylla-spree
Python | 428 lines | 390 code | 19 blank | 19 comment | 19 complexity | 31edc325f622e3f14b70ccdb03bb68a8 MD5 | raw file
  1. from datetime import timedelta
  2. from decimal import Decimal
  3. from scylla import records
  4. from scylla import convert
  5. from scylla import configuration
  6. from scylla import orientdb
  7. # recursion depth when initializing records
  8. _process_depth = 0
  9. # cache of records cleared after processing a response
  10. _record_cache = {}
  11. class SpreeRecord(records.Record):
  12. key_field = 'id'
  13. classname = 'SpreeRecord'
  14. _sub_records = {}
  15. _linked_records = {}
  16. _datetime_fields = ['created_at', 'updated_at', 'completed_at']
  17. _decimal_fields = []
  18. _int_fields = ['id']
  19. @classmethod
  20. def factory(cls, client, classname, obj, readonly_link=False):
  21. """Instantiates a SpreeRecord object using a subclass if one is available.
  22. Args:
  23. client: The spree api client that this record connects with
  24. classname: The name of the class in spree to append after Spree, which
  25. will be the OrientDB classname.
  26. obj: A dict or list that contains the data for this record(s),
  27. and its subrecords.
  28. """
  29. if obj is None:
  30. return None
  31. python_classname = 'Spree{0}'.format(classname)
  32. for subclass in cls.__subclasses__():
  33. if subclass.__name__ == python_classname:
  34. cls = subclass
  35. if isinstance(obj, records.Record):
  36. return obj
  37. elif isinstance(obj, dict):
  38. return cls.process_spree_obj(client, classname, obj, readonly_link)
  39. elif isinstance(obj, list):
  40. return [cls.process_spree_obj(client, classname, d, readonly_link) for d in obj]
  41. else:
  42. raise Exception('Subclass data must be a list or dict: '+str(obj))
  43. def __init__(self, client, classname, obj, readonly_link=False):
  44. super(SpreeRecord, self).__init__(obj)
  45. self.client = client
  46. self._readonly_link = readonly_link
  47. self.spree_module = client.name
  48. if classname:
  49. self.classname = self.spree_module + classname
  50. @classmethod
  51. def process_spree_obj(cls, client, classname, obj, readonly_link=False):
  52. global _process_depth
  53. if isinstance(obj, records.Record):
  54. # it's already converted
  55. return obj
  56. elif isinstance(obj, basestring) and obj[0] == '#':
  57. # this is just a string, so it's probably an RID because of a circular reference
  58. try:
  59. return _record_cache[obj]
  60. except KeyError:
  61. return obj
  62. # check record cache (creates circular reference instead of an infinite loop)
  63. if '@rid' in obj and obj['@rid'] in _record_cache:
  64. return _record_cache[obj['@rid']]
  65. # initialize the record
  66. rec = cls(client, classname, obj, readonly_link)
  67. # cache the record
  68. if '@rid' in obj:
  69. _record_cache[obj['@rid']] = rec
  70. # process the fields and initialize subrecords
  71. _process_depth += 1
  72. rec._process_fields()
  73. _process_depth -= 1
  74. # if we finished processing all records, clear our cache
  75. if _process_depth == 0:
  76. _record_cache.clear()
  77. return rec
  78. def _process_fields(self):
  79. #remove any invalid fields from the dict
  80. if "" in self.data:
  81. self.data.pop("", None)
  82. # create sub records (updates the record during save)
  83. for (field, classname) in self._sub_records.items():
  84. if field in self.data:
  85. self.data[field] = SpreeRecord.factory(self.client, classname, self.data[field])
  86. # create linked records
  87. for (field, classname) in self._linked_records.items():
  88. if field in self.data:
  89. self.data[field] = SpreeRecord.factory(self.client, classname, self.data[field], True)
  90. # process datetimes
  91. for field in self._datetime_fields:
  92. if field in self.data:
  93. self.data[field] = convert.parse_iso_date(self.data[field])
  94. for field in self._decimal_fields:
  95. if field in self.data:
  96. self.data[field] = Decimal(self[field])
  97. for field in self._int_fields:
  98. if field in self.data:
  99. self.data[field] = int(self[field])
  100. class SpreeAdjustableMixin(object):
  101. @property
  102. def tax_adjustments(self):
  103. return [a for a in self.get('adjustments') if a.is_tax]
  104. @property
  105. def discount_adjustments(self):
  106. return [a for a in self.get('adjustments') if a.is_discount]
  107. @property
  108. def markup_adjustments(self):
  109. return [a for a in self.get('adjustments') if a.is_markup]
  110. @property
  111. def total_tax(self):
  112. return sum(adjustment.get('amount') for adjustment in self.tax_adjustments) or Decimal()
  113. @property
  114. def total_discount(self):
  115. return sum(adjustment.get('amount') for adjustment in self.discount_adjustments) or Decimal()
  116. @property
  117. def total_markup(self):
  118. return sum(adjustment.get('amount') for adjustment in self.markup_adjustments) or Decimal()
  119. class SpreeCountry(SpreeRecord):
  120. def _process_fields(self):
  121. super(SpreeCountry, self)._process_fields()
  122. # states.state references a SpreeState
  123. if 'states' in self.data:
  124. for state_obj in self.data['states']:
  125. state_obj['state'] = SpreeRecord.factory(self.client, 'State', state_obj['state'], True)
  126. class SpreeAddress(SpreeRecord):
  127. _linked_records = {
  128. 'country': 'Country',
  129. 'state': 'State',
  130. }
  131. class SpreeUser(SpreeRecord):
  132. _sub_records = {
  133. 'ship_address': 'Address',
  134. 'bill_address': 'Address',
  135. }
  136. @property
  137. def full_name(self):
  138. bill_address = self.get('bill_address')
  139. if bill_address and bill_address.get('full_name'):
  140. return bill_address.get('full_name')
  141. else:
  142. return self.get('email')
  143. class SpreeAdjustment(SpreeRecord):
  144. _decimal_fields = ['amount']
  145. @property
  146. def is_tax(self):
  147. source_type = self.get('source_type')
  148. return self.get('eligible') and (source_type == 'Spree::TaxRate' or source_type == 'Spree::AvalaraTransaction')
  149. @property
  150. def is_shipping(self):
  151. return self.get('adjustable_type') == 'Spree::Shipment'
  152. @property
  153. def is_discount(self):
  154. return self.get('eligible') and not self.is_tax and self.get('amount') < 0.0
  155. @property
  156. def is_markup(self):
  157. return self.get('eligible') and not self.is_tax and self.get('amount') > 0.0
  158. class SpreeRefund(SpreeRecord):
  159. _decimal_fields = ['amount']
  160. _linked_records = {
  161. '_parent': 'Payment',
  162. }
  163. class SpreePayment(SpreeRecord):
  164. _decimal_fields = ['amount']
  165. _sub_records = {
  166. 'refunds': 'Refund',
  167. }
  168. _linked_records = {
  169. '_parent': 'Order',
  170. }
  171. @classmethod
  172. def capture_payments(cls, client, order_number, payments, from_rid=None):
  173. for payment in payments:
  174. if payment['state'] == 'checkout':
  175. cls.pay(client, order_number, payment['id'], from_rid=from_rid)
  176. @classmethod
  177. def pay(cls, client, order_number, number, from_rid=None):
  178. """Mark a payment as captured.
  179. """
  180. uri = "/orders/{0}/payments/{1}/capture".format(order_number, number)
  181. response = client.put(uri)
  182. # save updated shipment to orientdb
  183. payment = SpreeRecord.factory(client, 'Payment', response)
  184. payment.throw_at_orient()
  185. # link what updated this payment
  186. if from_rid:
  187. orientdb.create_edge(from_rid, payment.rid,
  188. 'Updated',
  189. content={'method':'PUT', 'uri':uri})
  190. return payment
  191. @property
  192. def is_invoice(self):
  193. return self.get('payment_method').get('name') == 'Invoice'
  194. @property
  195. def is_valid(self):
  196. return self.get('state') not in ['invalid', 'void', 'failed']
  197. @property
  198. def is_completed(self):
  199. return self.get('state') == 'completed'
  200. class SpreeShipment(SpreeAdjustableMixin, SpreeRecord):
  201. _sub_records = {
  202. 'adjustments': 'Adjustment',
  203. }
  204. _datetime_fields = ['created_at', 'updated_at', 'shipped_at']
  205. _decimal_fields = ['cost']
  206. @classmethod
  207. def shipReady(cls, client, shipments, tracking_number=None, shipped_at=None, from_rid=None):
  208. for shipment in shipments:
  209. if shipment['state'] == 'ready':
  210. shipment_number = shipment['number']
  211. if shipment['selected_shipping_rate']['shipping_method_id'] == 7:
  212. # digital shipment
  213. cls.ship(client, shipment_number, from_rid=from_rid)
  214. else:
  215. cls.ship(client, shipment_number, tracking_number, from_rid=from_rid)
  216. elif shipment['state'] == 'pending':
  217. raise Exception('Cannot ship a shipment in the "pending" state: {}'.format(shipment['number']))
  218. @classmethod
  219. def ship(cls, client, number, tracking_number=None, shipped_at=None, from_rid=None):
  220. """Mark a shipment as shipped.
  221. """
  222. params = {}
  223. if tracking_number:
  224. params['shipment[tracking]'] = tracking_number
  225. if shipped_at:
  226. params['shipment[shipped_at]'] = shipped_at
  227. uri = "/shipments/{0}/ship".format(number)
  228. response = client.put(uri, params)
  229. # save updated shipment to orientdb
  230. shipment = SpreeRecord.factory(client, 'Shipment', response)
  231. shipment.throw_at_orient()
  232. # link what updated this shipment
  233. if from_rid:
  234. orientdb.create_edge(from_rid, shipment.rid,
  235. 'Updated',
  236. content={'method':'PUT', 'uri':uri, 'params':params})
  237. return shipment
  238. @property
  239. def discount_rate(self):
  240. if self.get('cost') == 0:
  241. return 0.0
  242. return float(-100 * self.total_discount / self.get('cost'))
  243. class SpreeVariant(SpreeRecord):
  244. _sub_records = {
  245. #'images': 'Image',
  246. #'option_values': 'Option',
  247. }
  248. @property
  249. def full_name(self):
  250. name = self.get('name', '')
  251. options_text = self.get('options_text')
  252. if options_text:
  253. return u'{} ({})'.format(name, options_text)
  254. else:
  255. return name
  256. class SpreeLineItem(SpreeAdjustableMixin, SpreeRecord):
  257. _sub_records = {
  258. 'adjustments': 'Adjustment',
  259. }
  260. _linked_records = {
  261. 'variant': 'Variant',
  262. }
  263. _decimal_fields = ['price', 'total']
  264. _int_fields = ['id', 'quantity']
  265. @property
  266. def discount_rate(self):
  267. if self.subtotal == 0:
  268. return 0.0
  269. return float(-100 * self.total_discount / self.subtotal)
  270. @property
  271. def discount_rate_with_markup(self):
  272. total = self.subtotal + self.total_markup
  273. if total == 0:
  274. return 0.0
  275. return float(-100 * self.total_discount / total)
  276. @property
  277. def subtotal(self):
  278. return self.get('price') * self.get('quantity')
  279. class SpreeOrder(SpreeAdjustableMixin, SpreeRecord):
  280. _sub_records = {
  281. 'ship_address': 'Address',
  282. 'bill_address': 'Address',
  283. 'line_items': 'LineItem',
  284. 'shipments': 'Shipment',
  285. 'payments': 'Payment',
  286. 'adjustments': 'Adjustment',
  287. }
  288. _decimal_fields = ['total', 'tax_total', 'payment_total', 'item_total', 'included_tax_total', 'ship_total', 'adjustment_total', 'additional_tax_total']
  289. _int_fields = ['id', 'total_quantity']
  290. @property
  291. def is_paid_by_invoice(self):
  292. for payment in self.get('payments'):
  293. if payment.is_valid:
  294. if payment.is_invoice:
  295. # pay by check/invoice
  296. return True
  297. else:
  298. # credit card
  299. return False
  300. @property
  301. def is_completed(self):
  302. return self.get('completed_at') and self.get('state') == 'complete'
  303. @property
  304. def is_ready_to_ship(self):
  305. return self.get('shipment_state') in ['pending', 'ready', 'shipped']
  306. def get_url(self, page=''):
  307. return '{}/admin/orders/{}{}'.format(configuration.get('Spree', 'url'),
  308. self['number'],
  309. page)
  310. @property
  311. def payments_url(self):
  312. return self.get_url('/payments')
  313. @property
  314. def shipped_at(self):
  315. max_date = None
  316. for shipment in self.get('shipments'):
  317. shipped_at = shipment.get('shipped_at')
  318. if not shipped_at:
  319. return None
  320. elif not max_date or shipped_at > max_date:
  321. max_date = shipped_at
  322. return max_date
  323. @property
  324. def effective_date(self):
  325. return self.shipped_at or self.get('completed_at')
  326. @property
  327. def payment_due_date(self):
  328. return self.effective_date + timedelta(days=30)
  329. class SpreeProduct(SpreeRecord):
  330. _sub_records = {
  331. 'master': 'Variant',
  332. 'variants': 'Variant',
  333. }
  334. _datetime_fields = ['created_at', 'updated_at', 'available_on', 'discontinue_on']
  335. def __init__(self, client, classname, obj, readonly_link=False):
  336. try:
  337. obj['_sku'] = obj['master']['sku'] or obj['variants'][0]['sku']
  338. except IndexError:
  339. pass
  340. super(SpreeProduct, self).__init__(client, classname, obj, readonly_link=readonly_link)