diff --git a/dqc_us_rules/dqc_us_0006.py b/dqc_us_rules/dqc_us_0006.py index 400d07a6a7..f4ecd94095 100644 --- a/dqc_us_rules/dqc_us_0006.py +++ b/dqc_us_rules/dqc_us_0006.py @@ -4,19 +4,24 @@ from collections import defaultdict from datetime import timedelta from .util import facts, messages +import csv +import os CHECK_TYPES = ['textBlockItemType'] -CHECK_DEI = ['AmendmentDescription', 'AmendmentFlag', 'CurrentFiscalYearEndDate', 'DocumentPeriodEndDate', - 'DocumentFiscalYearFocus', 'DocumentFiscalPeriodFocus', 'DocumentType', 'EntityRegistrantName', - 'EntityCentralIndexKey', 'EntityFilerCategory'] -DATE_BOUNDS_DICT = { - "FY": {"min": 340, "max": 390}, - "Q1": {"min": 65, "max": 115}, - "Q2": {"min": 155, "max": 205}, - "Q3": {"min": 245, "max": 295} -} +CHECK_DEI = [ + 'AmendmentDescription', 'AmendmentFlag', 'CurrentFiscalYearEndDate', + 'DocumentPeriodEndDate', 'DocumentFiscalYearFocus', + 'DocumentFiscalPeriodFocus', 'DocumentType', 'EntityRegistrantName', + 'EntityCentralIndexKey', 'EntityFilerCategory' +] _CODE_NAME = 'DQC.US.0006' _RULE_VERSION = '1.0' +_DEFAULT_DATE_BOUNDS_FILE = os.path.join( + os.path.dirname(__file__), + 'resources', + 'DQC_US_0006', + 'dqc_06_date_bounds.csv' +) def validate_dates_within_periods(val): @@ -24,11 +29,13 @@ def validate_dates_within_periods(val): Check Date Ranges are within expected values for the fiscal focus period """ + date_bounds_dict = _date_bounds_from_csv() doc_type = facts.lookup_dei_facts('DocumentType', val.modelXbrl) if len(doc_type) != 1 or 'T' in doc_type[0].xValue: - # If it is a transitional document, or there is more than one document type declared, we will not run this check. + # If it is a transitional document, or there is more than one + # document type declared, we will not run this check. return - dict_of_facts = _date_range_check(CHECK_TYPES, CHECK_DEI, DATE_BOUNDS_DICT, val.modelXbrl) + dict_of_facts = _date_range_check(CHECK_TYPES, CHECK_DEI, date_bounds_dict, val.modelXbrl) for document_fiscal_period_focus, fact_list in dict_of_facts.items(): for fact in fact_list: val.modelXbrl.error('{}.14'.format(_CODE_NAME), messages.get_message(_CODE_NAME), concept=fact.qname, @@ -69,17 +76,38 @@ def _date_range_check(check_types, check_dei, date_bounds_dict, modelXbrl): def _dict_list_update(dict_a, dict_b): """ - helper for the LEA dictionaries, extends the lists from dict_a with the lists in dict_b. + Helper for the LEA dictionaries, extends the lists from dict_a with the + lists in dict_b. """ for key, val in dict_b.items(): dict_a[key].extend(val) return dict_a +def _date_bounds_from_csv(): + """ + Returns a map of {time_period: {'min':min_value,'max':max_value}} + ex: date_bounds_from_csv()['Q1'] = {'min':65,'max':115} + + :rtype: dict + :return: A map of {time_period: {'min':min_value,'max':max_value}}. + """ + with open(_DEFAULT_DATE_BOUNDS_FILE, 'r') as f: + reader = csv.reader(f) + date_bounds_dict = {} + next(reader, None) + for row in reader: + date_bounds_dict[row[0]] = {'min': int(row[1]), 'max': int(row[2])} + return date_bounds_dict + __pluginInfo__ = { 'name': _CODE_NAME, 'version': _RULE_VERSION, - 'description': '''Checks all of the specified types and concepts for their date ranges to verify the ranges are within expected paramters for the fiscal periods''', - #Mount points + 'description': ( + 'Checks all of the specified types and concepts for their date ' + 'ranges to verify the ranges are within expected paramters for the ' + 'fiscal periods' + ), + # Mount points 'Validate.XBRL.Finally': validate_dates_within_periods, } diff --git a/dqc_us_rules/resources/DQC_US_0006/dqc_06_date_bounds.csv b/dqc_us_rules/resources/DQC_US_0006/dqc_06_date_bounds.csv new file mode 100644 index 0000000000..23dc80a7bb --- /dev/null +++ b/dqc_us_rules/resources/DQC_US_0006/dqc_06_date_bounds.csv @@ -0,0 +1,5 @@ +Period,Min,Max +"FY","340","390" +"Q1","65","115" +"Q3","245","295" +"Q2","155","205" diff --git a/dqc_us_rules/util/facts.py b/dqc_us_rules/util/facts.py index 7b8c9e0a77..7db38167b3 100644 --- a/dqc_us_rules/util/facts.py +++ b/dqc_us_rules/util/facts.py @@ -175,7 +175,7 @@ def LegalEntityAxis_facts_by_member(facts): results = defaultdict(list) for fact in facts: legalDim = LEGALENTITYAXIS_DEFAULT - if fact_components_valid(fact): + if _fact_components_valid(fact): dims = [dim for dim in fact.context.segDimValues.values() if dim.isExplicit and dim.member is not None] for dim in dims: if dim.dimension.qname.localName == 'LegalEntityAxis': @@ -185,13 +185,13 @@ def LegalEntityAxis_facts_by_member(facts): return results -def fact_components_valid(fact): +def _fact_components_valid(fact): """ - Return true if all of the components in a fact are none + Return true if all of the components in a fact are not none :param fact: The fact to check if it is valid :type fact: arelle.ModelInstanceObject.ModelFact - :return: True if none of the components of the fact are None + :return: True if none of the components of the fact are not None :rtype: bool """ if fact is None: diff --git a/tests/unit_tests/test_dqc_us_0006.py b/tests/unit_tests/test_dqc_us_0006.py index f14085d0f2..b7d0730235 100644 --- a/tests/unit_tests/test_dqc_us_0006.py +++ b/tests/unit_tests/test_dqc_us_0006.py @@ -56,3 +56,43 @@ def test_lea_facts_and_update(self): expected = defaultdict(list) expected.update({'': [fact2, fact3], 'Company1': [fact1]}) self.assertEqual(res3, expected) + + +class TestDateBoundsCSV(unittest.TestCase): + def test_date_bounds_csv_keys_equal(self): + """ + Test to make sure that the dictionary read in from the csv shares + equals the original date_bounds_dict + """ + date_bounds_dict = { + 'FY': {'min': 340, 'max': 390}, + 'Q1': {'min': 65, 'max': 115}, + 'Q3': {'min': 245, 'max': 295}, + 'Q2': {'min': 155, 'max': 205} + } + + date_bounds_dict_from_csv = dqc_us_0006._date_bounds_from_csv() + + self.assertDictEqual(date_bounds_dict, date_bounds_dict_from_csv) + + def test_date_bounds_csv_keys_unequal(self): + """ + Test to make sure that the dictionary read is doesn't equal something + other than the original DATE_BOUNDS_DICT + """ + random_date_bounds_dict = { + 'FY': {'min': 374, 'max': 489}, + 'Q1': {'min': 234, 'max': 394}, + 'Q3': {'min': 890, 'max': 891}, + 'Q2': {'min': 300, 'max': 790} + } + + date_bounds_dict_from_csv = dqc_us_0006._date_bounds_from_csv() + self.assertEqual(sorted(list(random_date_bounds_dict.keys())), sorted(list(date_bounds_dict_from_csv.keys()))) + + for key in random_date_bounds_dict.keys(): + for subkey in random_date_bounds_dict[key].keys(): + self.assertNotEqual( + random_date_bounds_dict[key][subkey], + date_bounds_dict_from_csv[key][subkey] + ) diff --git a/tests/unit_tests/util/test_facts.py b/tests/unit_tests/util/test_facts.py index 872a9683e1..f8a8ccf25a 100644 --- a/tests/unit_tests/util/test_facts.py +++ b/tests/unit_tests/util/test_facts.py @@ -622,20 +622,21 @@ def test_member_qnames(self): expected = ['CashCheckAxis'] self.assertEqual(expected, fact_lib.member_qnames(fact)) + class TestFactsAreValid(unittest.TestCase): def test_fact_components_valid_on_valid_fact(self): """ Tests to make sure that a valid fact still works """ - fact = Mock(decimals='-4', value='869098', xvalue = 869098, precision = None) - self.assertTrue(fact_lib.fact_components_valid(fact)) + fact = Mock(decimals='-4', value='869098', xvalue=869098, precision=None) + self.assertTrue(fact_lib._fact_components_valid(fact)) def test_fact_components_valid_on_none_type_fact(self): """ Tests to make sure that a None type fact is not valid """ fact = None - self.assertFalse(fact_lib.fact_components_valid(fact)) + self.assertFalse(fact_lib._fact_components_valid(fact)) def test_fact_components_valid_on_none_type_context(self): """ @@ -643,12 +644,13 @@ def test_fact_components_valid_on_none_type_context(self): """ fact = Mock(decimals='-1', value='670', xValue=670, precision=None) fact.context = None - self.assertFalse(fact_lib.fact_components_valid(fact)) + self.assertFalse(fact_lib._fact_components_valid(fact)) def test_fact_components_valid_on_none_type_segDimValue(self): """ - Tests to make sure that a Fact.context with a None type segDimValue is not valid + Tests to make sure that a Fact.context with a None type segDimValue is + not valid """ fact = Mock(decimals='-2', value='6500', xValue=6500, precision=None) fact.context.segDimValues = None - self.assertFalse(fact_lib.fact_components_valid(fact)) + self.assertFalse(fact_lib._fact_components_valid(fact))