# -*- coding: utf-8; -*- ################################################################################ # # pyCOREPOS -- Python Interface to CORE POS # Copyright © 2018-2020 Lance Edgar # # This file is part of pyCOREPOS. # # pyCOREPOS is free software: you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) # any later version. # # pyCOREPOS 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 General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # pyCOREPOS. If not, see . # ################################################################################ """ Data model for CORE POS "office_op" DB """ import sqlalchemy as sa from sqlalchemy import orm from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.associationproxy import association_proxy Base = declarative_base() class Change(Base): """ Represents a changed (or deleted) record, which is pending synchronization to another system(s). .. note:: This table may or may not be installed to a given CORE Office Op DB. Its presence is required if Rattail datasync needs to "watch" the DB. """ __tablename__ = 'datasync_changes' id = sa.Column(sa.Integer(), nullable=False, primary_key=True) object_type = sa.Column(sa.String(length=255), nullable=False) object_key = sa.Column(sa.String(length=255), nullable=False) deleted = sa.Column(sa.Boolean(), nullable=False, default=False) class Parameter(Base): """ Represents a "parameter" value. """ __tablename__ = 'parameters' store_id = sa.Column(sa.SmallInteger(), primary_key=True, nullable=False) lane_id = sa.Column(sa.SmallInteger(), primary_key=True, nullable=False) param_key = sa.Column(sa.String(length=100), primary_key=True, nullable=False) param_value = sa.Column(sa.String(length=255), nullable=True) is_array = sa.Column(sa.Boolean(), nullable=True) def __str__(self): return "{}-{} {}".format(self.store_id, self.lane_id, self.param_key) class Department(Base): """ Represents a department within the organization. """ __tablename__ = 'departments' number = sa.Column('dept_no', sa.SmallInteger(), primary_key=True, autoincrement=False, nullable=False) name = sa.Column('dept_name', sa.String(length=30), nullable=True) tax = sa.Column('dept_tax', sa.Boolean(), nullable=True) food_stampable = sa.Column('dept_fs', sa.Boolean(), nullable=True) limit = sa.Column('dept_limit', sa.Float(), nullable=True) minimum = sa.Column('dept_minimum', sa.Float(), nullable=True) discount = sa.Column('dept_discount', sa.Boolean(), nullable=True) # TODO: probably should rename this attribute, but to what? dept_see_id = sa.Column(sa.Boolean(), nullable=True) modified = sa.Column(sa.DateTime(), nullable=True) modified_by_id = sa.Column('modifiedby', sa.Integer(), nullable=True) margin = sa.Column(sa.Float(), nullable=False, default=0) sales_code = sa.Column('salesCode', sa.Integer(), nullable=False, default=0) member_only = sa.Column('memberOnly', sa.SmallInteger(), nullable=False, default=0) def __str__(self): return self.name or '' class Subdepartment(Base): """ Represents a subdepartment within the organization. """ __tablename__ = 'subdepts' __table_args__ = ( sa.ForeignKeyConstraint(['dept_ID'], ['departments.dept_no']), ) number = sa.Column('subdept_no', sa.SmallInteger(), primary_key=True, autoincrement=False, nullable=False) name = sa.Column('subdept_name', sa.String(length=30), nullable=True) department_number = sa.Column('dept_ID', sa.SmallInteger(), nullable=True) department = orm.relationship( Department, doc=""" Reference to the parent :class:`Department` for this subdepartment. """) def __str__(self): return self.name or '' class Vendor(Base): """ Represents a vendor from which product may be purchased. """ __tablename__ = 'vendors' # TODO: this maybe should be the pattern we use going forward, for all # models? for now it was deemed necessary to "match" the API output vendorID = sa.Column(sa.Integer(), primary_key=True, autoincrement=False, nullable=False) id = orm.synonym('vendorID') vendorName = sa.Column(sa.String(length=50), nullable=True) name = orm.synonym('vendorName') vendorAbbreviation = sa.Column(sa.String(length=10), nullable=True) abbreviation = orm.synonym('vendorAbbreviation') discountRate = sa.Column(sa.Float(), nullable=True) discount_rate = orm.synonym('discountRate') contact = orm.relationship( 'VendorContact', uselist=False, doc=""" Reference to the :class:`VendorContact` instance for this vendor. """) phone = association_proxy( 'contact', 'phone', creator=lambda p: VendorContact(phone=p)) fax = association_proxy( 'contact', 'fax', creator=lambda f: VendorContact(fax=f)) email = association_proxy( 'contact', 'email', creator=lambda e: VendorContact(email=e)) website = association_proxy( 'contact', 'website', creator=lambda w: VendorContact(website=w)) notes = association_proxy( 'contact', 'notes', creator=lambda n: VendorContact(notes=n)) def __str__(self): return self.name or '' class VendorContact(Base): """ A general contact record for a vendor. """ __tablename__ = 'vendorContact' vendor_id = sa.Column('vendorID', sa.Integer(), sa.ForeignKey('vendors.vendorID'), primary_key=True, autoincrement=False, nullable=False) phone = sa.Column(sa.String(length=15), nullable=True) fax = sa.Column(sa.String(length=15), nullable=True) email = sa.Column(sa.String(length=50), nullable=True) website = sa.Column(sa.String(length=100), nullable=True) notes = sa.Column(sa.Text(), nullable=True) class Product(Base): """ Represents a product, purchased and/or sold by the organization. """ __tablename__ = 'products' __table_args__ = ( sa.ForeignKeyConstraint(['department'], ['departments.dept_no']), ) id = sa.Column(sa.Integer(), primary_key=True, autoincrement=True, nullable=False) upc = sa.Column(sa.String(length=13), nullable=True) description = sa.Column(sa.String(length=30), nullable=True) brand = sa.Column(sa.String(length=30), nullable=True) formatted_name = sa.Column(sa.String(length=30), nullable=True) normal_price = sa.Column(sa.Float(), nullable=True) price_method = sa.Column('pricemethod', sa.SmallInteger(), nullable=True) group_price = sa.Column('groupprice', sa.Float(), nullable=True) quantity = sa.Column(sa.SmallInteger(), nullable=True) special_price = sa.Column(sa.Float(), nullable=True) special_price_method = sa.Column('specialpricemethod', sa.SmallInteger(), nullable=True) special_group_price = sa.Column('specialgroupprice', sa.Float(), nullable=True) special_quantity = sa.Column('specialquantity', sa.SmallInteger(), nullable=True) start_date = sa.Column(sa.DateTime(), nullable=True) end_date = sa.Column(sa.DateTime(), nullable=True) department_number = sa.Column('department', sa.SmallInteger(), nullable=True) size = sa.Column(sa.String(length=9), nullable=True) tax = sa.Column(sa.SmallInteger(), nullable=True) foodstamp = sa.Column(sa.Boolean(), nullable=True) scale = sa.Column(sa.Boolean(), nullable=True) scale_price = sa.Column('scaleprice', sa.Boolean(), nullable=True, default=False) mix_match_code = sa.Column('mixmatchcode', sa.String(length=13), nullable=True) modified = sa.Column(sa.DateTime(), nullable=True) # advertised = sa.Column(sa.Boolean(), nullable=True) tare_weight = sa.Column('tareweight', sa.Float(), nullable=True) discount = sa.Column(sa.SmallInteger(), nullable=True) discount_type = sa.Column('discounttype', sa.SmallInteger(), nullable=True) line_item_discountable = sa.Column(sa.Boolean(), nullable=True) unit_of_measure = sa.Column('unitofmeasure', sa.String(length=15), nullable=True) wicable = sa.Column(sa.SmallInteger(), nullable=True) quantity_enforced = sa.Column('qttyEnforced', sa.Boolean(), nullable=True) id_enforced = sa.Column('idEnforced', sa.Boolean(), nullable=True) cost = sa.Column(sa.Float(), nullable=True, default=0) in_use = sa.Column('inUse', sa.Boolean(), nullable=True) flags = sa.Column('numflag', sa.Integer(), nullable=True, default=0) subdepartment_number = sa.Column('subdept', sa.SmallInteger(), nullable=True) deposit = sa.Column(sa.Float(), nullable=True) local = sa.Column(sa.Integer(), nullable=True, default=0) store_id = sa.Column(sa.SmallInteger(), nullable=True, default=0) default_vendor_id = sa.Column(sa.Integer(), nullable=True, default=0) current_origin_id = sa.Column(sa.Integer(), nullable=True, default=0) department = orm.relationship( Department, primaryjoin=Department.number == department_number, foreign_keys=[department_number], doc=""" Reference to the :class:`Department` to which the product belongs. """) vendor = orm.relationship( Vendor, primaryjoin=Vendor.id == default_vendor_id, foreign_keys=[default_vendor_id], doc=""" Reference to the default :class:`Vendor` from which the product is obtained. """) @property def full_description(self): fields = ['brand', 'description', 'size'] fields = [getattr(self, f) or '' for f in fields] fields = filter(bool, fields) return ' '.join(fields) def __str__(self): return self.description or '' class ProductFlag(Base): """ Represents a product flag attribute. """ __tablename__ = 'prodFlags' bit_number = sa.Column(sa.SmallInteger(), primary_key=True, autoincrement=False, nullable=False, default=0) description = sa.Column(sa.String(length=50), nullable=True) active = sa.Column(sa.Boolean(), nullable=True, default=True) def __str__(self): return self.description or '' class Employee(Base): """ Represents an employee within the organization. """ __tablename__ = 'employees' number = sa.Column('emp_no', sa.SmallInteger(), primary_key=True, autoincrement=False, nullable=False) cashier_password = sa.Column('CashierPassword', sa.String(length=50), nullable=True) admin_password = sa.Column('AdminPassword', sa.String(length=50), nullable=True) first_name = sa.Column('FirstName', sa.String(length=255), nullable=True) last_name = sa.Column('LastName', sa.String(length=255), nullable=True) job_title = sa.Column('JobTitle', sa.String(length=255), nullable=True) active = sa.Column('EmpActive', sa.Boolean(), nullable=True) frontend_security = sa.Column('frontendsecurity', sa.SmallInteger(), nullable=True) backend_security = sa.Column('backendsecurity', sa.SmallInteger(), nullable=True) birth_date = sa.Column('birthdate', sa.DateTime(), nullable=True) def __str__(self): return ' '.join([self.first_name or '', self.last_name or '']).strip() class MemberType(Base): """ Represents a type of membership within the organization. """ __tablename__ = 'memtype' id = sa.Column('memtype', sa.SmallInteger(), primary_key=True, nullable=False, default=0) description = sa.Column('memDesc', sa.String(length=20), nullable=True) customer_type = sa.Column('custdataType', sa.String(length=10), nullable=True) discount = sa.Column(sa.SmallInteger(), nullable=True) staff = sa.Column(sa.Boolean(), nullable=True) ssi = sa.Column(sa.Boolean(), nullable=True) # TODO: this was apparently added "recently" - isn't present in all DBs # (need to figure out how to conditionally include it in model?) # sales_code = sa.Column('salesCode', sa.Integer(), nullable=True) def __str__(self): return self.description or "" class Customer(Base): """ Represents a customer of the organization. """ __tablename__ = 'custdata' id = sa.Column(sa.Integer(), primary_key=True, autoincrement=True, nullable=False) card_number = sa.Column('CardNo', sa.Integer(), nullable=True) person_number = sa.Column('personNum', sa.SmallInteger(), nullable=False, default=1) first_name = sa.Column('FirstName', sa.String(length=30), nullable=True) last_name = sa.Column('LastName', sa.String(length=30), nullable=True) cash_back = sa.Column('CashBack', sa.Float(), nullable=False, default=60) balance = sa.Column('Balance', sa.Float(), nullable=False, default=0) discount = sa.Column('Discount', sa.SmallInteger(), nullable=True) member_discount_limit = sa.Column('MemDiscountLimit', sa.Float(), nullable=False, default=0) charge_limit = sa.Column('ChargeLimit', sa.Float(), nullable=False, default=0) charge_ok = sa.Column('ChargeOk', sa.Boolean(), nullable=False, default=False) write_checks = sa.Column('WriteChecks', sa.Boolean(), nullable=False, default=True) store_coupons = sa.Column('StoreCoupons', sa.Boolean(), nullable=False, default=True) type = sa.Column('Type', sa.String(length=10), nullable=False, default='pc') member_type_id = sa.Column('memType', sa.SmallInteger(), nullable=True) member_type = orm.relationship( MemberType, primaryjoin=MemberType.id == member_type_id, foreign_keys=[member_type_id], doc=""" Reference to the :class:`MemberType` to which this member belongs. """) staff = sa.Column(sa.Boolean(), nullable=False, default=False) ssi = sa.Column('SSI', sa.Boolean(), nullable=False, default=False) purchases = sa.Column('Purchases', sa.Float(), nullable=False, default=0) number_of_checks = sa.Column('NumberOfChecks', sa.SmallInteger(), nullable=False, default=0) member_coupons = sa.Column('memCoupons', sa.Integer(), nullable=False, default=1) blue_line = sa.Column('blueLine', sa.String(length=50), nullable=True) shown = sa.Column('Shown', sa.Boolean(), nullable=False, default=True) last_change = sa.Column('LastChange', sa.DateTime(), nullable=False) member_info = orm.relationship( 'MemberInfo', primaryjoin='MemberInfo.card_number == Customer.card_number', foreign_keys=[card_number], uselist=False, back_populates='customers', doc=""" Reference to the :class:`MemberInfo` instance for this customer. """) def __str__(self): return "{} {}".format(self.first_name or '', self.last_name or '').strip() class MemberInfo(Base): """ Contact info regarding a member of the organization. """ __tablename__ = 'meminfo' card_number = sa.Column('card_no', sa.Integer(), primary_key=True, autoincrement=False, nullable=False) last_name = sa.Column(sa.String(length=30), nullable=True) first_name = sa.Column(sa.String(length=30), nullable=True) other_last_name = sa.Column('othlast_name', sa.String(length=30), nullable=True) other_first_name = sa.Column('othfirst_name', sa.String(length=30), nullable=True) street = sa.Column(sa.String(length=255), nullable=True) city = sa.Column(sa.String(length=20), nullable=True) state = sa.Column(sa.String(length=2), nullable=True) zip = sa.Column(sa.String(length=10), nullable=True) phone = sa.Column(sa.String(length=30), nullable=True) email = sa.Column('email_1', sa.String(length=50), nullable=True) email2 = sa.Column('email_2', sa.String(length=50), nullable=True) ads_ok = sa.Column('ads_OK', sa.Boolean(), nullable=True, default=True) customers = orm.relationship( Customer, primaryjoin=Customer.card_number == card_number, foreign_keys=[Customer.card_number], back_populates='member_info', remote_side=Customer.card_number, doc=""" List of :class:`Customer` instances which are associated with this member info. """) dates = orm.relationship( 'MemberDate', primaryjoin='MemberDate.card_number == MemberInfo.card_number', foreign_keys='MemberDate.card_number', cascade='all, delete-orphan', doc=""" List of date records for the member. """, backref=orm.backref( 'member', doc=""" Reference to the member to whom the date record applies. """)) @property def full_name(self): return '{} {}'.format(self.first_name or '', self.last_name or '').strip() def __str__(self): return self.full_name class MemberDate(Base): """ Join/exit dates for members """ __tablename__ = 'memDates' card_number = sa.Column('card_no', sa.Integer(), primary_key=True, autoincrement=False, nullable=False) start_date = sa.Column(sa.DateTime(), nullable=True) end_date = sa.Column(sa.DateTime(), nullable=True) def __str__(self): return "{} thru {}".format( self.start_date.date() if self.start_date else "??", self.end_date.date() if self.end_date else "??") class MemberContact(Base): """ Contact preferences for members """ __tablename__ = 'memContact' card_number = sa.Column('card_no', sa.Integer(), primary_key=True, autoincrement=False, nullable=False) preference = sa.Column('pref', sa.Integer(), nullable=True) member = orm.relationship( MemberInfo, primaryjoin=MemberInfo.card_number == card_number, foreign_keys=[MemberInfo.card_number], uselist=False, doc=""" Reference to the member to whom the contact record applies. """, backref=orm.backref( 'contact', uselist=False, doc=""" Reference to contact preference record for the member. """)) def __str__(self): return str(self.preference) class HouseCoupon(Base): """ Represents a "house" (store) coupon. """ __tablename__ = 'houseCoupons' __table_args__ = ( sa.ForeignKeyConstraint(['department'], ['departments.dept_no']), ) coupon_id = sa.Column('coupID', sa.Integer(), primary_key=True, nullable=False) description = sa.Column(sa.String(length=30), nullable=True) start_date = sa.Column('startDate', sa.DateTime(), nullable=True) end_date = sa.Column('endDate', sa.DateTime(), nullable=True) limit = sa.Column(sa.SmallInteger(), nullable=True) member_only = sa.Column('memberOnly', sa.SmallInteger(), nullable=True) discount_type = sa.Column('discountType', sa.String(length=2), nullable=True) discount_value = sa.Column('discountValue', sa.Numeric(precision=10, scale=2), nullable=True) min_type = sa.Column('minType', sa.String(length=2), nullable=True) min_value = sa.Column('minValue', sa.Numeric(precision=10, scale=2), nullable=True) department_id = sa.Column('department', sa.Integer(), nullable=True) department = orm.relationship(Department) auto = sa.Column(sa.Boolean(), nullable=True, default=False) # TODO: this isn't yet supported in all production DBs # virtual_only = sa.Column('virtualOnly', sa.Boolean(), nullable=True, default=False) def __str__(self): return self.description or ''