diff --git a/rattail/django/rattail/admin.py b/rattail/django/rattail/admin.py index 621db96..a4fc19d 100644 --- a/rattail/django/rattail/admin.py +++ b/rattail/django/rattail/admin.py @@ -50,14 +50,27 @@ class ContactInfoInline(admin.TabularInline): extra = 0 +class PartyInline(admin.TabularInline): + """ + Base inline manager for all party models. + """ + + exclude = ('uuid', 'parent_type') + extra = 0 + + class PersonPhoneNumberInline(ContactInfoInline): model = PersonPhoneNumber class PersonEmailAddressInline(ContactInfoInline): model = PersonEmailAddress +class PersonPostalAddressInline(ContactInfoInline): + model = PersonPostalAddress + class PersonAdmin(ModelAdmin): - inlines = [PersonPhoneNumberInline, PersonEmailAddressInline] + inlines = [PersonPhoneNumberInline, PersonEmailAddressInline, + PersonPostalAddressInline] def get_formsets(self, request, obj=None): for inline in self.get_inline_instances(request): @@ -97,8 +110,12 @@ class StorePhoneNumberInline(ContactInfoInline): class StoreEmailAddressInline(ContactInfoInline): model = StoreEmailAddress +class StorePostalAddressInline(ContactInfoInline): + model = StorePostalAddress + class StoreAdmin(ModelAdmin): - inlines = [StorePhoneNumberInline, StoreEmailAddressInline] + inlines = [StorePhoneNumberInline, StoreEmailAddressInline, + StorePostalAddressInline] def get_formsets(self, request, obj=None): for inline in self.get_inline_instances(request): @@ -127,22 +144,21 @@ class VendorPhoneNumberInline(ContactInfoInline): class VendorEmailAddressInline(ContactInfoInline): model = VendorEmailAddress -class VendorContactInline(admin.TabularInline): - model = VendorContact - verbose_name = "Contact" - verbose_name_plural = "Contacts" - exclude = ('uuid',) - extra = 0 +class VendorPostalAddressInline(ContactInfoInline): + model = VendorPostalAddress + +class VendorPartyInline(PartyInline): + model = VendorParty class VendorAdmin(ModelAdmin): inlines = [VendorPhoneNumberInline, VendorEmailAddressInline, - VendorContactInline] + VendorPostalAddressInline, VendorPartyInline] def get_formsets(self, request, obj=None): for inline in self.get_inline_instances(request): if isinstance(inline, ContactInfoInline) and obj is None: continue - if isinstance(inline, VendorContactInline) and obj is None: + if isinstance(inline, PartyInline) and obj is None: continue yield inline.get_formset(request, obj) @@ -188,22 +204,7 @@ class ProductAdmin(ModelAdmin): admin.site.register(Product, ProductAdmin) -class EmployeePhoneNumberInline(ContactInfoInline): - model = EmployeePhoneNumber - -class EmployeeEmailAddressInline(ContactInfoInline): - model = EmployeeEmailAddress - -class EmployeeAdmin(ModelAdmin): - inlines = [EmployeePhoneNumberInline, EmployeeEmailAddressInline] - - def get_formsets(self, request, obj=None): - for inline in self.get_inline_instances(request): - if isinstance(inline, ContactInfoInline) and obj is None: - continue - yield inline.get_formset(request, obj) - -admin.site.register(Employee, EmployeeAdmin) +admin.site.register(Employee, ModelAdmin) admin.site.register(CustomerGroup, ModelAdmin) @@ -215,6 +216,9 @@ class CustomerPhoneNumberInline(ContactInfoInline): class CustomerEmailAddressInline(ContactInfoInline): model = CustomerEmailAddress +class CustomerPostalAddressInline(ContactInfoInline): + model = CustomerPostalAddress + class CustomerGroupAssignmentInline(admin.TabularInline): model = CustomerGroupAssignment verbose_name = "Group Assignment" @@ -222,9 +226,13 @@ class CustomerGroupAssignmentInline(admin.TabularInline): exclude = ('uuid',) extra = 0 +class CustomerPartyInline(PartyInline): + model = CustomerParty + class CustomerAdmin(ModelAdmin): inlines = [CustomerPhoneNumberInline, CustomerEmailAddressInline, - CustomerGroupAssignmentInline] + CustomerPostalAddressInline, CustomerGroupAssignmentInline, + CustomerPartyInline] def get_formsets(self, request, obj=None): for inline in self.get_inline_instances(request): @@ -232,6 +240,8 @@ class CustomerAdmin(ModelAdmin): continue if isinstance(inline, CustomerGroupAssignmentInline) and obj is None: continue + if isinstance(inline, PartyInline) and obj is None: + continue yield inline.get_formset(request, obj) admin.site.register(Customer, CustomerAdmin) diff --git a/rattail/django/rattail/migrations/0001_initial.py b/rattail/django/rattail/migrations/0001_initial.py index f04bdd0..27b6423 100644 --- a/rattail/django/rattail/migrations/0001_initial.py +++ b/rattail/django/rattail/migrations/0001_initial.py @@ -12,8 +12,11 @@ class Migration(SchemaMigration): db.create_table('rattail_people', ( ('uuid', self.gf('rattail.django.rattail.models.core.UUIDField')(max_length=32, primary_key=True)), ('first_name', self.gf('django.db.models.fields.CharField')(max_length=50, null=True, blank=True)), + ('middle_name', self.gf('django.db.models.fields.CharField')(max_length=50, null=True, blank=True)), ('last_name', self.gf('django.db.models.fields.CharField')(max_length=50, null=True, blank=True)), ('display_name', self.gf('django.db.models.fields.CharField')(max_length=100, null=True, blank=True)), + ('driver_license_state', self.gf('django.db.models.fields.CharField')(max_length=4, null=True, blank=True)), + ('driver_license_number', self.gf('django.db.models.fields.CharField')(max_length=20, null=True, blank=True)), )) db.send_create_signal('rattail', ['Person']) @@ -22,7 +25,7 @@ class Migration(SchemaMigration): ('uuid', self.gf('rattail.django.rattail.models.core.UUIDField')(max_length=32, primary_key=True)), ('parent_type', self.gf('django.db.models.fields.CharField')(max_length=20)), ('parent_uuid', self.gf('django.db.models.fields.CharField')(max_length=32)), - ('preference', self.gf('django.db.models.fields.IntegerField')()), + ('preference', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)), ('type', self.gf('django.db.models.fields.CharField')(max_length=15, null=True, blank=True)), ('number', self.gf('django.db.models.fields.CharField')(max_length=20)), )) @@ -33,12 +36,38 @@ class Migration(SchemaMigration): ('uuid', self.gf('rattail.django.rattail.models.core.UUIDField')(max_length=32, primary_key=True)), ('parent_type', self.gf('django.db.models.fields.CharField')(max_length=20)), ('parent_uuid', self.gf('django.db.models.fields.CharField')(max_length=32)), - ('preference', self.gf('django.db.models.fields.IntegerField')()), + ('preference', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)), ('type', self.gf('django.db.models.fields.CharField')(max_length=15, null=True, blank=True)), ('address', self.gf('django.db.models.fields.CharField')(max_length=255)), )) db.send_create_signal('rattail', ['EmailAddress']) + # Adding model 'PostalAddress' + db.create_table('rattail_postal_addresses', ( + ('uuid', self.gf('rattail.django.rattail.models.core.UUIDField')(max_length=32, primary_key=True)), + ('parent_type', self.gf('django.db.models.fields.CharField')(max_length=20)), + ('parent_uuid', self.gf('django.db.models.fields.CharField')(max_length=32)), + ('preference', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)), + ('type', self.gf('django.db.models.fields.CharField')(max_length=15, null=True, blank=True)), + ('street', self.gf('django.db.models.fields.CharField')(max_length=100, null=True, blank=True)), + ('street2', self.gf('django.db.models.fields.CharField')(max_length=100, null=True, blank=True)), + ('city', self.gf('django.db.models.fields.CharField')(max_length=100, null=True, blank=True)), + ('state', self.gf('django.db.models.fields.CharField')(max_length=2, null=True, blank=True)), + ('zipcode', self.gf('django.db.models.fields.CharField')(max_length=10, null=True, blank=True)), + )) + db.send_create_signal('rattail', ['PostalAddress']) + + # Adding model 'Party' + db.create_table('rattail_parties', ( + ('uuid', self.gf('rattail.django.rattail.models.core.UUIDField')(max_length=32, primary_key=True)), + ('parent_type', self.gf('django.db.models.fields.CharField')(max_length=20)), + ('parent_uuid', self.gf('django.db.models.fields.CharField')(max_length=32)), + ('person', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['rattail.Person'], db_column='person_uuid')), + ('type', self.gf('django.db.models.fields.CharField')(max_length=25, null=True, blank=True)), + ('preference', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)), + )) + db.send_create_signal('rattail', ['Party']) + # Adding model 'Role' db.create_table('rattail_roles', ( ('uuid', self.gf('rattail.django.rattail.models.core.UUIDField')(max_length=32, primary_key=True)), @@ -114,15 +143,6 @@ class Migration(SchemaMigration): )) db.send_create_signal('rattail', ['Vendor']) - # Adding model 'VendorContact' - db.create_table('rattail_vendor_contacts', ( - ('uuid', self.gf('rattail.django.rattail.models.core.UUIDField')(max_length=32, primary_key=True)), - ('vendor', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['rattail.Vendor'], db_column='vendor_uuid')), - ('person', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['rattail.Person'], db_column='person_uuid')), - ('preference', self.gf('django.db.models.fields.IntegerField')()), - )) - db.send_create_signal('rattail', ['VendorContact']) - # Adding model 'Product' db.create_table('rattail_products', ( ('uuid', self.gf('rattail.django.rattail.models.core.UUIDField')(max_length=32, primary_key=True)), @@ -174,9 +194,9 @@ class Migration(SchemaMigration): # Adding model 'Employee' db.create_table('rattail_employees', ( ('uuid', self.gf('rattail.django.rattail.models.core.UUIDField')(max_length=32, primary_key=True)), - ('id', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)), + ('id', self.gf('django.db.models.fields.CharField')(max_length=20, null=True, blank=True)), ('person', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['rattail.Person'], db_column='person_uuid')), - ('status', self.gf('django.db.models.fields.IntegerField')()), + ('status', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)), ('display_name', self.gf('django.db.models.fields.CharField')(max_length=100, null=True, blank=True)), )) db.send_create_signal('rattail', ['Employee']) @@ -190,15 +210,6 @@ class Migration(SchemaMigration): )) db.send_create_signal('rattail', ['Customer']) - # Adding model 'CustomerPerson' - db.create_table('rattail_customers_people', ( - ('uuid', self.gf('rattail.django.rattail.models.core.UUIDField')(max_length=32, primary_key=True)), - ('customer', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['rattail.Customer'], db_column='customer_uuid')), - ('person', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['rattail.Person'], db_column='person_uuid')), - ('ordinal', self.gf('django.db.models.fields.IntegerField')()), - )) - db.send_create_signal('rattail', ['CustomerPerson']) - # Adding model 'CustomerGroup' db.create_table('rattail_customer_groups', ( ('uuid', self.gf('rattail.django.rattail.models.core.UUIDField')(max_length=32, primary_key=True)), @@ -240,9 +251,6 @@ class Migration(SchemaMigration): # Deleting model 'CustomerGroup' db.delete_table('rattail_customer_groups') - # Deleting model 'CustomerPerson' - db.delete_table('rattail_customers_people') - # Deleting model 'Customer' db.delete_table('rattail_customers') @@ -258,9 +266,6 @@ class Migration(SchemaMigration): # Deleting model 'Product' db.delete_table('rattail_products') - # Deleting model 'VendorContact' - db.delete_table('rattail_vendor_contacts') - # Deleting model 'Vendor' db.delete_table('rattail_vendors') @@ -288,6 +293,12 @@ class Migration(SchemaMigration): # Deleting model 'Role' db.delete_table('rattail_roles') + # Deleting model 'Party' + db.delete_table('rattail_parties') + + # Deleting model 'PostalAddress' + db.delete_table('rattail_postal_addresses') + # Deleting model 'EmailAddress' db.delete_table('rattail_email_addresses') @@ -302,7 +313,10 @@ class Migration(SchemaMigration): 'rattail.person': { 'Meta': {'object_name': 'Person', 'db_table': "'rattail_people'"}, 'display_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'driver_license_state': ('django.db.models.fields.CharField', [], {'max_length': '4', 'null': 'True', 'blank': 'True'}), + 'driver_license_number': ('django.db.models.fields.CharField', [], {'max_length': '20', 'null': 'True', 'blank': 'True'}), 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'middle_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), 'uuid': ('rattail.django.rattail.models.core.UUIDField', [], {'max_length': '32', 'primary_key': 'True'}) }, @@ -311,7 +325,7 @@ class Migration(SchemaMigration): 'number': ('django.db.models.fields.CharField', [], {'max_length': '20'}), 'parent_type': ('django.db.models.fields.CharField', [], {'max_length': '20'}), 'parent_uuid': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - 'preference': ('django.db.models.fields.IntegerField', [], {}), + 'preference': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 'type': ('django.db.models.fields.CharField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), 'uuid': ('rattail.django.rattail.models.core.UUIDField', [], {'max_length': '32', 'primary_key': 'True'}) }, @@ -320,10 +334,32 @@ class Migration(SchemaMigration): 'address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 'parent_type': ('django.db.models.fields.CharField', [], {'max_length': '20'}), 'parent_uuid': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - 'preference': ('django.db.models.fields.IntegerField', [], {}), + 'preference': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 'type': ('django.db.models.fields.CharField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), 'uuid': ('rattail.django.rattail.models.core.UUIDField', [], {'max_length': '32', 'primary_key': 'True'}) }, + 'rattail.postaladdress': { + 'Meta': {'object_name': 'PostalAddress', 'db_table': "'rattail_postal_addresses'"}, + 'city': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'parent_type': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'parent_uuid': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'preference': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.CharField', [], {'max_length': '2', 'null': 'True', 'blank': 'True'}), + 'street': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'street2': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), + 'uuid': ('rattail.django.rattail.models.core.UUIDField', [], {'max_length': '32', 'primary_key': 'True'}), + 'zipcode': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}) + }, + 'rattail.party': { + 'Meta': {'object_name': 'Party', 'db_table': "'rattail_parties'"}, + 'parent_type': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'parent_uuid': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['rattail.Person']", 'db_column': "'person_uuid'"}), + 'preference': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '25', 'null': 'True', 'blank': 'True'}), + 'uuid': ('rattail.django.rattail.models.core.UUIDField', [], {'max_length': '32', 'primary_key': 'True'}), + }, 'rattail.role': { 'Meta': {'object_name': 'Role', 'db_table': "'rattail_roles'"}, 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '25'}), @@ -381,13 +417,6 @@ class Migration(SchemaMigration): 'special_discount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '5', 'decimal_places': '3', 'blank': 'True'}), 'uuid': ('rattail.django.rattail.models.core.UUIDField', [], {'max_length': '32', 'primary_key': 'True'}) }, - 'rattail.vendorcontact': { - 'Meta': {'object_name': 'VendorContact', 'db_table': "'rattail_vendor_contacts'"}, - 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['rattail.Person']", 'db_column': "'person_uuid'"}), - 'preference': ('django.db.models.fields.IntegerField', [], {}), - 'uuid': ('rattail.django.rattail.models.core.UUIDField', [], {'max_length': '32', 'primary_key': 'True'}), - 'vendor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['rattail.Vendor']", 'db_column': "'vendor_uuid'"}) - }, 'rattail.product': { 'Meta': {'object_name': 'Product', 'db_table': "'rattail_products'"}, 'brand': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['rattail.Brand']", 'null': 'True', 'db_column': "'brand_uuid'", 'blank': 'True'}), @@ -433,9 +462,9 @@ class Migration(SchemaMigration): 'rattail.employee': { 'Meta': {'object_name': 'Employee', 'db_table': "'rattail_employees'"}, 'display_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), - 'id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.CharField', [], {'max_length': '20', 'null': 'True', 'blank': 'True'}), 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['rattail.Person']", 'db_column': "'person_uuid'"}), - 'status': ('django.db.models.fields.IntegerField', [], {}), + 'status': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 'uuid': ('rattail.django.rattail.models.core.UUIDField', [], {'max_length': '32', 'primary_key': 'True'}) }, 'rattail.customer': { @@ -445,13 +474,6 @@ class Migration(SchemaMigration): 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), 'uuid': ('rattail.django.rattail.models.core.UUIDField', [], {'max_length': '32', 'primary_key': 'True'}) }, - 'rattail.customerperson': { - 'Meta': {'object_name': 'CustomerPerson', 'db_table': "'rattail_customers_people'"}, - 'customer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['rattail.Customer']", 'db_column': "'customer_uuid'"}), - 'ordinal': ('django.db.models.fields.IntegerField', [], {}), - 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['rattail.Person']", 'db_column': "'person_uuid'"}), - 'uuid': ('rattail.django.rattail.models.core.UUIDField', [], {'max_length': '32', 'primary_key': 'True'}) - }, 'rattail.customergroup': { 'Meta': {'object_name': 'CustomerGroup', 'db_table': "'rattail_customer_groups'"}, 'id': ('django.db.models.fields.CharField', [], {'max_length': '20', 'null': 'True', 'blank': 'True'}), diff --git a/rattail/django/rattail/models/contact.py b/rattail/django/rattail/models/contact.py index 11bf429..dfb53bf 100644 --- a/rattail/django/rattail/models/contact.py +++ b/rattail/django/rattail/models/contact.py @@ -29,12 +29,14 @@ from __future__ import absolute_import from django.db import models +from django.core.exceptions import ObjectDoesNotExist from rattail.django.rattail.models import Model, uuid_field -__all__ = ['PhoneNumber', 'EmailAddress', - 'Person', 'PersonPhoneNumber', 'PersonEmailAddress'] +__all__ = ['Contact', 'PhoneNumber', 'EmailAddress', 'PostalAddress', + 'Person', 'PersonPhoneNumber', 'PersonEmailAddress', 'PersonPostalAddress', + 'Party'] class ContactInfoManager(models.Manager): @@ -57,7 +59,7 @@ class ContactInfo(Model): uuid = uuid_field() parent_type = models.CharField(max_length=20) - preference = models.IntegerField() + preference = models.IntegerField(blank=True, null=True) type = models.CharField(max_length=15, blank=True, null=True) @@ -101,20 +103,223 @@ class EmailAddress(ContactInfo): return unicode(self.address) -class Person(Model): +class PostalAddress(ContactInfo): + """ + Represents a postal (mailing) address associated with a contactable entity. + """ + + class Meta(ContactInfo.Meta): + abstract = True + db_table = ContactInfo.prefix('postal_addresses') + verbose_name = "Postal Address" + verbose_name_plural = "Postal Addresses" + + street = models.CharField(max_length=100, blank=True, null=True) + street2 = models.CharField(max_length=100, blank=True, null=True) + city = models.CharField(max_length=100, blank=True, null=True) + state = models.CharField(max_length=2, blank=True, null=True) + zipcode = models.CharField(max_length=10, blank=True, null=True) + + def __repr__(self): + return "<%s: %s>" % (self.__class__.__name__, self.street) + + def __unicode__(self): + return unicode(self.street) + + +class Contact(Model): + """ + Base class for models which may be associated with contact info data. + + This class exists only to define convenience methods and properties on the + derived class. + """ + + class Meta(Model.Meta): + abstract = True + + def __init__(self, *args, **kwargs): + super(Contact, self).__init__(*args, **kwargs) + self._phones = {} + self._emails = {} + self._addresses = {} + self._preferred = {} + + def _translate_info_class(self, class_): + from rattail.django.rattail import models + class_name = self.__class__.__name__ + class_.__name__ + if hasattr(models, class_name): + class_ = getattr(models, class_name) + return class_ + + def _filter_queryset(self, qs): + parent = self.__class__.__name__.lower() + kwargs = {parent: self} + return qs.filter(**kwargs) + + def _assign_parent(self, info): + parent = self.__class__.__name__.lower() + setattr(info, parent, self) + + def _get_info_dict(self, class_): + if issubclass(class_, PhoneNumber): + return self._phones + elif issubclass(class_, EmailAddress): + return self._emails + elif issubclass(class_, PostalAddress): + return self._addresses + raise ValueError("Expected a subclass of ContactInfo; got %s" % class_.__name__) + + def _get_info_key(self, class_): + if issubclass(class_, PhoneNumber): + return 'phone' + elif issubclass(class_, EmailAddress): + return 'email' + elif issubclass(class_, PostalAddress): + return 'address' + raise ValueError("Expected a subclass of ContactInfo; got %s" % class_.__name__) + + def _get_info(self, class_, type, info_dict=None): + class_ = self._translate_info_class(class_) + if info_dict is None: + info_dict = self._get_info_dict(class_) + if type not in info_dict: + qs = class_.objects + qs = self._filter_queryset(qs) + qs = qs.filter(type=type) + try: + info = qs.get() + except ObjectDoesNotExist: + info = None + info_dict[type] = info + return info_dict[type] + + def get_phone(self, type): + return self._get_info(PhoneNumber, type) + + def get_email(self, type): + return self._get_info(EmailAddress, type) + + def get_address(self, type): + return self._get_info(PostalAddress, type) + + def _update_info(self, class_, type, data): + class_ = self._translate_info_class(class_) + info_dict = self._get_info_dict(class_) + info = self._get_info(class_, type, info_dict) + + # Check data for values; if none exist then we'll delete the info. + values = False + for value in data.itervalues(): + if value: + values = True + break + + if values: + if not info: + info = class_() + self._assign_parent(info) + info.type = type + info_dict[type] = info + for key, value in data.iteritems(): + setattr(info, key, value) + info.save() + return info + + if info: + info.delete() + info_dict[type] = None + return None + + def update_phone(self, type, data): + return self._update_info(PhoneNumber, type, data) + + def update_email(self, type, data): + return self._update_info(EmailAddress, type, data) + + def update_address(self, type, data): + return self._update_info(PostalAddress, type, data) + + def _get_preferred_info(self, class_): + class_ = self._translate_info_class(class_) + info_key = self._get_info_key(class_) + if info_key not in self._preferred: + qs = class_.objects + qs = self._filter_queryset(qs) + qs = qs.filter(preference=1) + try: + info = qs.get() + except ObjectDoesNotExist: + info = None + self._preferred[info_key] = info + return self._preferred[info_key] + + def get_preferred_phone(self): + return self._get_preferred_info(PhoneNumber) + + def get_preferred_email(self): + return self._get_preferred_info(EmailAddress) + + def get_preferred_address(self): + return self._get_preferred_info(PostalAddress) + + @property + def phone(self): + return self.get_preferred_phone() + + @property + def email(self): + return self.get_preferred_email() + + @property + def address(self): + return self.get_preferred_address() + + def _set_preferred_info(self, class_, info): + class_ = self._translate_info_class(class_) + info_dict = self._get_info_dict(class_) + info_key = self._get_info_key(class_) + + if info.preference != 1: + info.preference = 1 + info.save() + self._preferred[info_key] = info + + preference = 2 + for other_info in sorted(info_dict.values(), + key=lambda x: x.preference if x else 999): + if other_info and other_info is not info: + other_info.preference = preference + other_info.save() + preference += 1 + + def set_preferred_phone(self, phone): + self._set_preferred_info(PhoneNumber, phone) + + def set_preferred_email(self, email): + self._set_preferred_info(EmailAddress, email) + + def set_preferred_address(self, address): + self._set_preferred_info(PostalAddress, address) + + +class Person(Contact): """ Represents a real, living and breathing person. (Or, at least was previously living and breathing, in the case of the deceased.) """ - class Meta(Model.Meta): - db_table = Model.prefix('people') + class Meta(Contact.Meta): + db_table = Contact.prefix('people') verbose_name_plural = "People" uuid = uuid_field() first_name = models.CharField(max_length=50, blank=True, null=True) + middle_name = models.CharField(max_length=50, blank=True, null=True) last_name = models.CharField(max_length=50, blank=True, null=True) display_name = models.CharField(max_length=100, blank=True, null=True, verbose_name="Display Name") + driver_license_state = models.CharField(max_length=4, blank=True, null=True) + driver_license_number = models.CharField(max_length=20, blank=True, null=True) def __repr__(self): return "" % self.display_name @@ -122,16 +327,51 @@ class Person(Model): def __unicode__(self): return unicode(self.display_name or '') + def update_display_name(self): + orig_display_name = self.display_name + if self.first_name and self.last_name: + self.display_name = self.first_name + ' ' + self.last_name + elif self.first_name: + self.display_name = self.first_name + elif self.last_name: + self.display_name = self.last_name + return self.display_name != orig_display_name + def save(self, *args, **kwargs): if not self.display_name: - if self.first_name and self.last_name: - self.display_name = self.first_name + ' ' + self.last_name - elif self.first_name: - self.display_name = self.first_name - elif self.last_name: - self.display_name = self.last_name + self.update_display_name() super(Person, self).save(*args, **kwargs) + _employee = None + + @property + def employee(self): + if self._employee is None: + from rattail.django.rattail.models import Employee + qs = Employee.objects + qs = qs.filter(person=self) + try: + self._employee = qs.get() + except ObjectDoesNotExist: + pass + return self._employee + + def make_employee(self, **kwargs): + if self.employee: + return + + from rattail.django.rattail.models import Employee + employee = Employee(**kwargs) + employee.person = self + employee.save() + self._employee = employee + return employee + + def unmake_employee(self): + if self.employee: + self.employee.delete() + self._employee = None + class PersonContactInfo(models.Model): """ @@ -141,7 +381,7 @@ class PersonContactInfo(models.Model): class Meta: abstract = True - store = models.ForeignKey(Person, db_column='parent_uuid') + person = models.ForeignKey(Person, db_column='parent_uuid') objects = ContactInfoManager('Person') def save(self, *args, **kwargs): @@ -159,3 +399,44 @@ class PersonEmailAddress(EmailAddress, PersonContactInfo): """ Represents an email address associated with a :class:`Person`. """ + + +class PersonPostalAddress(PostalAddress, PersonContactInfo): + """ + Represents a postal (mailing) address associated with a :class:`Person`. + """ + + +class PartyManager(models.Manager): + + def __init__(self, parent_type): + super(PartyManager, self).__init__() + self.parent_type = parent_type + + def get_query_set(self): + qs = super(PartyManager, self).get_query_set() + qs = qs.filter(parent_type=self.parent_type) + qs = qs.order_by('preference') + return qs + + +class Party(Model): + """ + Base class for "party" relationships, e.g. sales contacts for vendors and + authorized shoppers for customers. + """ + + class Meta(Model.Meta): + abstract = True + db_table = Model.prefix('parties') + verbose_name = "Party" + verbose_name_plural = "Parties" + + uuid = uuid_field() + parent_type = models.CharField(max_length=20) + person = models.ForeignKey(Person, db_column='person_uuid') + type = models.CharField(max_length=25, blank=True, null=True) + preference = models.IntegerField(blank=True, null=True) + + def __unicode__(self): + return u'%s (%s)' % (self.person, self.type) diff --git a/rattail/django/rattail/models/core.py b/rattail/django/rattail/models/core.py index 0c1755a..d4f6afb 100644 --- a/rattail/django/rattail/models/core.py +++ b/rattail/django/rattail/models/core.py @@ -32,7 +32,7 @@ from django.conf import settings import edbob -__all__ = ['Model', 'uuid_field'] +__all__ = ['Model', 'uuid_field', 'ProxyModel', 'PersonProxyModel'] class Model(models.Model): @@ -61,6 +61,80 @@ class Model(models.Model): return table_name +class ProxyModel(models.Model): + """ + Base class for models which intend to "proxy" certain attributes to an + underlying Rattail model. This is designed for use in apps which use + Rattail as their primary schema but need to extend it in some way. + """ + + class Meta: + abstract = True + + _proxy_attribute_map = {} + + def __getattr__(self, name): + for proxy, attrs in self.__class__._proxy_attribute_map.iteritems(): + for attr in attrs: + if isinstance(attr, tuple): + convenient, actual = attr + else: + convenient = actual = attr + if convenient == name: + return getattr(getattr(self, proxy), actual) + return super(ProxyModel, self).__getattr__(name) + + def __setattr__(self, name, value): + for proxy, attrs in self.__class__._proxy_attribute_map.iteritems(): + for attr in attrs: + if isinstance(attr, tuple): + convenient, actual = attr + else: + convenient = actual = attr + if convenient == name: + print 'setting %s.%s.%s (via %s) to %s' % ( + self.__class__.__name__, proxy, actual, convenient, repr(value)) + return setattr(getattr(self, proxy), actual, value) + return super(ProxyModel, self).__setattr__(name, value) + + +class PersonProxyModel(ProxyModel): + """ + Proxy model specific to :class:`rattail.django.rattail.models.Person`. + """ + + class Meta(ProxyModel.Meta): + abstract = True + + _proxy_person = 'person' + + def __init__(self, *args, **kwargs): + self._proxy_attribute_map[self._proxy_person] = [ + 'first_name', + 'middle_name', + 'last_name', + 'driver_license_number', + 'driver_license_state', + 'address', + 'update_address', + 'set_preferred_address', + 'phone', + 'get_phone', + 'update_phone', + 'set_preferred_phone', + 'email', + 'update_email', + 'set_preferred_email', + 'make_employee', + 'unmake_employee', + ] + super(PersonProxyModel, self).__init__(*args, **kwargs) + + def save(self, **kwargs): + getattr(self, self._proxy_person).save(**kwargs) + super(PersonProxyModel, self).save(**kwargs) + + class UUIDField(models.CharField): """ "Custom" type for UUID fields. This is actually only necessary so that the diff --git a/rattail/django/rattail/models/rattail.py b/rattail/django/rattail/models/rattail.py index 76dd036..f0974ac 100644 --- a/rattail/django/rattail/models/rattail.py +++ b/rattail/django/rattail/models/rattail.py @@ -29,6 +29,7 @@ from __future__ import absolute_import from django.db import models +from django.core.exceptions import ObjectDoesNotExist from edbob.db.extensions.contact.enum import EMAIL_PREFERENCE @@ -38,17 +39,18 @@ from rattail.db.extension.enum import EMPLOYEE_STATUS from rattail.django.rattail.models import Model, uuid_field from rattail.django.rattail.models.contact import ( - ContactInfoManager, PhoneNumber, EmailAddress) + ContactInfoManager, PhoneNumber, EmailAddress, PostalAddress, + PartyManager, Party, Person) from rattail.django.util import enum_to_choices -__all__ = ['Store', 'StorePhoneNumber', 'StoreEmailAddress', +__all__ = ['Store', 'StorePhoneNumber', 'StoreEmailAddress', 'StorePostalAddress', 'Department', 'Subdepartment', 'Category', 'Brand', - 'Vendor', 'VendorPhoneNumber', 'VendorEmailAddress', 'VendorContact', + 'Vendor', 'VendorPhoneNumber', 'VendorEmailAddress', 'VendorPostalAddress', 'VendorParty', 'Product', 'ProductPrice', 'ProductCost', - 'Employee', 'EmployeePhoneNumber', 'EmployeeEmailAddress', - 'Customer', 'CustomerPhoneNumber', 'CustomerEmailAddress', 'CustomerPerson', - 'CustomerGroup', 'CustomerGroupAssignment', + 'Employee', + 'Customer', 'CustomerPhoneNumber', 'CustomerEmailAddress', 'CustomerPostalAddress', + 'CustomerGroup', 'CustomerGroupAssignment', 'CustomerParty', 'LabelProfile'] @@ -120,6 +122,12 @@ class StoreEmailAddress(EmailAddress, StoreContactInfo): """ +class StorePostalAddress(PostalAddress, StoreContactInfo): + """ + Represents a postal (mailing) address associated with a :class:`Store`. + """ + + class Department(Model): """ Represents an organizational department. @@ -251,26 +259,27 @@ class VendorEmailAddress(EmailAddress, VendorContactInfo): """ -class VendorContact(Model): +class VendorPostalAddress(PostalAddress, VendorContactInfo): """ - Represents a point of contact (e.g. salesperson) for a vendor, from the - retailer's perspective. + Represents a postal (mailing) address associated with a :class:`Vendor`. + """ + + +class VendorParty(Party): + """ + Represents a "party" (e.g. sales contact) for a vendor, from the retailer's + perspective. """ - class Meta(Model.Meta): - db_table = Model.prefix('vendor_contacts') - verbose_name = "Vendor Contact" - - uuid = uuid_field() - vendor = models.ForeignKey(Vendor, db_column='vendor_uuid') - person = models.ForeignKey('Person', db_column='person_uuid') - preference = models.IntegerField() + vendor = models.ForeignKey(Vendor, db_column='parent_uuid') + objects = PartyManager('Vendor') def __repr__(self): - return "" % (self.vendor, self.person) + return "" % (self.vendor, self.person) - def __unicode__(self): - return unicode(self.person) + def save(self, *args, **kwargs): + self.parent_type = 'Vendor' + super(VendorParty, self).save(*args, **kwargs) class Product(Model): @@ -378,9 +387,9 @@ class Employee(Model): EMPLOYEE_STATUS_CHOICES = enum_to_choices(EMPLOYEE_STATUS) uuid = uuid_field() - id = models.IntegerField(blank=True, null=True, verbose_name="ID") - person = models.ForeignKey('Person', db_column='person_uuid') - status = models.IntegerField(choices=EMPLOYEE_STATUS_CHOICES) + id = models.CharField(max_length=20, blank=True, null=True, verbose_name="ID") + person = models.OneToOneField(Person, db_column='person_uuid', related_name='+') + status = models.IntegerField(choices=EMPLOYEE_STATUS_CHOICES, blank=True, null=True) display_name = models.CharField(max_length=100, blank=True, null=True, verbose_name="Display Name") def __repr__(self): @@ -402,34 +411,6 @@ class Employee(Model): super(Employee, self).save(*args, **kwargs) -class EmployeeContactInfo(models.Model): - """ - Base class for employee contact models (phone number, etc.). - """ - - class Meta: - abstract = True - - employee = models.ForeignKey(Employee, db_column='parent_uuid') - objects = ContactInfoManager('Employee') - - def save(self, *args, **kwargs): - self.parent_type = 'Employee' - super(EmployeeContactInfo, self).save(*args, **kwargs) - - -class EmployeePhoneNumber(PhoneNumber, EmployeeContactInfo): - """ - Represents a phone (or fax) number associated with a :class:`Employee`. - """ - - -class EmployeeEmailAddress(EmailAddress, EmployeeContactInfo): - """ - Represents an email address associated with a :class:`Employee`. - """ - - class Customer(Model): """ Represents a customer account. Customer accounts may consist of more than @@ -453,6 +434,34 @@ class Customer(Model): def __unicode__(self): return unicode(self.name or '') + _primary_party = None + + @property + def primary_party(self): + if self._primary_party is None: + qs = CustomerParty.objects + qs = qs.filter(customer=self) + qs = qs.filter(preference=1) + try: + self._primary_party = qs.get() + except ObjectDoesNotExist: + pass + return self._primary_party + + def update_name(self): + orig_name = self.name + if self.primary_party: + person = self.primary_party.person + if not person.display_name: + person.update_display_name() + self.name = person.display_name + return self.name != orig_name + + def save(self, *args, **kwargs): + if not self.name: + self.update_name() + super(Customer, self).save(*args, **kwargs) + class CustomerContactInfo(models.Model): """ @@ -482,26 +491,27 @@ class CustomerEmailAddress(EmailAddress, CustomerContactInfo): """ -class CustomerPerson(Model): +class CustomerPostalAddress(PostalAddress, CustomerContactInfo): """ - Represents the association between a :class:`Person` and a customer account - (:class:`Customer`). + Represents a postal (mailing) address associated with a :class:`Customer`. + """ + + +class CustomerParty(Party): + """ + Represents a "party" (e.g. authorized shopper) for a customer, from the + retailer's perspective. """ - class Meta(Model.Meta): - db_table = Model.prefix('customers_people') - verbose_name = "Customer Person" - - uuid = uuid_field() - customer = models.ForeignKey(Customer, db_column='customer_uuid') - person = models.ForeignKey('Person', db_column='person_uuid') - ordinal = models.IntegerField() + customer = models.ForeignKey(Customer, db_column='parent_uuid') + objects = PartyManager('Customer') def __repr__(self): - return "" % self.person + return "" % (self.customer, self.person) - def __unicode__(self): - return unicode(self.person or '') + def save(self, *args, **kwargs): + self.parent_type = 'Customer' + super(CustomerParty, self).save(*args, **kwargs) class CustomerGroup(Model): diff --git a/rattail/django/rattail/urls.py b/rattail/django/rattail/urls.py new file mode 100644 index 0000000..cd67578 --- /dev/null +++ b/rattail/django/rattail/urls.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2012 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ + +""" +``rattail.django.rattail.urls`` -- URL Configuration +""" + + +from django.conf.urls import patterns, url + +from rattail.django.rattail import views + + +urlpatterns = patterns( + '', + + url(r'^zipcode/$', + views.get_zipcode_data, + name='rattail_zipcode'), + + ) diff --git a/rattail/django/rattail/views.py b/rattail/django/rattail/views.py new file mode 100644 index 0000000..b8398c2 --- /dev/null +++ b/rattail/django/rattail/views.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2012 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ + +""" +``rattail.django.rattail.views`` -- Views +""" + +from django.http import HttpResponse +from django.utils import simplejson as json + +from pyzipcode import ZipCodeDatabase + + +zcdb = ZipCodeDatabase() + + +def get_zipcode_data(request): + """ + Returns everything knowable about a given zipcode, as a JSON string. + Returns an empty string if the zipcode is not found in the database. + """ + + zipcode = request.GET.get('zipcode') + zipcode = zcdb.get(zipcode) + if not zipcode: + return HttpResponse('') + + zipcode = zipcode[0] + data = { + 'city': zipcode.city, + 'state': zipcode.state, + 'zip': zipcode.zip, + 'latitude': zipcode.latitude, + 'longitude': zipcode.longitude, + 'timezone': zipcode.timezone, + 'dst': zipcode.dst, + } + return HttpResponse(json.dumps(data), content_type='application/json')