diff --git a/enums.py b/enums.py new file mode 100644 index 0000000..730f7bf --- /dev/null +++ b/enums.py @@ -0,0 +1,53 @@ +STATE_POSTAL_NUMERIC = { + 'AL': 1, + 'AK': 2, + 'AZ': 4, + 'AR': 5, + 'CA': 6, + 'CO': 8, + 'CT': 9, + 'DE': 10, + 'DC': 11, + 'FL': 12, + 'GA': 13, + 'HI': 15, + 'ID': 16, + 'IL': 17, + 'IN': 18, + 'IA': 19, + 'KS': 20, + 'KY': 21, + 'LA': 22, + 'ME': 23, + 'MD': 24, + 'MA': 25, + 'MI': 26, + 'MN': 27, + 'MS': 28, + 'MO': 29, + 'MT': 30, + 'NE': 31, + 'NV': 32, + 'NH': 33, + 'NJ': 34, + 'NM': 35, + 'NY': 36, + 'NC': 37, + 'ND': 38, + 'OH': 39, + 'OK': 40, + 'OR': 41, + 'PA': 42, + 'RI': 44, + 'SC': 45, + 'SD': 46, + 'TN': 47, + 'TX': 48, + 'UT': 49, + 'VT': 50, + 'VA': 51, + 'WA': 53, + 'WV': 54, + 'WI': 55, + 'WY': 56, +} diff --git a/fields.py b/fields.py index 5f44ee0..99f8fc8 100644 --- a/fields.py +++ b/fields.py @@ -1,7 +1,17 @@ import decimal, datetime +import inspect +from enums import STATE_POSTAL_NUMERIC class ValidationError(Exception): - pass + def __init__(self, msg, field=None): + self.msg = msg + self.field = field + + def __str__(self): + if self.field: + return "(%s.%s) %s" % (self.field.parent_name, self.field.name, self.msg) + else: + return repr(self.msg) class Field(object): creation_counter = 0 @@ -42,9 +52,9 @@ class Field(object): class TextField(Field): def validate(self): if self.value == None and self.required: - raise ValidationError("value required") - if len(self.value) > self.max_length: - raise ValidationError("value is too long") + raise ValidationError("value required", field=self) + if len(self.get_data()) > self.max_length: + raise ValidationError("value is too long", field=self) def get_data(self): value = self.value or "" @@ -54,9 +64,28 @@ class TextField(Field): class StateField(TextField): - def __init__(self, name=None, required=True): - return super(StateField, self).__init__(name=name, max_length=2, required=required) + def __init__(self, name=None, required=True, use_numeric=False): + super(StateField, self).__init__(name=name, max_length=2, required=required) + self.use_numeric = use_numeric + def get_data(self): + value = self.value or "" + if value.strip() and self.use_numeric: + return str(STATE_POSTAL_NUMERIC[value.upper()]).zfill(self.max_length) + else: + return value.ljust(self.max_length).encode('ascii') + + def validate(self): + super(StateField, self).validate() + if self.value and self.value.upper() not in STATE_POSTAL_NUMERIC.keys(): + raise ValidationError("%s is not a valid state abbreviation" % self.value, field=self) + + def parse(self, s): + if s.strip() and self.use_numeric: + states = dict( [(v,k) for (k,v) in STATE_POSTAL_NUMERIC.items()] ) + self.value = states[int(s)] + else: + self.value = s class EmailField(TextField): def __init__(self, name=None, required=True, max_length=None): @@ -66,14 +95,16 @@ class EmailField(TextField): class NumericField(TextField): def validate(self): super(NumericField, self).validate() - try: - int(self.value) - except ValueError: - raise ValidationError("field contains non-numeric characters") + if self.value: + try: + int(self.value) + except ValueError: + raise ValidationError("field contains non-numeric characters", field=self) + def get_data(self): value = self.value or "" - return value.zfill(self.max_length) + return str(value).zfill(self.max_length) def parse(self, s): self.value = int(s) @@ -89,6 +120,9 @@ class StaticField(TextField): pass class BlankField(TextField): + def __init__(self, name=None, max_length=0, required=False): + super(TextField, self).__init__(name=name, max_length=max_length, required=required, uppercase=False) + def get_data(self): return " " * self.max_length @@ -109,12 +143,13 @@ class BooleanField(Field): def parse(self, s): self.value = (s == '1') + class MoneyField(Field): def validate(self): if self.value == None and self.required: - raise ValidationError("value required") + raise ValidationError("value required", field=self) if len(str(int((self.value or 0)*100))) > self.max_length: - raise ValidationError("value is too long") + raise ValidationError("value is too long", field=self) def get_data(self): return str(int((self.value or 0)*100)).encode('ascii').zfill(self.max_length) @@ -122,15 +157,12 @@ class MoneyField(Field): def parse(self, s): self.value = decimal.Decimal(s) * decimal.Decimal('0.01') + class DateField(TextField): def __init__(self, name=None, required=True, value=None): - super(TextField, self).__init__(name=name, required=required, max_length=8) - if isinstance(value, datetime.date): - self._value = value - elif value: - self._value = datetime.date(*[int(x) for x in value[4:8], value[0:2], value[2:4]]) - else: - self._value = None + super(TextField, self).__init__(name=name, required=required, max_length=8) + if value: + self.value = value def get_data(self): if self._value: @@ -138,6 +170,53 @@ class DateField(TextField): return '0' * self.max_length def parse(self, s): - self.value = datetime.date(*[int(x) for x in s[4:8], s[0:2], s[2:4]]) + if int(s) > 0: + self.value = datetime.date(*[int(x) for x in s[4:8], s[0:2], s[2:4]]) + else: + self.value = None + + def __setvalue(self, value): + if isinstance(value, datetime.date): + self._value = value + elif value: + self._value = datetime.date(*[int(x) for x in value[4:8], value[0:2], value[2:4]]) + else: + self._value = None + + def __getvalue(self): + return self._value + + value = property(__getvalue, __setvalue) +class MonthYearField(TextField): + def __init__(self, name=None, required=True, value=None): + super(TextField, self).__init__(name=name, required=required, max_length=6) + + if value: + self.value = value + + def get_data(self): + if self._value: + return self._value.strftime("%m%Y") + return '0' * self.max_length + + def parse(self, s): + if int(s) > 0: + self.value = datetime.date(*[int(x) for x in s[2:6], s[0:2], 1]) + else: + self.value = None + + def __setvalue(self, value): + if isinstance(value, datetime.date): + self._value = value + elif value: + self._value = datetime.date(*[int(x) for x in value[2:6], value[0:2], 1]) + else: + self._value = None + + def __getvalue(self): + return self._value + + value = property(__getvalue, __setvalue) + diff --git a/model.py b/model.py index 7036833..f3b8658 100644 --- a/model.py +++ b/model.py @@ -10,6 +10,7 @@ class Model(object): field = getattr(self, key) if not field.name: setattr(field, 'name', key) + setattr(field, 'parent_name', self.__class__.__name__) def __setattr__(self, key, value): if hasattr(self, key) and isinstance(getattr(self, key), Field): diff --git a/record.py b/record.py index 9efcc3f..39a82a5 100644 --- a/record.py +++ b/record.py @@ -27,7 +27,7 @@ class SubmitterRecord(model.Model): blank2 = BlankField(max_length=5) company_foreign_state_province= TextField(max_length=23, required=False) company_foreign_postal_code = TextField(max_length=15, required=False) - company_country_code = TextField(max_length=2) + company_country_code = TextField(max_length=2, required=False) submitter_name = TextField(max_length=57) submitter_address = TextField(max_length=22) submitter_delivery_address = TextField(max_length=22) @@ -38,7 +38,7 @@ class SubmitterRecord(model.Model): blank3 = BlankField(max_length=5) submitter_foreign_state_province = TextField(max_length=23, required=False) submitter_foreign_postal_code = TextField(max_length=15, required=False) - submitter_country_code = TextField(max_length=2) + submitter_country_code = TextField(max_length=2, required=False) contact_name = TextField(max_length=27) contact_phone = TextField(max_length=15) contact_phone_ext = TextField(max_length=5, required=False) @@ -86,7 +86,7 @@ class EmployeeWageRecord(model.Model): employee_first_name = TextField(max_length=15) employee_middle_name = TextField(max_length=15) employee_last_name = TextField(max_length=20) - employee_suffix = TextField(max_length=4) + employee_suffix = TextField(max_length=4, required=False) location_address = TextField(max_length=22) delivery_address = TextField(max_length=22) city = TextField(max_length=22) @@ -162,13 +162,13 @@ class StateWageRecord(model.Model): record_identifier = 'RS' required = False - state_code = NumericField(max_length=2) - taxing_entity_code = TextField(max_length=5) + state_code = StateField(use_numeric=True) + taxing_entity_code = TextField(max_length=5, required=False) ssn = NumericField(max_length=9, required=False) employee_first_name = TextField(max_length=15) employee_middle_name = TextField(max_length=15) employee_last_name = TextField(max_length=20) - employee_suffix = TextField(max_length=4) + employee_suffix = TextField(max_length=4, required=False) location_address = TextField(max_length=22) delivery_address = TextField(max_length=22) city = TextField(max_length=22) @@ -178,29 +178,34 @@ class StateWageRecord(model.Model): blank1 = BlankField(max_length=5) foreign_state_province = TextField(max_length=23, required=False) foreign_postal_code = TextField(max_length=15, required=False) - country_code = TextField(max_length=2) + country_code = TextField(max_length=2, required=False) optional_code = TextField(max_length=2, required=False) - reporting_period = NumericField(max_length=6) # MAYBE MAKE A CUSTOM FIELD TYPE FOR THIS + reporting_period = MonthYearField() quarterly_unemp_ins_wages = MoneyField(max_length=11) quarterly_unemp_ins_taxable_wages = MoneyField(max_length=11) number_of_weeks_worked = NumericField(max_length=2) - date_first_employed = DateField() - date_of_separation = DateField() + date_first_employed = DateField(required=False) + date_of_separation = DateField(required=False) blank2 = BlankField(max_length=5) state_employer_account_num = NumericField(max_length=20) blank3 = BlankField(max_length=6) - state_code_2 = NumericField(max_length=2) + state_code_2 = StateField(use_numeric=True) state_taxable_wages = MoneyField(max_length=11) state_income_tax_wh = MoneyField(max_length=11) - other_state_data = TextField(max_length=10) + other_state_data = TextField(max_length=10, required=False) tax_type_code = TextField(max_length=1) # VALIDATE C, D, E, or F local_taxable_wages = MoneyField(max_length=11) local_income_tax_wh = MoneyField(max_length=11) state_control_number = NumericField(max_length=7, required=False) - supplemental_data1 = TextField(max_length=75) - supplemental_data2 = TextField(max_length=75) + supplemental_data1 = TextField(max_length=75, required=False) + supplemental_data2 = TextField(max_length=75, required=False) blank4 = BlankField(max_length=25) + def validate_tax_type_code(self, field): + if field.value not in ['C','D','E','F']: + raise ValidationError("%s not one of (c,d,e,f)" % field.value, field=f) + + class TotalRecord(model.Model): record_identifier = 'RT' required = True