From 5781cbf3353bac1bb6ea47296cbddbd3c88ca6ed Mon Sep 17 00:00:00 2001 From: Binh Van Nguyen Date: Sat, 7 May 2011 15:19:48 -0500 Subject: [PATCH] Finished up most of the record order validation and also checking for all required records in a set. Added a controller class but decided to put stuff in __init__ instead, at least for now. Added a DateField which converts datetime.date into the proper string format for EFW2 files (hopefully), this should still be tested next week. --- __init__.py | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++-- fields.py | 22 +++++++++++++--- model.py | 1 + record.py | 28 +++++++++++++------- 4 files changed, 113 insertions(+), 14 deletions(-) diff --git a/__init__.py b/__init__.py index c72374e..bd92468 100644 --- a/__init__.py +++ b/__init__.py @@ -31,6 +31,18 @@ def test_dump(): return out +def test_record_order(): + import record + records = [ + record.SubmitterRecord(), + record.EmployerRecord(), + record.EmployeeWageRecord(), + record.TotalRecord(), + record.FinalRecord(), + ] + verify_record_order(records) + + def test_load(fp): return load(fp) @@ -50,7 +62,6 @@ def load(fp): record.read(fp) yield record - def loads(s): import StringIO fp = StringIO.StringIO(s) @@ -61,7 +72,6 @@ def dump(records, fp): for r in records: fp.write(r.output()) - def dumps(records): import StringIO fp = StringIO.StringIO() @@ -69,3 +79,65 @@ def dumps(records): fp.seek(0) return fp.read() + +# THIS WAS IN CONTROLLER, BUT UNLESS WE +# REALLY NEED A CONTROLLER CLASS, IT'S SIMPLER +# TO JUST KEEP IT IN HERE. +def verify_required_records(records): + types = [rec.__class__.__name__ for rec in records] + req_types = [] + + for r in record.RECORD_TYPES: + klass = record.__dict__[r] + if klass.required: + req_types.append(klass.__name__) + + while req_types: + req = req_types[0] + if req not in types: + raise ValidationError("Record set missing required record: %s" % req) + else: + req_types.remove(req) + +def verify_record_order(records): + import record + from fields import ValidationError + + # 1st record must be SubmitterRecord + if not isinstance(records[0], record.SubmitterRecord): + raise ValidationError("First record must be SubmitterRecord") + + # 2nd record must be EmployeeRecord + if not isinstance(records[1], record.EmployerRecord): + raise ValidationError("The first record after SubmitterRecord must be an EmployeeRecord") + + # FinalRecord - Must be the last record on the file + if not isinstance(records[-1], record.FinalRecord): + raise ValidationError("Last record must be a FinalRecord") + + # an EmployerRecord *must* come after each EmployeeWageREcord + for i in range(len(records)): + if isinstance(records[i], record.EmployerRecord): + if not isinstance(records[i+1], record.EmployeeWageRecord): + raise ValidationError("All EmployerRecords must be followed by an EmployeeWageRecord") + + num_ro_records = len(filter(lambda x:isinstance(x, record.OptionalEmployeeWageRecord), records)) + num_ru_records = len(filter(lambda x:isinstance(x, record.OptionalTotalRecord), records)) + num_employer_records = len(filter(lambda x:isinstance(x, record.EmployerRecord), records)) + num_total_records = len(filter(lambda x: isinstance(x, record.TotalRecord), records)) + + # a TotalRecord is required for each instance of an EmployeeRecord + if num_total_records != num_employer_records: + raise ValidationError("Number of TotalRecords (%d) does not match number of EmployeeRecords (%d)" % ( + num_total_records, num_employer_records)) + + # an OptionalTotalRecord is required for each OptionalEmployeeWageRecord + if num_ro_records != num_ru_records: + raise ValidationError("Number of OptionalEmployeeWageRecords (%d) does not match number OptionalTotalRecords (%d)" % ( + num_ro_records, num_ru_records)) + + # FinalRecord - Must appear only once on each file. + if len(filter(lambda x:isinstance(x, record.FinalRecord), records)) != 1: + raise ValidationError("Incorrect number of FinalRecords") + + diff --git a/fields.py b/fields.py index 5119211..5f44ee0 100644 --- a/fields.py +++ b/fields.py @@ -1,4 +1,4 @@ -import decimal +import decimal, datetime class ValidationError(Exception): pass @@ -123,5 +123,21 @@ class MoneyField(Field): self.value = decimal.Decimal(s) * decimal.Decimal('0.01') class DateField(TextField): - #FIXME I NEED TO BE WRITTEN! - pass + 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 + + def get_data(self): + if self._value: + return self._value.strftime('%m%d%Y') + 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]]) + + diff --git a/model.py b/model.py index 731a088..7036833 100644 --- a/model.py +++ b/model.py @@ -2,6 +2,7 @@ from fields import Field class Model(object): record_identifier = ' ' + required = False def __init__(self): for (key, value) in self.__class__.__dict__.items(): diff --git a/record.py b/record.py index 5efcdf3..9efcc3f 100644 --- a/record.py +++ b/record.py @@ -1,14 +1,15 @@ import model from fields import * -__all__ = ['SubmitterRecord', 'EmployerRecord', +__all__ = RECORD_TYPES = ['SubmitterRecord', 'EmployerRecord', 'EmployeeWageRecord', 'OptionalEmployeeWageRecord', 'TotalRecord', 'OptionalTotalRecord', 'StateTotalRecord', 'FinalRecord',] class SubmitterRecord(model.Model): record_identifier = 'RA' - + required = True + submitter_ein = NumericField(max_length=9) user_id = TextField(max_length=8) software_vendor = TextField(max_length=4) @@ -51,7 +52,8 @@ class SubmitterRecord(model.Model): class EmployerRecord(model.Model): record_identifier = 'RE' - + required = True + tax_year = NumericField(max_length=4) agent_indicator = NumericField(max_length=1) employer_ein = TextField(max_length=9) @@ -78,7 +80,8 @@ class EmployerRecord(model.Model): class EmployeeWageRecord(model.Model): record_identifier = 'RW' - + required = True + ssn = NumericField(max_length=9, required=False) employee_first_name = TextField(max_length=15) employee_middle_name = TextField(max_length=15) @@ -129,7 +132,8 @@ class EmployeeWageRecord(model.Model): class OptionalEmployeeWageRecord(model.Model): record_identifier = 'RO' - + required = False + blank1 = BlankField(max_length=9) allocated_tips = MoneyField(max_length=11) uncollected_tax_on_tips = MoneyField(max_length=11) @@ -156,6 +160,7 @@ class OptionalEmployeeWageRecord(model.Model): class StateWageRecord(model.Model): record_identifier = 'RS' + required = False state_code = NumericField(max_length=2) taxing_entity_code = TextField(max_length=5) @@ -179,8 +184,8 @@ class StateWageRecord(model.Model): 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 = TextField(max_length=8)#DateField() - date_of_separation = TextField(max_length=8)#DateField() + date_first_employed = DateField() + date_of_separation = DateField() blank2 = BlankField(max_length=5) state_employer_account_num = NumericField(max_length=20) blank3 = BlankField(max_length=6) @@ -198,6 +203,7 @@ class StateWageRecord(model.Model): class TotalRecord(model.Model): record_identifier = 'RT' + required = True number_of_rw_records = NumericField(max_length=7) wages_tips = NumericField(max_length=15) @@ -231,7 +237,8 @@ class TotalRecord(model.Model): class OptionalTotalRecord(model.Model): record_identifier = 'RU' - + required = False + number_of_ro_records = NumericField(max_length=7) allocated_tips = NumericField(max_length=15) uncollected_tax_on_tips = NumericField(max_length=15) @@ -257,11 +264,14 @@ class OptionalTotalRecord(model.Model): class StateTotalRecord(model.Model): record_identifier = 'RV' + required = False + supplemental_data = TextField(max_length=510) class FinalRecord(model.Model): record_identifier = 'RF' - + required = True + blank1 = BlankField(max_length=5) number_of_rw_records = NumericField(max_length=9) blank2 = BlankField(max_length=496)