/scylla_spree/records.py
Python | 428 lines | 390 code | 19 blank | 19 comment | 19 complexity | 31edc325f622e3f14b70ccdb03bb68a8 MD5 | raw file
- from datetime import timedelta
- from decimal import Decimal
- from scylla import records
- from scylla import convert
- from scylla import configuration
- from scylla import orientdb
- # recursion depth when initializing records
- _process_depth = 0
- # cache of records cleared after processing a response
- _record_cache = {}
- class SpreeRecord(records.Record):
- key_field = 'id'
- classname = 'SpreeRecord'
- _sub_records = {}
- _linked_records = {}
- _datetime_fields = ['created_at', 'updated_at', 'completed_at']
- _decimal_fields = []
- _int_fields = ['id']
- @classmethod
- def factory(cls, client, classname, obj, readonly_link=False):
- """Instantiates a SpreeRecord object using a subclass if one is available.
- Args:
- client: The spree api client that this record connects with
- classname: The name of the class in spree to append after Spree, which
- will be the OrientDB classname.
- obj: A dict or list that contains the data for this record(s),
- and its subrecords.
- """
- if obj is None:
- return None
- python_classname = 'Spree{0}'.format(classname)
- for subclass in cls.__subclasses__():
- if subclass.__name__ == python_classname:
- cls = subclass
- if isinstance(obj, records.Record):
- return obj
- elif isinstance(obj, dict):
- return cls.process_spree_obj(client, classname, obj, readonly_link)
- elif isinstance(obj, list):
- return [cls.process_spree_obj(client, classname, d, readonly_link) for d in obj]
- else:
- raise Exception('Subclass data must be a list or dict: '+str(obj))
- def __init__(self, client, classname, obj, readonly_link=False):
- super(SpreeRecord, self).__init__(obj)
- self.client = client
- self._readonly_link = readonly_link
- self.spree_module = client.name
- if classname:
- self.classname = self.spree_module + classname
- @classmethod
- def process_spree_obj(cls, client, classname, obj, readonly_link=False):
- global _process_depth
- if isinstance(obj, records.Record):
- # it's already converted
- return obj
- elif isinstance(obj, basestring) and obj[0] == '#':
- # this is just a string, so it's probably an RID because of a circular reference
- try:
- return _record_cache[obj]
- except KeyError:
- return obj
- # check record cache (creates circular reference instead of an infinite loop)
- if '@rid' in obj and obj['@rid'] in _record_cache:
- return _record_cache[obj['@rid']]
- # initialize the record
- rec = cls(client, classname, obj, readonly_link)
- # cache the record
- if '@rid' in obj:
- _record_cache[obj['@rid']] = rec
- # process the fields and initialize subrecords
- _process_depth += 1
- rec._process_fields()
- _process_depth -= 1
- # if we finished processing all records, clear our cache
- if _process_depth == 0:
- _record_cache.clear()
- return rec
- def _process_fields(self):
- #remove any invalid fields from the dict
- if "" in self.data:
- self.data.pop("", None)
- # create sub records (updates the record during save)
- for (field, classname) in self._sub_records.items():
- if field in self.data:
- self.data[field] = SpreeRecord.factory(self.client, classname, self.data[field])
- # create linked records
- for (field, classname) in self._linked_records.items():
- if field in self.data:
- self.data[field] = SpreeRecord.factory(self.client, classname, self.data[field], True)
- # process datetimes
- for field in self._datetime_fields:
- if field in self.data:
- self.data[field] = convert.parse_iso_date(self.data[field])
- for field in self._decimal_fields:
- if field in self.data:
- self.data[field] = Decimal(self[field])
- for field in self._int_fields:
- if field in self.data:
- self.data[field] = int(self[field])
- class SpreeAdjustableMixin(object):
- @property
- def tax_adjustments(self):
- return [a for a in self.get('adjustments') if a.is_tax]
- @property
- def discount_adjustments(self):
- return [a for a in self.get('adjustments') if a.is_discount]
- @property
- def markup_adjustments(self):
- return [a for a in self.get('adjustments') if a.is_markup]
- @property
- def total_tax(self):
- return sum(adjustment.get('amount') for adjustment in self.tax_adjustments) or Decimal()
- @property
- def total_discount(self):
- return sum(adjustment.get('amount') for adjustment in self.discount_adjustments) or Decimal()
- @property
- def total_markup(self):
- return sum(adjustment.get('amount') for adjustment in self.markup_adjustments) or Decimal()
- class SpreeCountry(SpreeRecord):
- def _process_fields(self):
- super(SpreeCountry, self)._process_fields()
- # states.state references a SpreeState
- if 'states' in self.data:
- for state_obj in self.data['states']:
- state_obj['state'] = SpreeRecord.factory(self.client, 'State', state_obj['state'], True)
- class SpreeAddress(SpreeRecord):
- _linked_records = {
- 'country': 'Country',
- 'state': 'State',
- }
- class SpreeUser(SpreeRecord):
- _sub_records = {
- 'ship_address': 'Address',
- 'bill_address': 'Address',
- }
- @property
- def full_name(self):
- bill_address = self.get('bill_address')
- if bill_address and bill_address.get('full_name'):
- return bill_address.get('full_name')
- else:
- return self.get('email')
- class SpreeAdjustment(SpreeRecord):
- _decimal_fields = ['amount']
- @property
- def is_tax(self):
- source_type = self.get('source_type')
- return self.get('eligible') and (source_type == 'Spree::TaxRate' or source_type == 'Spree::AvalaraTransaction')
- @property
- def is_shipping(self):
- return self.get('adjustable_type') == 'Spree::Shipment'
- @property
- def is_discount(self):
- return self.get('eligible') and not self.is_tax and self.get('amount') < 0.0
- @property
- def is_markup(self):
- return self.get('eligible') and not self.is_tax and self.get('amount') > 0.0
- class SpreeRefund(SpreeRecord):
- _decimal_fields = ['amount']
- _linked_records = {
- '_parent': 'Payment',
- }
- class SpreePayment(SpreeRecord):
- _decimal_fields = ['amount']
- _sub_records = {
- 'refunds': 'Refund',
- }
- _linked_records = {
- '_parent': 'Order',
- }
- @classmethod
- def capture_payments(cls, client, order_number, payments, from_rid=None):
- for payment in payments:
- if payment['state'] == 'checkout':
- cls.pay(client, order_number, payment['id'], from_rid=from_rid)
- @classmethod
- def pay(cls, client, order_number, number, from_rid=None):
- """Mark a payment as captured.
- """
- uri = "/orders/{0}/payments/{1}/capture".format(order_number, number)
- response = client.put(uri)
- # save updated shipment to orientdb
- payment = SpreeRecord.factory(client, 'Payment', response)
- payment.throw_at_orient()
- # link what updated this payment
- if from_rid:
- orientdb.create_edge(from_rid, payment.rid,
- 'Updated',
- content={'method':'PUT', 'uri':uri})
- return payment
- @property
- def is_invoice(self):
- return self.get('payment_method').get('name') == 'Invoice'
- @property
- def is_valid(self):
- return self.get('state') not in ['invalid', 'void', 'failed']
- @property
- def is_completed(self):
- return self.get('state') == 'completed'
- class SpreeShipment(SpreeAdjustableMixin, SpreeRecord):
- _sub_records = {
- 'adjustments': 'Adjustment',
- }
- _datetime_fields = ['created_at', 'updated_at', 'shipped_at']
- _decimal_fields = ['cost']
- @classmethod
- def shipReady(cls, client, shipments, tracking_number=None, shipped_at=None, from_rid=None):
- for shipment in shipments:
- if shipment['state'] == 'ready':
- shipment_number = shipment['number']
- if shipment['selected_shipping_rate']['shipping_method_id'] == 7:
- # digital shipment
- cls.ship(client, shipment_number, from_rid=from_rid)
- else:
- cls.ship(client, shipment_number, tracking_number, from_rid=from_rid)
- elif shipment['state'] == 'pending':
- raise Exception('Cannot ship a shipment in the "pending" state: {}'.format(shipment['number']))
- @classmethod
- def ship(cls, client, number, tracking_number=None, shipped_at=None, from_rid=None):
- """Mark a shipment as shipped.
- """
- params = {}
- if tracking_number:
- params['shipment[tracking]'] = tracking_number
- if shipped_at:
- params['shipment[shipped_at]'] = shipped_at
- uri = "/shipments/{0}/ship".format(number)
- response = client.put(uri, params)
- # save updated shipment to orientdb
- shipment = SpreeRecord.factory(client, 'Shipment', response)
- shipment.throw_at_orient()
- # link what updated this shipment
- if from_rid:
- orientdb.create_edge(from_rid, shipment.rid,
- 'Updated',
- content={'method':'PUT', 'uri':uri, 'params':params})
- return shipment
- @property
- def discount_rate(self):
- if self.get('cost') == 0:
- return 0.0
- return float(-100 * self.total_discount / self.get('cost'))
- class SpreeVariant(SpreeRecord):
- _sub_records = {
- #'images': 'Image',
- #'option_values': 'Option',
- }
- @property
- def full_name(self):
- name = self.get('name', '')
- options_text = self.get('options_text')
- if options_text:
- return u'{} ({})'.format(name, options_text)
- else:
- return name
- class SpreeLineItem(SpreeAdjustableMixin, SpreeRecord):
- _sub_records = {
- 'adjustments': 'Adjustment',
- }
- _linked_records = {
- 'variant': 'Variant',
- }
- _decimal_fields = ['price', 'total']
- _int_fields = ['id', 'quantity']
- @property
- def discount_rate(self):
- if self.subtotal == 0:
- return 0.0
- return float(-100 * self.total_discount / self.subtotal)
- @property
- def discount_rate_with_markup(self):
- total = self.subtotal + self.total_markup
- if total == 0:
- return 0.0
- return float(-100 * self.total_discount / total)
- @property
- def subtotal(self):
- return self.get('price') * self.get('quantity')
- class SpreeOrder(SpreeAdjustableMixin, SpreeRecord):
- _sub_records = {
- 'ship_address': 'Address',
- 'bill_address': 'Address',
- 'line_items': 'LineItem',
- 'shipments': 'Shipment',
- 'payments': 'Payment',
- 'adjustments': 'Adjustment',
- }
- _decimal_fields = ['total', 'tax_total', 'payment_total', 'item_total', 'included_tax_total', 'ship_total', 'adjustment_total', 'additional_tax_total']
- _int_fields = ['id', 'total_quantity']
- @property
- def is_paid_by_invoice(self):
- for payment in self.get('payments'):
- if payment.is_valid:
- if payment.is_invoice:
- # pay by check/invoice
- return True
- else:
- # credit card
- return False
- @property
- def is_completed(self):
- return self.get('completed_at') and self.get('state') == 'complete'
- @property
- def is_ready_to_ship(self):
- return self.get('shipment_state') in ['pending', 'ready', 'shipped']
- def get_url(self, page=''):
- return '{}/admin/orders/{}{}'.format(configuration.get('Spree', 'url'),
- self['number'],
- page)
- @property
- def payments_url(self):
- return self.get_url('/payments')
- @property
- def shipped_at(self):
- max_date = None
- for shipment in self.get('shipments'):
- shipped_at = shipment.get('shipped_at')
- if not shipped_at:
- return None
- elif not max_date or shipped_at > max_date:
- max_date = shipped_at
- return max_date
- @property
- def effective_date(self):
- return self.shipped_at or self.get('completed_at')
- @property
- def payment_due_date(self):
- return self.effective_date + timedelta(days=30)
- class SpreeProduct(SpreeRecord):
- _sub_records = {
- 'master': 'Variant',
- 'variants': 'Variant',
- }
- _datetime_fields = ['created_at', 'updated_at', 'available_on', 'discontinue_on']
- def __init__(self, client, classname, obj, readonly_link=False):
- try:
- obj['_sku'] = obj['master']['sku'] or obj['variants'][0]['sku']
- except IndexError:
- pass
- super(SpreeProduct, self).__init__(client, classname, obj, readonly_link=readonly_link)