def __init__(self, parent=None, period=None): self.id = ID() # does not get its own bbid, just holds namespace # parent for Financials is BusinessUnit self.relationships = Relationships(self, parent=parent) self._period = period self._restricted = False self._full_order = self.DEFAULT_ORDER.copy() statements = [Statement(name=self.OVERVIEW_NAME, parent=self, period=period), Statement(name=self.INCOME_NAME, parent=self, period=period), CashFlow(name=self.CASH_NAME, parent=self, period=period), Statement(name=self.VALUATION_NAME, parent=self, period=period, compute=False), BalanceSheet(name=self.START_BAL_NAME, parent=self, period=period), BalanceSheet(name=self.ENDING_BAL_NAME, parent=self, period=period)] self._statement_directory = dict() for stmt in statements: self._statement_directory[stmt.name.casefold()] = stmt self.update_statements = list()
def from_database(cls, portal_data, link_list=list()): new = cls(None) new.tags = Tags.from_database(portal_data['tags']) new._parameters = Parameters.from_database(portal_data['parameters'], target='parameters') new._type = portal_data['type'] new.life = LifeCycle.from_database(portal_data['life']) new.location = portal_data['location'] new.size = portal_data['size'] ids = portal_data['used'] real_ids = [ID.from_database(id).bbid for id in ids] new.used = set(real_ids) new.guide = Guide.from_database(portal_data['guide']) new.components = [ ID.from_database(id).bbid for id in portal_data['components'] ] new.interview = InterviewTracker.from_database( portal_data['interview'], link_list) new.summary = None # Obsolete new.valuation = None # Obsolete new.stage = None # Obsolete new._path_archive = portal_data['path_archive'] # don't bother reinfl- # ating archived paths, they won't be used new._used_archive = portal_data['used_archive'] fins = portal_data.get('financials_structure') new_fins = Financials.from_database(fins, new, period=None) new.set_financials(new_fins) new.cap_table = CapTable.from_database(portal_data['cap_table']) return new
def get_line(self, **kargs): """ Model.get_line() -> LineItem --``bbid`` is bbid of line --``buid`` is BU id Method finds a LineItem matching the locator. """ period_end = kargs['period'] bbid = ID.from_database(kargs['bbid']).bbid buid = ID.from_database(kargs['buid']).bbid fins_attr = kargs['financials_attr'] if period_end: key = ( kargs.get('resolution', 'monthly'), kargs.get('name', 'default'), ) time_line = self.timelines[key] if isinstance(period_end, str): period_end = date_from_iso(period_end) period = time_line[period_end] else: period = self.time_line.current_period financials = self.get_financials(buid, period) line = financials.find_line(bbid, fins_attr) return line
def __init__(self, name, fins=None, model=None): TagsMixIn.__init__(self, name) self._parameters = Parameters() self._type = None self.id = ID() self.life = LifeCycle() self.location = None self.size = 1 self.xl = xl_mgmt.UnitData() self.components = None self._set_components() # Only used in copy() self.relationships = Relationships(self, model=model) # financials must follow relationships in __init__ because we set the # period on financials, an the period is retrieved from model, which # is stored on relationships. self.financials = None self.set_financials(fins) # Attributes related to Path self._stage = None self.used = set() self.guide = Guide() self.interview = InterviewTracker() self.summary = None self.valuation = None # for monitoring, temporary storage for existing path and used sets self._path_archive = list() self._used_archive = list() self.cap_table = CapTable()
def from_database(cls, data, statement): """ LineItem.from_database() -> None **CLASS METHOD** Method deserializes all LineItems belonging to ``statement``. """ # first pass: create a dict of lines new = cls(parent=None, ) new.tags = Tags.from_database(data['tags']) id_str = data['driver_id'] if id_str: new._driver_id = ID.from_database(id_str).bbid # defer resolution of .xl new.xl_data = xl_mgmt.LineData(new) new.xl_format = xl_mgmt.LineFormat.from_database(data['xl_format']) new.summary_type = data['summary_type'] new.summary_count = data['summary_count'] new._consolidate = data['consolidate'] new._replica = data['replica'] new._include_details = data['include_details'] new._sum_details = data['sum_details'] new.log = data['log'] new.guide = Guide.from_database(data['guide']) new.id.bbid = ID.from_database(data['bbid']).bbid position = data['position'] position = int(position) if position else None new.position = position workspace = data.get('workspace', None) if workspace and workspace != 'null': new.workspace.update(workspace) usage = data.get('usage', None) if usage and usage != 'null': new.usage = LineItemUsage.from_database(usage) old_magic_keys = { "kpi", "covenants", "financials", "overall", "business summary" } if 'show on report' in new.tags.all or (new.tags.all & old_magic_keys): new.usage.show_on_report = True if "business summary" in new.tags.all: new.usage.show_on_card = True if 'monitor' in new.tags.all: new.usage.monitor = True return new
def __init__(self, model): self.model = model self.bu_directory = dict() self.ty_directory = dict() self.id = ID() self.id.set_namespace(model.id.bbid) self.id.assign(seed='taxonomy directory')
def __init__(self, name=None, spacing=100, parent=None, period=None): TagsMixIn.__init__(self, name) self._consolidated = False self._details = dict() self.relationships = Relationships(self, parent=parent) self.POSITION_SPACING = max(1, int(spacing)) self.id = ID() # does not get its own bbid, just holds namespace self._restricted = False # whether user can modify structure, self._period = period
def __init__(self, name=None, priority=DEFAULT_PRIORITY_LEVEL, quality=DEFAULT_QUALITY_REQUIREMENT): PrintAsLine.__init__(self) TagsMixIn.__init__(self, name) self.guide = Guide(priority, quality) self.relationships = Relationships(self) self.id = ID()
class Step(PrintAsLine, TagsMixIn): """ Class for tracking logical steps. Has the tags and guide interface of LineItem but doesn't commit to a numeric value. Pretty lightweight and flexible. ==================== ====================================================== Attribute Description ==================== ====================================================== DATA: guide instance of Guide relationships instance of Relationships class FUNCTIONS: pre_format() sets instance.formatted to a line with a checkbox ==================== ====================================================== """ DEFAULT_PRIORITY_LEVEL = 1 DEFAULT_QUALITY_REQUIREMENT = 5 def __init__(self, name=None, priority=DEFAULT_PRIORITY_LEVEL, quality=DEFAULT_QUALITY_REQUIREMENT): PrintAsLine.__init__(self) TagsMixIn.__init__(self, name) self.guide = Guide(priority, quality) self.relationships = Relationships(self) self.id = ID() def to_database(self, **kwargs): result = dict() result['tags'] = self.tags.to_database() result['guide'] = self.guide.to_database() return result @classmethod def from_database(cls, data): result = cls() result.tags = Tags.from_database(data['tags']) result.guide = Guide.from_database(data['guide']) return result def pre_format(self, **kargs): #custom formatting logic if self.tags.name: kargs["name"] = self.tags.name self.formatted = printing_tools.format_completed(self, **kargs) def register(self, namespace): self.id.set_namespace(namespace)
def __init__(self, name=None): TagsMixIn.__init__(self, name=name) self.conversion_table = dict() self.formula_bbid = None self.id = ID() self.parameters = Parameters() self.run_on_past = False # OBSOLETE self.workConditions = dict() self.active = True
def __init__(self, name): if not name: name = bb_settings.DEFAULT_MODEL_NAME TagsMixIn.__init__(self, name) # read-only attributes self._company = None self._fiscal_year_end = None self._processing_status = 'intake' self._ref_date = None self._started = False # container for holding Drivers self.drivers = DriverContainer() # set and assign unique ID - models carry uuids in the origin namespace self.id = ID() self.id.assign(name) # set up Portal data, this is used primarily by Wrapper self.portal_data = dict() self.portal_data['industry'] = None self.portal_data['summary'] = None self.portal_data['business_name'] = None self.portal_data['business_id'] = 99999999 self.portal_data['user_context'] = None self.portal_data['tags'] = None self.portal_data['update_count'] = 0 self.portal_data['monitoring'] = False self.report_summary = None self.taxo_dir = TaxoDir(model=self) self.taxonomy = Taxonomy(self.taxo_dir) self.timelines = dict() timeline = TimeLine(self) self.set_timeline(timeline) # business units self.target = None self.bu_directory = dict() self.ty_directory = dict() # scenarios parameters self.scenarios = dict() for s in DEFAULT_SCENARIOS: self.scenarios[s] = dict() self.transcript = list() self.topic_list = list()
def from_database(cls, portal_data, taxo_dir): """ Taxonomy.from_database() -> TaxoDir --``portal_data`` is a dictionary containing serialized TaxoDir data --``model`` is the Model instance the new TaxoDir will be attached to Method deserializes TaxoDir into a rich object from flat portal data. """ new = cls(taxo_dir) for taxo_unit in portal_data: key_list = taxo_unit.pop('keys') temp = new for k in key_list: this_dict = temp if k in this_dict: temp = dict.__getitem__(this_dict, k) else: dict.__setitem__(this_dict, k, cls(taxo_dir)) temp = dict.__getitem__(this_dict, k) id_hex = taxo_unit['bbid'] bbid = ID.from_database(id_hex).bbid if id_hex else None bu = new.taxo_dir.get(bbid) dict.__setitem__(this_dict, k, bu) return new
def __init__(self, start_date, end_date, parent=None, **kargs): # TimePeriodBase.__init__(self, start_date, end_date, model=model) TagsMixIn.__init__(self) self.start = start_date self.end = end_date self.financials = dict() self.id = ID() self.relationships = Relationships(self) self.relationships.set_parent(parent) self.past_end = None self.next_end = None self.parameters = Parameters() self.unit_parameters = Parameters() self._line_item_storage = dict() # {"value": value of any primitive type, # "xl_data": flat LineData object without styles info, # "hardcoded": bool} self.complete = True self.periods_used = 1
def __init__(self, name=None, value=None, parent=None, period=None): BaseFinancialsComponent.__init__(self, name=name, parent=parent, period=period) self._local_value = None self.guide = Guide(priority=3, quality=1) self.log = [] self.position = None # summary_type determines how the line is summarized self.summary_type = 'sum' # summary_count is used for summary_type == 'average' self.summary_count = 0 self._consolidate = True self._replica = False self._hardcoded = False self._include_details = True self._sum_details = True self.id = ID() self._driver_id = None if value is not None: self.set_value(value, self.SIGNATURE_FOR_CREATION) self.workspace = dict() self.usage = LineItemUsage() self.xl_data = xl_mgmt.LineData(self) self.xl_format = xl_mgmt.LineFormat()
def from_database(cls, portal_data, statement): target = portal_data.pop('target') line_item = LineItem.from_database(portal_data, statement) new = cls(None) new.__dict__.update(line_item.__dict__) new.target = ID.from_database(target).bbid return new
def __init__(self, model, resolution='monthly', name='default', interval=1): dict.__init__(self) self.id = ID() # Timeline objects support the id interface and pass the model's id # down to time periods. The Timeline instance itself does not get # its own bbid. self.model = model self.resolution = resolution self.name = name self.interval = interval self.master = None self.parameters = Parameters() self.has_been_extrapolated = False self.ref_date = None self.id.set_namespace(model.id.bbid)
def from_database(cls, portal_data): new = cls() new.tags = Tags.from_database(portal_data.get('tags')) new.conversion_table = portal_data.get('conversion_table') new.formula_bbid = ID.from_database( portal_data.get('formula_bbid')).bbid new.parameters = Parameters.from_database( portal_data.get('parameters'), target='parameters') new.run_on_past = portal_data.get('run_on_past', False) formula = cls._FM.local_catalog.issue(new.formula_bbid) new.id.set_namespace(formula.id.namespace) new.id.assign(new.name or formula.tags.name) return new
def process(self, message, *pargs, **kargs): """ Analyst.process(message) -> message Method works to improve the model until it's either (i) good enough to stop work altogether or (ii) a question for the user comes up and the Engine needs to pause work. """ n = 0 message = self.choose_direction(message, *pargs, **kargs) # use choose_direction() to for substantive work. method also weeds # out messages that are ready for portal delivery right away. yenta.diary.clear() while self.status in [TOPIC_NEEDED, PENDING_RESPONSE]: # model = message[0] # if self.status == PENDING_RESPONSE: topic_bbid_hex = model.transcript[-1][0]["topic_bbid"] topic_bbid = ID.from_database(topic_bbid_hex).bbid topic = yenta.TM.local_catalog.issue(topic_bbid) logger.info('{} {}'.format(self.status, topic.source)) message = topic.process(message) # elif self.status == TOPIC_NEEDED: topic = yenta.select_topic(model) if topic: logger.info('{} {}'.format(self.status, topic.source)) message = topic.process(message) else: pass # Yenta.select_topic() returned None for Topic, which means # it couldn't find any matches in the Topic Catalog. In such # an event, Yenta notes dry run on focal point and IC shifts # to the next focal point message = self.choose_direction(message, *pargs, **kargs) # the engine has done more work on the model. use choose_direction() # to see if it can stop or needs to continue. # n = n + 1 if n > self.max_cycles: break # circuit-breaker logic # return message
def find_line(self, line_id, statement_attr): """ Financials.find_line() -> LineItem --``line_id`` bbid of line Finds a LineItem across all statements by its bbid. """ if isinstance(line_id, str): line_id = ID.from_database(line_id).bbid statement = self.get_statement(statement_attr) if statement: for line in statement.get_full_ordered(): if line.id.bbid == line_id: return line raise bb_exceptions.StructureError( 'Could not find line with id {}'.format(line_id) )
def copy(self, clean=False): """ Financials.copy() -> Financials Return a deep copy of instance. Method starts with a shallow copy and then substitutes deep copies for the values of each attribute in instance.ORDER """ new_instance = Financials() new_instance._full_order = self._full_order.copy() for key, stmt in self._statement_directory.items(): new_statement = stmt.copy(clean=clean) new_statement.relationships.set_parent(new_instance) new_instance._statement_directory[key] = new_statement new_instance.id = ID() new_instance.register(self.id.namespace) return new_instance
def from_database(cls, portal_data, model, **kargs): """ TimeLine.from_database(portal_data) -> TimeLine **CLASS METHOD** Method extracts a TimeLine from portal_data. """ if isinstance(portal_data['period_start'], str): period_start = date_from_iso(portal_data['period_start']) period_end = date_from_iso(portal_data['period_end']) else: period_start = portal_data['period_start'] period_end = portal_data['period_end'] new = cls(period_start, period_end) new.complete = portal_data.get('complete') or False new.periods_used = portal_data.get('periods_used') or 1 new.parameters.add( Parameters.from_database(portal_data['parameters'], target='parameters')) new._inflate_line_storage(portal_data['financials_values']) # convert unit_parameters keys to UUID for k, v in Parameters.from_database(portal_data['unit_parameters'], target='unit_parameters').items(): new.unit_parameters.add({ID.from_database(k).bbid: v}) time_line = kargs['time_line'] time_line.add_period(new) return new
def __init__(self): self.id = ID() self.directory = dict() self.by_name = dict()
def from_database(cls, portal_data): new = cls() new.__dict__.update(portal_data) new.used = [ID.from_database(id).bbid for id in portal_data['used']] return new
class BusinessUnit(TagsMixIn, Equalities): """ Object describes a group of business activity. A business unit can be a store, a region, a product, a team, a relationship (or many relationships). ==================== ====================================================== Attribute Description ==================== ====================================================== DATA: cap_table instance of CapTable class complete bool; if financials are complete for unit in period components instance of Components class, stores business units filled bool; True if fill_out() has run to completion financials instance of Financials object guide instance of Guide object id instance of ID object interview instance of InterviewTracker object life instance of Life object location placeholder for location functionality parameters flexible storage for data that shapes unit performance relationships instance of Relationships class size int; number of real-life equivalents obj represents stage property; returns non-public stage or interview type str or None; unit's in-model type (e.g., "team") used set; contains BBIDs of used Topics FUNCTIONS: add_component() adds unit to instance components add_driver() registers a driver archive_path() archives existing path then sets to blank Statement archive_used() archives existing used topics and sets to blank set compute() consolidates and derives a statement for all units copy() create a copy of this BusinessUnit instance fill_out() runs calculations to fill out financial statements get_current_period() returns current period on BU.model.time_line get_financials() returns Financials from a period or creates a new one get_parameters() returns Parameters from TimeLine, Period, self kill() make dead, optionally recursive make_past() put a younger version of financials in prior period recalculate() reset financials, compute again, repeat for future reset_financials() resets instance and (optionally) component financials set_financials() attaches a Financials object from the right template synchronize() set components to same life, optionally recursive ==================== ====================================================== """ irrelevantAttributes = [ "all", "filled", "guide", "id", "parent", "part_of" ] _UPDATE_BALANCE_SIGNATURE = "Update balance" def __init__(self, name, fins=None, model=None): TagsMixIn.__init__(self, name) self._parameters = Parameters() self._type = None self.id = ID() self.life = LifeCycle() self.location = None self.size = 1 self.xl = xl_mgmt.UnitData() self.components = None self._set_components() # Only used in copy() self.relationships = Relationships(self, model=model) # financials must follow relationships in __init__ because we set the # period on financials, an the period is retrieved from model, which # is stored on relationships. self.financials = None self.set_financials(fins) # Attributes related to Path self._stage = None self.used = set() self.guide = Guide() self.interview = InterviewTracker() self.summary = None self.valuation = None # for monitoring, temporary storage for existing path and used sets self._path_archive = list() self._used_archive = list() self.cap_table = CapTable() @classmethod def from_database(cls, portal_data, link_list=list()): new = cls(None) new.tags = Tags.from_database(portal_data['tags']) new._parameters = Parameters.from_database(portal_data['parameters'], target='parameters') new._type = portal_data['type'] new.life = LifeCycle.from_database(portal_data['life']) new.location = portal_data['location'] new.size = portal_data['size'] ids = portal_data['used'] real_ids = [ID.from_database(id).bbid for id in ids] new.used = set(real_ids) new.guide = Guide.from_database(portal_data['guide']) new.components = [ ID.from_database(id).bbid for id in portal_data['components'] ] new.interview = InterviewTracker.from_database( portal_data['interview'], link_list) new.summary = None # Obsolete new.valuation = None # Obsolete new.stage = None # Obsolete new._path_archive = portal_data['path_archive'] # don't bother reinfl- # ating archived paths, they won't be used new._used_archive = portal_data['used_archive'] fins = portal_data.get('financials_structure') new_fins = Financials.from_database(fins, new, period=None) new.set_financials(new_fins) new.cap_table = CapTable.from_database(portal_data['cap_table']) return new def to_database(self, taxonomy=False): data = dict() data['parameters'] = list( self._parameters.to_database(target='parameters')) data['type'] = self._type data['components'] = [k.hex for k in self.components.keys()] data['bbid'] = self.id.bbid.hex data['life'] = self.life.to_database() data['location'] = self.location data['name'] = self.name data['title'] = self.title data['size'] = self.size data['tags'] = self.tags.to_database() data['financials_structure'] = self.financials.to_database() data['stage'] = None data['used'] = [id.hex for id in self.used] data['guide'] = self.guide.to_database() data['interview'] = self.interview.to_database() data['summary'] = None # Obsolete data['valuation'] = None # Obsolete # for monitoring, temporary storage for existing path and used sets data['path_archive'] = self._path_archive data['used_archive'] = self._used_archive data['taxonomy'] = taxonomy data['cap_table'] = self.cap_table.to_database() return data @property def parameters(self): params = self._parameters try: period_params = self.period.unit_parameters[self.id.bbid] except AttributeError: pass except KeyError: pass else: params.update(period_params) return params @property def stage(self): """ **property** When instance._stage points to a True object, property returns the object. Otherwise property returns model.interview. Since the default value for instance._path is None, property starts out with a ``pass-through``, backwards-compatible value. Setter sets _stage to value. Deleter sets _stage to None to restore default pass-through state. """ result = self._stage if not result: result = self.interview return result @stage.setter def stage(self, value): self._stage = value @stage.deleter def stage(self): self._stage = None @property def type(self): """ **property** Getter returns instance._type. Setter registers instance bbid under the new value key and removes old registration (when instance.period is defined). Deletion prohibited. """ return self._type @type.setter def type(self, value): """ **property** Sets ._type. Updates ty_directory when type is changed. """ old_type = self.type self._type = value model = self.relationships.model if model: in_model = False in_taxonomy = False # Determine if bu is in model or taxo_dir if self.id.bbid in model.bu_directory: if self is model.bu_directory[self.id.bbid]: ty_directory = model.ty_directory in_model = True if self.id.bbid in model.taxo_dir.bu_directory: if self is model.taxo_dir.bu_directory[self.id.bbid]: ty_directory = model.taxo_dir.ty_directory in_taxonomy = True if in_model and in_taxonomy: print(self.name + " is both model.bu_dir and taxo_dir!!") raise bb_exceptions.BBAnalyticalError if in_model or in_taxonomy: if old_type in ty_directory: old_id_set = ty_directory.get(old_type) old_id_set.remove(self.id.bbid) # Remove sets if they are empty if len(old_id_set) == 0: ty_directory.pop(old_type) new_id_set = ty_directory.setdefault(value, set()) new_id_set.add(self.id.bbid) # Entries are sets of bbids for units that belong to that type @type.deleter def type(self): c = "``type`` is a property; delete prohibited. to represent generic " c += "unit, set to None instead." raise bb_exceptions.ManagedAttributeError(c) def __hash__(self): return self.id.__hash__() def __str__(self, lines=None): """ BusinessUnit.__str__() -> str Method concatenates each line in ``lines``, adds a new-line character at the end, and returns a string ready for printing. If ``lines`` is None, method calls views.view_as_unit() on instance. """ # Get string list, slap a new-line at the end of every line and return # a string with all the lines joined together. if not lines: lines = views.view_as_base(self) # Add empty strings for header and footer padding lines.insert(0, "") lines.append("") box = "\n".join(lines) return box def add_component(self, bu, update_id=True, register_in_dir=True, overwrite=False): """ BusinessUnit.add_component() -> None Method prepares a bu and adds it to instance components. Method always sets bu namespace_id to instance's own namespace_id. If ``updateID`` is True, method then assigns a new bbid to the instance. Method raises IDNamespaceError if the bu's bbid falls outside the instance's namespace id. This is most likely if updateID is False and the bu retains an old bbid from a different namespace (e.g., when someone inserts a business unit from one model into another without updating the business unit's bbid). If register_in_dir is true, method raises IDCollisionError if the period's directory already contains the new business unit's bbid. If all id verification steps go smoothly, method delegates insertion down to Components.add_item(). Usage Scenarios: Adding a newly created ChildBU: ParentBU.add_component(ChildBU, True, True) Transferring a ChildBU from one parent to another: ParentBU2.add_component(ChildBU, False, False) """ bu.valuation = None bu.relationships.set_model(self.relationships.model) # Step 1: optionally update ids. if update_id: bu._update_id(namespace=self.id.bbid, recur=True) # Step 2: Add component self.components.add_item(bu) # Step 3: Register the units. Will raise errors on collisions. bu._register_in_dir(recur=True, overwrite=overwrite) def remove_component(self, buid): """ BusinessUnit.add_component() -> None """ # Step 1: remove from directories mo = self.relationships.model bu = self.components[buid] mo.ty_directory[bu.type] -= {bu.id.bbid} mo.bu_directory.pop(bu.id.bbid) # Step 2: remove component self.components.remove_item(buid) def archive_path(self): """ BusinessUnit.archive_path() -> None Method archives existing path to BusinessUnit._path_archive and sets the path to a clean Statement(). Method is used by monitoring to pre-process before setting the monitoring path. """ if self.stage.path is not None: self._path_archive.append(self.stage.path.to_database()) new_path = Statement() self.stage.set_path(new_path) def archive_used(self): """ BusinessUnit.archive_used() -> None Method archives existing set of used topics to BusinessUnit._used_archive and sets used to a new empty set. Method is used by monitoring to pre-process before setting the monitoring path. """ used = [id.hex for id in self.used] self._used_archive.append(used) self.used = set() def check_statement_structure(self, statement_name, period=None): if not period: period = self.relationships.model.get_timeline().current_period struct_stmt = self.financials.get_statement(statement_name) struct_stmt = struct_stmt.copy(clean=True) fins = self.get_financials(period) fins._statement_directory[statement_name.casefold()] = struct_stmt struct_stmt.relationships.set_parent(fins) struct_stmt.set_period(period) def compute(self, statement_name, period=None): """ BusinessUnit.compute() -> None --``statement`` name of statement to operate on Method recursively runs consolidation and derivation logic on statements for instance and components. """ if not period: period = self.relationships.model.get_timeline().current_period for unit in self.components.get_all(): unit.compute(statement_name, period=period) self._consolidate(statement_name, period) self._derive(statement_name, period) def consolidate_fins_structure(self): for unit in self.components.get_all(): unit.consolidate_fins_structure() for statement in self.financials.full_order: self._consolidate(statement, period=None, struct=True) self.relationships.model.clear_fins_storage() def copy(self): """ BU.copy() -> BU Method returns a new business unit that is a deep-ish copy of the instance. The new bu starts out as a shallow Tags.copy() copy of the instance. The method then sets the following attributes on the new bu to either deep or class-specific copies of the instance values: - components - financials - header.profile - id (vanilla shallow copy) - life The class-specific copy methods for components, drivers, and financials all return deep copies of the object and its contents. See their respective class documentation for mode detail. """ result = copy.copy(self) result.tags = self.tags.copy() result.relationships = self.relationships.copy() # Start with a basic shallow copy, then add tags # r_comps = self.components.copy() result._set_components(r_comps) r_fins = self.financials.copy() result.set_financials(r_fins) result.id = copy.copy(self.id) result.guide = copy.deepcopy(self.guide) # Have to make a deep copy of guide because it is composed of Counter # objects. Guide shouldn't point to a business unit or model result.life = self.life.copy() result.summary = None result.valuation = None result._parameters = self._parameters.copy() result._stage = None result.used = set() r_interview = self.interview.copy() result.interview = r_interview return result def fill_out(self, period=None): """ BusinessUnit.fill_out() -> None Method is the driver for filling out instance financials. Will no-op if instance.filled is True. Otherwise, will consolidate and derive overview, income, and cash statements for the instance and its components. Then, will process balance sheets. At conclusion, method sets instance.filled to True to make sure that subsequent calls do not increment existing values. NOTE: consolidate() blocks derive() on the same lineitem. Once a non-None value is written into a Lineitem at one component, BusinessUnit.derive() will never run again for that LineItem, either at that component or any parent or ancestor of that component. """ financials = self.get_financials(period) # Designate which statements to compute in between balance sheet ops # And which to compute after all ops mid_compute = list() end_compute = list() use_list = mid_compute for statement in financials.full_ordered: if not statement: continue if statement.balance_sheet: use_list = end_compute continue if statement.compute: use_list.append(statement.name) if not period: period = self.get_current_period() self._load_starting_balance(period) for statement in mid_compute: self.compute(statement, period) self._compute_ending_balance(period) self._check_start_balance(period) for statement in end_compute: self.compute(statement, period) def kill(self, date=None, recur=True): """ BusinessUnit.kill() -> None Enters a death event on the specified date. Also enters a ``killed`` event. If ``date`` is None, uses own ref_date. If ``recur`` is True, repeats for all components. """ if date is None: date = self.life.ref_date self.life.events[self.life.KEY_DEATH] = date self.life.events[common_events.KEY_KILLED] = date if recur: for unit in self.components.values(): unit.kill(date, recur) def make_past(self, overwrite=False): """ BusinessUnit.make_past() -> None --``overwrite``: if True, will replace existing instance.past Create a past for instance. Routine operates by making an instance copy, fitting the copy to the n-1 period (located at instance.period.past), and then recursively linking all of the instance components to their younger selves. """ model = self.relationships.model period = model.get_timeline().current_period self.get_financials(period.past) for bu in self.components.get_all(): bu.make_past() def recalculate(self, adjust_future=True, period=None): """ BusinessUnit.recalculate () -> None Recalculate instance finanicals. If ``adjust_future`` is True, will repeat for all future snapshots. """ period.clear() # self.reset_financials(period=period) self.fill_out(period=period) if adjust_future and period and period.future: self.recalculate(adjust_future=True, period=period.future) def recompute(self, statement_name, period=None, adjust_future=True): """ BusinessUnit.recompute () -> None --``statement_name`` is the string name of the statement attribute on financials --``period`` is an instance of TimePeriod --``adjust_future`` is a boolean, whether to run in future periods Recompute a particular statement on financials. If ``adjust_future`` is True, will repeat for all future snapshots. """ self.reset_statement(statement_name, period=period) self.compute(statement_name, period=period) if adjust_future and period and period.future: self.recompute(statement_name, adjust_future=adjust_future, period=period.future) def reset_financials(self, period=None, recur=True): """ BusinessUnit.reset_financials() -> None Method resets financials for instance and, if ``recur`` is True, for each of the components. Method sets instance.filled to False. """ # self.filled = False financials = self.get_financials(period) financials.reset() if recur: pool = self.components.get_all() for bu in pool: bu.reset_financials(period=period, recur=recur) def reset_statement(self, statement_name, period=None, recur=True): """ BusinessUnit.reset_statement() -> None --``statement_name`` is the string name of the statement attribute on financials --``period`` is an instance of TimePeriod --``recur`` is a bool; whether to reset components Method resets the given statement for this unit and optionally each of its components. """ fins = self.get_financials(period) statement = fins.get_statement(statement_name) statement.reset() if recur: for unit in self.components.get_all(): unit.reset_statement(statement_name, period=period) def set_financials(self, fins=None): """ BusinessUnit.set_financials() -> None Method for initializing instance.financials with a properly configured Financials object. Method will set instance financials to ``fins``, if caller specifies ``fins``. Otherwise, method will set financials to a new Financials instance. """ if fins is None: fins = Financials(parent=self) fins.relationships.set_parent(self) self.financials = fins def set_name(self, name): self.tags.set_name(name) mo = self.relationships.model if mo: if self.id.bbid in mo.bu_directory: mo.set_company(mo.get_company()) elif self.id.bbid in mo.taxo_dir.bu_directory: self._update_id(self.id.namespace) mo.taxo_dir.refresh_ids() else: self._update_id(self.id.namespace) else: self._update_id(self.id.namespace) def synchronize(self, recur=True): """ BusinessUnit.synchronize() -> None Set life on all components to copy of caller. If ``recur`` is True, repeat all the way down. """ for unit in self.components.values(): unit.life = self.life.copy() if recur: unit.synchronize() def get_current_period(self): """ BusinessUnit.get_current_period() -> TimePeriod Convenience method to get current_period from parent model's default timeline. """ model = self.relationships.model if model: return model.get_timeline().current_period def get_financials(self, period=None): """ BusinessUnit.get_financials() -> Financials() --``period`` TimePeriod Returns this BUs financials in a given period. If no financials exist, creates a new financials with same structure Stores financials in period.financials dict, keyed by BU.id.bbid """ model = self.relationships.model now = model.get_timeline().current_period if model else None if not period: period = now if period is None: c = "PERIOD IS NONE!!!!! CANNOT GET FINANCIALS" raise ValueError(c) if self.id.bbid in period.financials: # the best case we expect: financials have been assigned to a period fins = period.financials[self.id.bbid] else: # make sure balance sheets have matching structures self.financials.check_balance_sheets() fins = self.financials.copy(clean=True) fins.relationships.set_parent(self) fins.period = period fins.populate_from_stored_values(period) fins.restrict() for statement in fins.full_ordered: if statement: for line in statement._details.values(): line.update_stored_value(recur=False) period.financials[self.id.bbid] = fins return fins def get_parameters(self, period=None): """ BusinessUnit.get_financials() -> Financials() --``period`` TimePeriod Method combines all parameters from reachable sources in order of precedence. Driver updates the results with own parameters. """ if not period: period = self.get_current_period() time_line = period.relationships.parent # Specific parameters trump general ones. Start with time_line, then # update for period (more specific). Driver trumps with own parameters. params = dict() if hasattr(time_line, 'parameters'): params.update(time_line.parameters) if hasattr(period, 'parameters'): params.update(period.parameters) params.update(self._parameters) if hasattr(period, 'unit_parameters'): params.update(period.unit_parameters.get(self.id.bbid, {})) return params # *************************************************************************# # NON-PUBLIC METHODS # # *************************************************************************# def _check_start_balance(self, period): """ BusinessUnit._check_start_balance() -> None Compares starting and ending balances. Adds missing lines to starting balance to keep layout consistent. """ pool = self.components.get_all() for unit in pool: unit._check_start_balance(period) financials = self.get_financials(period) for end_line in financials.ending.get_ordered(): start_line = financials.starting.find_first(end_line.name) if start_line: self._check_line(start_line, end_line) else: new_line = end_line.copy() new_line.clear(force=True) financials.starting.add_line(new_line, position=end_line.position, noclear=True) def _check_line(self, start_line, end_line): """ BusinessUnit._check_line() -> None Compares starting and ending balances. Adds missing lines to starting balance to keep layout consistent. """ if end_line._details: for end in end_line.get_ordered(): start = start_line.find_first(end.name) if start: self._check_line(start, end) else: new_line = end.copy() new_line.clear(force=True) start_line.add_line(new_line, position=end.position, noclear=True) def _compute_ending_balance(self, period): """ BusinessUnit._compute_ending_balance() -> None Method recursively fills out balance sheets for instance and components. Method adjusts shape of ending and starting balance sheets, runs consolidation logic, updates balance sheets, then runs derivation logic. """ for unit in self.components.get_all(): unit._compute_ending_balance(period) financials = self.get_financials(period) self._consolidate("ending", period) self._update_balance(period) # Sets ending balance lines to starting values by default self._derive("ending", period) # Derive() will overwrite ending balance sheet where appropriate def _consolidate(self, statement_name, period=None, struct=False): """ BusinessUnit.consolidate() -> None Method iterates through instance components in order and consolidates each living component into instance using BU.consolidate_unit() """ pool = self.components.get_all() # Need stable order to make sure we pick up peer lines from units in # the same order. Otherwise, their order might switch and financials # would look different (even though the bottom line would be the same). for unit in pool: self._consolidate_unit(unit, statement_name, period, struct=struct) def _consolidate_unit(self, sub, statement_name, period=None, struct=False): """ BusinessUnit.consolidate_unit() -> None -- ``sub`` should be a BusinessUnit object --``statement_name`` is the name of the financial statement to work on Method consolidates value of line items from sub's statement to same statement in instance financials. Usually sub is a constituent/component of the instance, but this is not mandatory. Method delegates to Statement.increment() for actual consolidation work. """ # Step Only: Actual consolidation if struct: sub_fins = sub.financials top_fins = self.financials else: sub_fins = sub.get_financials(period) top_fins = self.get_financials(period) sub_statement = sub_fins.get_statement(statement_name) top_statement = top_fins.get_statement(statement_name) xl_only = True if period: if sub.life.conceived(period): xl_only = False if struct: xl_only = False if sub_statement and top_statement: top_statement.increment( sub_statement, consolidating=True, xl_only=xl_only, xl_label=sub.title, ) def _derive(self, statement_name, period=None): """ BusinessUnit.derive() -> None --``statement_name`` is the name of the financial statement to work on Method walks through lines in statement and delegates to BusinessUnit._derive_line() for all substantive derivation work. """ financials = self.get_financials(period) this_statement = financials.get_statement(statement_name) if this_statement: for line in this_statement.get_ordered(): self._derive_line(line, period) def _derive_line(self, line, period=None): """ BusinessUnit.derive_line() -> None --``line`` is the LineItem to work on Method computes the value of a line using drivers stored on the instance. Method builds a queue of applicable drivers for the provided LineItem. Method then runs the drivers in the queue sequentially. Each LineItem gets a unique queue. Method will not derive any lines that are hardcoded or have already been consolidated (LineItem.hardcoded == True or LineItem.has_been_consolidated == True). """ # Repeat for any details if line._details: for detail in line.get_ordered(): if detail.replica: continue # Skip replicas to make sure we apply the driver only once # A replica should never have any details else: self._derive_line(detail, period) # look for drivers based on line name, line parent name, all line tags driver = line.get_driver() if driver: driver.workOnThis(line, bu=self, period=period) def _load_starting_balance(self, period): """ BusinessUnit._load_balance() -> None Connect starting balance sheet to past if available, copy shape to ending balance sheet. """ pool = self.components.get_ordered() # Need stable order to make sure we pick up peer lines from units in # the same order. Otherwise, their order might switch and financials # would look different (even though the bottom line would be the same). for unit in pool: unit._load_starting_balance(period) if period.past: before_fins = self.get_financials(period.past) period_fins = self.get_financials(period) period_fins.starting = before_fins.ending def _register_in_dir(self, recur=True, overwrite=True): """ BusinessUnit._register_in_dir() -> None Method updates the bu_directory on with (bbid:bu). Method does nothing if BU is not connected to the Model yet. If ``recur`` == True, repeats for every component in instance. If ``overwrite`` == False, method will raise an error if any of its component bbids is already in the period's bu_directory at the time of call. NOTE: Method will raise an error only if the calling instance's own components have ids that overlap with the bu_directory. To the extent any of the caller's children have an overlap, the error will appear only when the recursion gets to them. As a result, by the time the error occurs, some higher-level or sibling components may have already updated the period's directory. """ # UPGRADE-S: Can fix the partial-overwrite problem by refactoring this # routine into 2 pieces. build_dir(recur=True) would walk the tree and # return a clean dict. update_dir(overwrite=bool) would compare that # dict with the existing directory and raise an error if there is # an overlap. Also carries a speed benefit, cause only compare once. model = self.relationships.model if not model: # Do nothing if Business Unit is not part of the model yet. return # Default case bu_directory = model.bu_directory ty_directory = model.ty_directory # If _register_in_dir is called from taxonomy_template.add_component(bu) parent_bu_components = self.relationships.parent if parent_bu_components: # For every ChildBU, its parent is a Components object # You must go 2 parent levels up to get to ParentBU parent_bu = parent_bu_components.relationships.parent if parent_bu: if parent_bu.id.bbid in model.taxo_dir.bu_directory: bu_directory = model.taxo_dir.bu_directory ty_directory = model.taxo_dir.ty_directory if not overwrite: # Check for collisions first, then register if none arise. if self.id.bbid in bu_directory: c = ("bu_directory already contains an object with " "the same bbid as this unit. \n" "unit id: {bbid}\n" "known unit name: {name}\n" "new unit name: {mine}\n\n").format( bbid=self.id.bbid, name=bu_directory[self.id.bbid].tags.name, mine=self.tags.name, ) raise bb_exceptions.IDCollisionError(c) bu_directory[self.id.bbid] = self brethren = ty_directory.setdefault(self.type, set()) brethren.add(self.id.bbid) if recur: try: components = self.components.values() except AttributeError: components = list() for unit in components: unit._register_in_dir(recur, overwrite) def _set_components(self, comps=None): """ BusinessUnit._set_components() -> None Method sets instance.components to the specified object, sets object to point to instance as its parent. If ``comps`` is None, method generates a clean instance of Components(). """ if not comps: comps = Components() comps.relationships.set_parent(self) self.components = comps def _update_balance(self, period): """ BusinessUnit._load_balance() -> None Connect starting balance sheet to past if available, copy shape to ending balance sheet. """ financials = self.get_financials(period) starting_balance = financials.starting ending_balance = financials.ending # Method expects balance sheet to come with accurate tables. We first # build the table in load_balance(). We then run consolidate(), which # will automatically update the tables if it changes the statement. if starting_balance and ending_balance: for name, starting_line in starting_balance._details.items(): if starting_line.has_been_consolidated: continue elif starting_line.value is not None: ending_line = ending_balance.find_first(starting_line.name) self._update_lines(starting_line, ending_line) def _update_id(self, namespace, recur=True): """ BusinessUnit._update_id() -> None Assigns instance a new id in the namespace, based on the instance name. If ``recur`` == True, updates ids for all components in the parent instance bbid namespace. """ self.id.set_namespace(namespace) self.id.assign(self.tags.name) self.financials.register(namespace=self.id.bbid) # This unit now has an id in the namespace. Now pass our bbid down as # the namespace for all downstream components. if recur: try: components = self.components.values() except AttributeError: components = list() for unit in components: unit._update_id(namespace=self.id.bbid, recur=True) try: self.components.refresh_ids() except AttributeError: pass def _update_lines(self, start_line, end_line): """ BusinessUnit._update_lines() -> None Tool for BusinessUnit._update_balance(). Method recursively walks through top-level LineItem details from the starting balance sheet ``start_line`` and assigns their values to the matching line in the ending balance sheet ``end_line``. """ if start_line._details: for name, line in start_line._details.items(): ending_line = end_line.find_first(line.tags.name) self._update_lines(line, ending_line) else: if end_line.has_been_consolidated: pass elif start_line.value is not None: end_line.set_value(start_line.value, self._UPDATE_BALANCE_SIGNATURE)
class Driver(TagsMixIn): """ Drivers apply formulas to business units. 1) Each Driver applies one and only one formula. A Driver may apply the formula whenever the Driver works on a LineItem that satisfies that Driver instance's workConditions. To limit the size of extrapolated models, a Driver instance stores only a bbid for its formula, not the formula itself. When the Driver has to work, the Driver retrieves the formula from the formula catalog (managed by FormulaManager) and applies it to the relevant line and business unit. When the Driver calls a formula, the Driver provides the formula with the target LineItem, the Driver's parentObject and signature, and an arbitrary set of additional work parameters stored in Driver.data. Formulas generally compute the value of one LineItem based on data from other LineItems or BusinessUnit attributes. 2) Drivers work only on objects that satisfy that instance's workConditions. Drivers require the target object tags to contain **ALL** values specified in each workConditions key. A Driver does NOT require that the object contain ONLY the items specified in workConditions. Setting the value for a workCondition key to None means that all objects will match it. In other words, None is the absence of a workCondition. 3) A BusinessUnit may contain several different Drivers applicable to a given line. In such an event, a BusinessUnit will construct a queue of Drivers for the lineItem prior to running them. Drivers signal where they should appear in queue through their position attribute. A driver with a position of 10 will run before one with position == 20. Drivers do not need to have consecutive positions to run. Queues should generally be limited to 5 drivers per line. 4) A Topic should provide the Driver with a signature prior to injecting the Driver into a BusinessUnit. The driver signature should contain the Topic's descriptive information. Driver signatures should be informative to both machine and human readers. 5) A Topic should assign the driver an id within the Topic's namespace. BusinessUnits won't add a driver if the BU already contains a driver with the same id. 6) Topics and other modules should store any model-specific data used by the driver's formula in the driver's ``data`` dictionary. For example, if a formula relies on average ticket price to compute revenue, ``data`` should contain something like "atp" : 4. ==================== ====================================================== Attribute Description ==================== ====================================================== DATA: active bool; is instance turned on conversion_table dict; parameter name : formula argument data dict; place holder for driver-specific data id instance of ID formula_bbid bbid for formula that Driver applies position int; from 0 to 100 relationships instance of Relationships class run_on_past bool; default is False, whether to run driver in past signature string; how the driver signs lines it modifies workConditions dict; criteria for objects driver will process FUNCTIONS: __eq__ True for objects with the same work function __ne__ returns bool negative of __eq__ __hash__ returns hash of instance bbid, raises error if blank copy() returns a new instance w own objects in key places configure() set data and formula on instance in order setSignature() sets instance signature to object setWorkConditions() sets conditions for suitable objects workOnThis() gets and runs formula with instance data and sig validate() check that driver can perform computations ==================== ====================================================== """ _FM = None # We will connect the class to the FormulaManager at the bottom of the # module. Driver objects will then be able to pull formulas directly # from catalog. @classmethod def _disconnect(cls): cls._FM = None @classmethod def _set_formula_manager(cls, new_FM): cls._FM = new_FM def __init__(self, name=None): TagsMixIn.__init__(self, name=name) self.conversion_table = dict() self.formula_bbid = None self.id = ID() self.parameters = Parameters() self.run_on_past = False # OBSOLETE self.workConditions = dict() self.active = True def __eq__(self, comp, trace=False, tab_width=4): """ Driver.__eq__() -> bool Method returns True if instance and comp hash the same, False otherwise. """ result = False if hash(self) == hash(comp): result = True return result def __ne__(self, comp, trace=False, tab_width=4): """ Driver.__ne__() -> bool Method returns boolean negative of __eq__. """ result = not self.__eq__(comp, trace, tab_width) return result def __hash__(self): """ Driver.__hash__() -> obj Method returns the hash of the instance's bbid. """ if not self.id.bbid: c1 = "Driver.id.bbid blank. Driver expected to have filled bbid at" c2 = "runtime." c = c1 + c2 raise bb_exceptions.IDAssignmentError(c) return hash(self.id.bbid) @property def signature(self): return self.name @classmethod def from_database(cls, portal_data): new = cls() new.tags = Tags.from_database(portal_data.get('tags')) new.conversion_table = portal_data.get('conversion_table') new.formula_bbid = ID.from_database( portal_data.get('formula_bbid')).bbid new.parameters = Parameters.from_database( portal_data.get('parameters'), target='parameters') new.run_on_past = portal_data.get('run_on_past', False) formula = cls._FM.local_catalog.issue(new.formula_bbid) new.id.set_namespace(formula.id.namespace) new.id.assign(new.name or formula.tags.name) return new def to_database(self): data = dict() data['tags'] = self.tags.to_database() data['conversion_table'] = self.conversion_table data['formula_bbid'] = self.formula_bbid.hex data['parameters'] = list( self.parameters.to_database(target='parameters')) data['run_on_past'] = self.run_on_past return data def configure(self, data, formula, conversion_table=None): """ Driver.configure() -> None Configure instance for use. Steps: -- set instance conversion_table -- set instance parameters to data, with overwrite permissions -- set formula """ if conversion_table is not None: self.conversion_table = conversion_table self._set_parameters(data, overwrite=True) # Overwrite any existing parameters on configuration. Do this to # make sure topics that reconfigure the same driver template can run # multiple times. self._set_formula(formula) # get namespace for driver base = self.name or formula.tags.name self.id.set_namespace(formula.id.namespace) self.id.assign(seed=base) def copy(self): """ Driver.copy() -> Driver Method returns a new Driver object. Result is a shallow copy of instance except for the following attributes, which are deep: -- parameters -- workConditions Original object copies tags to result using Tags._copy_tags_to(), so method will enforce tag rules when specified. NOTE: Result points to same object as original on ``id`` and ``formula_bbid``. Therefore, the result should continue to point to the same formula even if that formula's id changes in place. The result should also track any in-place changes to original's id (e.g., change in namespace). NOTE2: Result points to the original conversion table. """ result = copy.copy(self) result.tags = self.tags.copy() result.parameters = copy.deepcopy(self.parameters) result.formula_bbid = copy.copy(self.formula_bbid) return result def validate(self, check_data=True, parent=None): """ Driver.validate() -> bool Check if instance is properly configured to do work. Returns False iff: -- instance points to a formula catalog with 0 keys -- instance does not have a formula_bbid -- the formula_bbid specified for instance is not in catalog -- instance and parent don't supply adequate data (also throws exception). """ result = True if len(self._FM.local_catalog.by_id) == 0: result = False if result: if not self.formula_bbid: result = False if result: if self.formula_bbid not in self._FM.local_catalog.by_id: result = False if check_data: result = self._check_data(parent) # Always check data, regardless of result. Function will throw # exception if the instance lacks required data for the its formula. return result def workOnThis(self, line, bu, period=None): """ Driver.workOnThis() -> None Method retries Driver's formula from the formula catalog. Method then applies the formula to line, with Driver's parentObject, data, and signature as context. Method is a no-op if instance is not active. """ tl = period.relationships.parent if tl is tl.model.time_line: if period is tl.current_period.past: if not self.run_on_past: return if all((not line.hardcoded, not line.has_been_consolidated, not (line.sum_details and line._details))): line.clear(recur=False) formula = self._FM.local_catalog.issue(self.formula_bbid) # formula_catalog.issue() only performs dict retrieval and # return for key. params = self._build_params(parent=bu, period=period) if not bb_settings.PREP_FOR_EXCEL: formula.func(line, bu, params, self.signature) else: output = formula.func(line, bu, params, self.signature) if not output.steps: c = ("Formula did not return all required information\n" "Name: {name}\n" "BBID: {bbid}\n" "Excel formula template missing!").format( name=formula.tags.name, bbid=self.formula_bbid, ) raise bb_exceptions.ExcelPrepError(c) data_cluster = self.to_excel() data_cluster.references = output.references data_cluster.name = formula.tags.name data_cluster.comment = output.comment data_cluster.formula = output.steps line.xl_data.add_derived_calculation(data_cluster) # Each function is "disposable", so we explicitly delete the # pointer after each use. del formula def to_excel(self): """ Driver.to_excel() -> DriverData Return a record set with instance parameters and conversion map. """ result = xl_mgmt.DriverData() result.conversion_map = self.conversion_table.copy() for param_name, param_value in self.parameters.items(): row = xl_mgmt.RowData() row[xl_mgmt.RowData.field_names.LABELS] = param_name row[xl_mgmt.RowData.field_names.VALUES] = param_value # UPGRADE / ISSUE: We need to find a way for managing parameters # whose values are containers or other mutable structures in Excel. result.rows.append(row) return result # *************************************************************************# # NON-PUBLIC METHODS # # *************************************************************************# def _build_params(self, parent, period=None): """ Driver._build_params() -> dict Prepare a parameter dictionary for the formula. Expects ``parent`` to be a business unit with a defined period pointer. Method builds its result by collating parameters from the parent, parent's time_line and instance. Specific parameters trump general ones. Method then converts uniquely named global parameters to standard formula arguments using instance.conversion_table. Result includes both the original and converted keys. """ if not period: period = parent.get_current_period() params = parent.get_parameters(period) params.update(self.parameters) converted = self._map_params_to_formula(params) params.update(converted) # Turn unique shared data into common variables that the formula can # understand. So a key like "lowest maintenance bid" becomes # "base annual expense". # extra info needed by formulas params['period'] = period return params def _check_data(self, parent=None): """ Driver._check_data() -> bool Check if instance and parent specify all required data for formula. Throw DefinitionError if that's not the case. """ result = False known_params = self._build_params(parent) formula = self._FM.local_catalog.issue(self.formula_bbid) for parameter in formula.required_data: if parameter not in known_params: c = "" c += "Instance data does not include required parameter ``%s``." c = c % (parameter) raise bb_exceptions.DefinitionError(c) break else: result = True return result def _map_params_to_formula(self, params, conversion_table=None): """ Driver._map_params_to_formula() -> dict Return a dictionary that maps values from ``params`` to keys in the conversion table. """ result = dict() if conversion_table is None: conversion_table = self.conversion_table for param_name, var_name in conversion_table.items(): result[var_name] = params[param_name] return result def _set_formula(self, F): """ Driver._set_formula() -> None Set instance.formula_bbid to that of the argument. """ if F.id.bbid: self.formula_bbid = F.id.bbid else: c = "" c += "Formula does not have valid bbid; bbid required for use in\n" c += "Driver." raise bb_exceptions.DefinitionError(c) def _set_parameters(self, new_data, overwrite=False): """ Driver._set_parameters() -> None Add new_data to instance.parameters. """ new_data = copy.deepcopy(new_data) self.parameters.add(new_data, overwrite) def setWorkConditions(self, *kwargs): # OBSOLETE pass
class TaxoDir: """ TaxoDir objects have a bu_directory and ty_directory designed for storing and searching for Taxonomy BusinessUnits. These directories will be separate from the directories in Model. ==================== ===================================================== Attribute Description ==================== ===================================================== DATA: model pointer to Model bu_directory dict; key = bbid, val = business units ty_directory dict; key = strings, val = sets of bbids id instance of ID class FUNCTIONS: add() method for adding templates to taxonomy directories clear() clears bu and ty directory get() method retrieves template with specified ID get_by_type() method retrieves templates of specified type get_tagged() return dict of units (by bbid) with specified tags register() method registers template in taxonomy directory ==================== ===================================================== """ def __init__(self, model): self.model = model self.bu_directory = dict() self.ty_directory = dict() self.id = ID() self.id.set_namespace(model.id.bbid) self.id.assign(seed='taxonomy directory') @classmethod def from_database(cls, portal_data, model, link_list): """ TaxoDir.from_database() -> TaxoDir --``portal_data`` is a dictionary containing serialized TaxoDir data --``model`` is the Model instance the new TaxoDir will be attached to Method deserializes TaxoDir into a rich object from flat portal data. """ new = cls(model) for unit in portal_data: tmp = BusinessUnit.from_database(unit, link_list) tmp._set_components() new.register(tmp) return new def to_database(self): """ TaxoDir.to_database() -> dict Method flattens TaxoDir into a serialized dictionary. """ taxo_list = list() for unit in self.bu_directory.values(): data = unit.to_database(taxonomy=True) taxo_list.append(data) return taxo_list def add(self, template, overwrite=False): """ TaxoDir.add() -> TaxoDir --``template`` is an instance of BusinessUnit to be used as a template unit --``overwrite`` bool; whether to overwrite existing template with matching bbid Method registers new template in TaxoDir, which adds the template to instance directories. """ self.register(template, update_id=True, overwrite=overwrite, recur=True) def clear(self): """ TaxoDir.clear() -> None Method resets instance directories. """ self.bu_directory = dict() self.ty_directory = dict() def get(self, bbid): """ TaxoDir.get() -> BusinessUnit --``bbid`` BBID of template to retrieve Return the template with specified bbid or None. """ template = self.bu_directory.get(bbid, None) return template def get_by_type(self, type): """ TaxoDir.get_by_type() -> dict --``type`` string; type of unit to retrieve Return a dictionary of units (by bbid) of the specified type. """ ids = self.ty_directory.get(type, set()) templates = dict() for bbid in ids: templates[bbid] = self.get(bbid) return templates def get_lowest_units(self, pool=None, run_on_empty=False): """ TaxoDir.get_lowest_units() -> list Method returns a list of units in pool that have no components. Method expects ``pool`` to be an iterable of bbids. If ``pool`` is None, method will build its own pool from all keys in the instance's bu_directory. Method will raise error if asked to run on an empty pool unless ``run_on_empty`` == True. NOTE: method performs identity check (``is``) for building own pool; accordingly, running a.select_bottom_units(pool = set()) will raise an exception. """ if pool is None: pool = sorted(self.bu_directory.keys()) else: pool = sorted(pool) #make sure to sort pool for stable output order # if any([pool, run_on_empty]): foundation = [] for bbid in pool: bu = self.bu_directory[bbid] if bu.components: continue else: foundation.append(bu) # return foundation # else: c = "``pool`` is empty, method requires explicit permission to run." raise bb_exceptions.ProcessError(c) def get_tagged(self, *tags, pool=None): """ TaxoDir.get_tagged() -> dict --``tags`` list of tags to search for on templates Return a dictionary of units (by bbid) that carry the specified tags. Delegates all selection work to tools.for_tag_operations.get_tagged() """ if not pool: pool = self.bu_directory.values() # We want a consistent order for the pool across run times pool = sorted(pool, key=lambda bu: bu.id.bbid) tagged_dict = tools.for_tag_operations.get_tagged(pool, *tags) return tagged_dict def refresh_ids(self): units = list(self.bu_directory.values()) self.clear() for unit in units: self.bu_directory[unit.id.bbid] = unit type_set = self.ty_directory.setdefault(unit.type, set()) type_set.add(unit.id.bbid) def register(self, bu, update_id=True, overwrite=False, recur=True): """ TaxoDir.register() -> None --``bu`` is an instance of BusinessUnit Manually add unit to TaxoDir. Unit will appear in directories. If bu has child units, those units will automatically register Generally it is better to avoid having child units in taxonomy """ if update_id: bu._update_id(namespace=self.id.bbid, recur=True) if not bu.id.bbid: c = "Cannot add content without a valid bbid." raise bb_exceptions.IDError(c) if not overwrite: # Check for collisions first, then register if none arise. if bu.id.bbid in self.bu_directory: c = ("TaxoDir.bu_directory already contains an object with " "the same bbid as this unit. \n" "unit id: {bbid}\n" "known unit name: {name}\n" "new unit name: {new_name}\n\n").format( bbid=self.id.bbid, name=self.bu_directory[bu.id.bbid].tags.name, new_name=bu.name, ) raise bb_exceptions.IDCollisionError(c) # Register the unit. self.bu_directory[bu.id.bbid] = bu # Setdefault returns dict[key] if value exists, or sets dict[key]=set() brethren = self.ty_directory.setdefault(bu.type, set()) brethren.add(bu.id.bbid) bu.relationships.set_model(self.model) if recur: for child_bu in bu.components.values(): self.register(child_bu, update_id=update_id, overwrite=overwrite, recur=recur)
class BaseFinancialsComponent(Equalities, TagsMixIn): """ A BaseFinancialComponent is a container that supports fast lookup and ordered views. CONTAINER: BaseFinancialComponents generally contain Lines, which may themselves contain additional Lines. You can use BaseFinancialComponent.find_first() or find_all() to locate the item you need from the top level of any statement. You can add details to a statement through add_line(). BaseFinancialComponents also support the append() and extend() list interfaces. You can easily combine statements by running stmt_a.increment(stmt_b). ORDER: BaseFinancialComponents provide ordered views of their contents on demand through get_ordered(). The ordered view is a list of details sorted by their relative position. Positions should be integers GTE 0. Positions can (and usually should) be non-consecutive. You can specify your own positions when adding a detail or have the BaseFinancialComponent automatically assign a position to the detail. You can change detail order by adjusting the .position attribute of any detail. BaseFinancialComponent automatically maintains and repairs order. If you add a line in an existing position, the BaseFinancialComponent will move the existing lines and all those behind it back. If you manually change the position of one line to conflict with another, BaseFinancialComponent will sort them in alphabetical order. RECURSION: You can view the full, recursive list of all the details in a statement by running stmt.get_full_ordered(). ==================== ====================================================== Attribute Description ==================== ====================================================== DATA: consolidated whether Statement has been consolidated POSITION_SPACING default distance between positions relationships instance of Relationships class FUNCTIONS: to_database() creates a flattened version of instance for Portal add_line() add line to instance append() add line to instance in final position copy() return deep copy extend() append multiple lines to instance in order find_all() return all matching items find_first() return the first matching item get_ordered() return list of instance details get_full_ordered() return recursive list of details increment() add data from another statement link_to() links statements in Excel reset() clear values set_period() sets period on instance and its details ==================== ====================================================== """ keyAttributes = ["_details"] # Should rename this comparable_attributes def __init__(self, name=None, spacing=100, parent=None, period=None): TagsMixIn.__init__(self, name) self._consolidated = False self._details = dict() self.relationships = Relationships(self, parent=parent) self.POSITION_SPACING = max(1, int(spacing)) self.id = ID() # does not get its own bbid, just holds namespace self._restricted = False # whether user can modify structure, self._period = period def __eq__(self, comparator, trace=False, tab_width=4): """ Statement.__eq__() -> bool Method explicitly delegates work to Equalities.__eq__(). The Financials class defines keyAttributes as an empty list at the class level, so Equalities will run pure-play list.__eq__() comparison logic, but still support tracing. """ return Equalities.__eq__(self, comparator, trace, tab_width) def __ne__(self, comparator, trace=False, tab_width=4): """ Statement.__ne__() -> bool Method explicitly delegates all work to Equalities. """ return Equalities.__ne__(self, comparator, trace, tab_width) def __str__(self): result = "\n" header = str(self.tags.name).upper() header = header.center(bb_settings.SCREEN_WIDTH) result += header result += "\n\n" if self._details: for line in self.get_ordered(): result += str(line) else: comment = "[intentionally left blank]" comment = comment.center(bb_settings.SCREEN_WIDTH) comment = "\n" + comment result += comment result += "\n\n" return result @property def has_been_consolidated(self): """ read-only property. """ return self._consolidated @property def model(self): if self.period: model = self.period.relationships.parent.model else: model = None return model @property def period(self): return self._period def to_database(self): """ BaseFinancialComponent.to_database() -> dict Method returns a serialized representation of a BaseFinancialsComponent """ parent_bbid = self.relationships.parent.id.bbid if \ self.relationships.parent else None row = { 'bbid': self.id.bbid.hex if self.id.bbid else None, 'parent_bbid': parent_bbid, 'name': self.name, 'title': self.title, 'tags': self.tags.to_database() } return row def add_line(self, new_line, position=None, noclear=False): """ BaseFinancialComponent.add_line() -> None Add line to instance at position. If ``position`` is None, method appends line. If position conflicts with existing line, method will place it at the requested position and push back all of the lines behind it by POSITION_SPACING. """ if not self._restricted: self._inspect_line_for_insertion(new_line) if position is None: self.append(new_line, noclear=noclear) else: new_line.position = position if not self._details: self._bind_and_record(new_line, noclear=noclear) # This block differs from append in that we preserve the # requested line position else: ordered = self.get_ordered() if new_line.position < ordered[0].position or ordered[ -1].position < new_line.position: self._bind_and_record(new_line, noclear=noclear) # Requested position falls outside existing range. No # conflict, insert line as is. else: # Potential conflict in positions. Spot existing, adjust as # necessary. for i in range(len(ordered)): existing_line = ordered[i] if new_line.position < existing_line.position: self._bind_and_record(new_line, noclear=noclear) break # If we get here, ok to insert as-is. New position # could only conflict with lines below the current # because we are going through the lines in order. # But we know that the line does not because the # conflict block breaks the loop. elif new_line.position == existing_line.position: # Conflict resolution block. tail = ordered[i:] for pushed_line in tail: pushed_line.position += self.POSITION_SPACING self._bind_and_record(new_line, noclear=noclear) break else: continue def add_line_to(self, line, *ancestor_tree, noclear=False): """ **OBSOLETE** Legacy interface for find_first() and add_line(). BaseFinancialComponent.add_line_to() -> None Method adds line to instance. ``ancestor_tree`` is a list of 1+ strings. The strings represent names of lines in instance, from senior to junior. Method adds line as a part of the most junior member of the ancestor tree. Method will throw KeyError if instance does not contain the ancestor tree in full. EXAMPLE: >>> F = Statement() >>> ... >>> print(F) revenue ............................None mens ............................None footwear .......................None >>> sandals = LineItem("sandals") >>> sandals.setValue(6, "example") >>> F.add_line_to(sandals, "revenue", "mens", "footwear") >>> print(F) revenue ............................None mens ............................None footwear .......................None sandals..........................6 """ if not self._restricted: if ancestor_tree: detail = self.find_first(*ancestor_tree) if detail is None: raise KeyError(ancestor_tree) else: detail.add_line(line, noclear=noclear) else: self.append(line, noclear=noclear) def add_top_line(self, line, after=None, noclear=False): """ **OBSOLETE** Legacy interface for add_line() BaseFinancialComponent.add_top_line() -> None Insert line at the top level of instance. Method expects ``after`` to be the name of the item after which caller wants to insert line. If ``after`` == None, method appends line to self. """ if not self._restricted: if after: new_position = self._details[ after].position + self.POSITION_SPACING self.add_line(line, new_position, noclear=noclear) else: self.append(line, noclear=noclear) def append(self, line, noclear=False): """ BaseFinancialComponent.append() -> None Add line to instance in final position. """ if not self._restricted: self._inspect_line_for_insertion(line) # Will throw exception if problem ordered = self.get_ordered() if ordered: last_position = ordered[-1].position else: last_position = 0 new_position = last_position + self.POSITION_SPACING line.position = new_position self._bind_and_record(line, noclear=noclear) def copy(self, check_include_details=False, clean=False): """ BaseFinancialComponent.copy() -> Statement Method returns a deep copy of the instance and any details. If ```` is True, copy will conform to ``out`` rules. """ result = copy.copy(self) if clean: result.set_period(None) result._consolidated = False result.tags = self.tags.copy() result.relationships = self.relationships.copy() # Tags.copy returns a shallow copy of the instance w deep copies # of the instance tag attributes. result._details = dict() # Clean dictionary if bb_settings.DEBUG_MODE: pool = self.get_ordered() else: pool = self._details.values() add_details = True if check_include_details: if not self.include_details: add_details = False if add_details: # Preserve relative order for own_line in pool: if check_include_details and not own_line.consolidate: continue cid = check_include_details new_line = own_line.copy(check_include_details=cid, clean=clean) result.add_line(new_line, position=own_line.position, noclear=True) return result def extend(self, lines, noclear=False): """ BaseFinancialComponent.extend() -> None lines can be either an ordered container or a Statement object """ if not self._restricted: try: for line in lines: self.append(line, noclear=noclear) except TypeError: for line in lines.get_ordered(): self.append(line, noclear=noclear) def find_all(self, *ancestor_tree, remove=False): """ BaseFinancialComponent.find_all() -> list Return a list of details that matches the ancestor_tree. The ancestor tree should be one or more strings naming objects in order of their relationship, from most junior to most senior. Method searches breadth-first within instance, then depth-first within instance details. If ``remove`` is True, method **removes** the result from its parent prior to delivery. NOTE: Use caution when removing items through this method, since you may have difficulty putting them back. For most removal tasks, find_first(remove=True) will offer significantly more comfort at a relatively small performance cost. """ ancestor_tree = [a.strip() for a in ancestor_tree] result = [] caseless_root_name = ancestor_tree[0].casefold() if remove: root = self._details.pop(caseless_root_name, None) # Pull root out of details else: root = self._details.get(caseless_root_name) # Keep root in details if root: remainder = ancestor_tree[1:] if remainder: lower_nodes = root.find_all(*remainder, remove=remove) if lower_nodes: result.extend(lower_nodes) else: # Nothing left, at the final node node = root result.append(node) else: for detail in self.get_ordered(): lower_nodes = detail.find_all(*ancestor_tree, remove=remove) if lower_nodes: result.extend(lower_nodes) continue return result def find_first(self, *ancestor_tree, remove=False): """ BaseFinancialComponent.find_first() -> Line or None Return a detail that matches the ancestor tree or None. The ancestor tree should be one or more strings naming objects in order of their relationship, from highest to lowest. So if "dogs" is part of "mammals", you can run stmt.find_first("mammals", "dogs"). If only one object named "bubbles" exists in the instance and any of its details, a call to stmt.find_first("bubbles") will return the same result. Method searches breadth-first within instance, then depth-first within instance details. If ``remove`` is True, method **removes** the result from its parent prior to delivery. NOTE: Use caution when removing items through this method, since you may have difficulty putting them back. The best way to reinsert an item you accidentally removed is to find its parent using detail.relationships.parent and insert the item directly back. """ ancestor_tree = [a.strip() for a in ancestor_tree] result = None caseless_root_name = ancestor_tree[0].casefold() if remove: root = self._details.pop(caseless_root_name, None) # Pull root out of details else: root = self._details.get(caseless_root_name) # Keep root in details if root: remainder = ancestor_tree[1:] if remainder: result = root.find_first(*remainder, remove=remove) else: result = root # Caller specified one criteria and we matched it. Stop work. else: for detail in self.get_ordered(): result = detail.find_first(*ancestor_tree, remove=remove) if result is not None: break else: continue return result def get_full_ordered(self): """ BaseFinancialComponent.get_full_ordered() -> list Return ordered list of lines and their details. Result will show lines in order of relative position depth-first. """ result = list() for line in self.get_ordered(): if getattr(line, '_details', None): # this allows Step objects (and others not having details) to # be held by statement, co-mingled with LineItems, primarily # for the purpose of the path result.append(line) increment = line.get_full_ordered() result.extend(increment) else: result.append(line) return result def get_ordered(self): """ BaseFinancialComponent.get_ordered() -> list Return a list of details in order of relative position. """ result = sorted(self._details.values(), key=lambda line: line.position) return result def increment(self, matching_statement, consolidating=False, xl_label=None, override=False, xl_only=False, over_time=False): """ BaseFinancialComponent.increment() -> None Increment matching lines, add new ones to instance. Works recursively. If ``consolidating`` is True, method sets obj._consolidated = True. """ if bb_settings.DEBUG_MODE: pool = matching_statement._get_ordered_items_debug() else: pool = matching_statement._details.items() # for name, external_line in pool: for name, external_line in pool: # ORDER SHOULD NOT MATTER HERE # If we get here, the line has survived screening. We now have two # ways to add its information to the instance. Option A, is to # increment the value on a matching line. Option B is to copy the # line into the instance. We apply Option B only when we can't do # Option A. own_line = self._details.get(name) if own_line: # Option A allowed = own_line.consolidate or not consolidating if allowed: own_line.increment(external_line, consolidating=consolidating, xl_label=xl_label, override=override, xl_only=xl_only, over_time=over_time) else: # Option B if external_line.consolidate or over_time: chk = not over_time local_copy = external_line.copy(check_include_details=chk) if not over_time: local_copy.remove_driver(recur=True) # Dont enforce rules to track old line.replicate() method if consolidating: if external_line.value is not None: if not local_copy._consolidated and not xl_only: local_copy._consolidated = True # Pick up lines with None values, but don't tag # them. We want to allow derive to write to these # if necessary. # need to make sure Chef knows to consolidate this # source line (or its details) also self._add_lines_in_chef(local_copy, external_line, xl_label=xl_label) if self._restricted: print(self) print(local_copy) c = "Trying to add line to restricted statement" raise ValueError(c) self.add_line(local_copy, local_copy.position) # For speed, could potentially add all the lines and then # fix positions once. def link_to(self, matching_statement): """ BaseFinancialComponent.link_to() -> None --``matching_statement`` is another Statement object Method links lines from instance to matching_statement in Excel. """ for line in self.get_ordered(): oline = matching_statement.find_first(line.name) line.link_to(oline) def register(self, namespace): """ BaseFinancialComponent.register() -> None --``namespace`` is the namespace to assign to instance Method sets namespace of instance and assigns BBID. Method recursively registers components. """ self.id.set_namespace(namespace) self.id.assign(self.name) for line in self.get_ordered(): line.register(namespace=self.id.bbid) def reset(self): """ BaseFinancialComponent.reset() -> None Clear all values, preserve line shape. """ # clears values, not shape if bb_settings.DEBUG_MODE: pool = self.get_ordered() else: pool = self._details.values() for line in pool: line.clear(recur=True) def peer_locator(self): """ Placeholder method that needs to be overridden by child classes Returns: """ pass def restrict(self): # recursively set statement and all contained lines to restricted=True self._restricted = True for line in self._details.values(): line.restrict() def set_name(self, name): TagsMixIn.set_name(self, name) def set_period(self, period): self._period = period for line in self._details.values(): line.set_period(period) #*************************************************************************# # NON-PUBLIC METHODS # #*************************************************************************# def _add_lines_in_chef(self, local_copy, external_line, xl_label=None): """ BaseFinancialComponent._add_lines_in_chef() -> None Add lines to consolidated.sources list used by Chef. """ # need to make sure Chef knows to consolidate this # source line (and its details) also if not local_copy._details: local_copy.xl_data.add_consolidated_source(external_line, label=xl_label) else: for n, l in local_copy._details.items(): detail_to_append = external_line._details.get(n) self._add_lines_in_chef(l, detail_to_append, xl_label) def _bind_and_record(self, line, noclear=False): """ BaseFinancialComponent._bind_and_record() -> None Set instance as line parent, add line to details. """ line.relationships.set_parent(self) try: line.set_period(self._period) except AttributeError: pass if self.id.bbid: line.register(namespace=self.id.bbid) else: line.register(namespace=self.id.namespace) self._details[line.tags.name] = line if not noclear and self.model is not None: # the only time we would ever not do this is on a copy call self.model.clear_fins_storage() def _inspect_line_for_insertion(self, line): """ BaseFinancialComponent._inspect_line_for_insertion() -> None Will throw exception if Line if you can't insert line into instance. """ if not line.tags.name: c = "Cannot add nameless lines." raise bb_exceptions.BBAnalyticalError(c) if line.tags.name in self._details: c = "Implicit overwrites prohibited: {}".format(line.tags.name) raise bb_exceptions.BBAnalyticalError(c) def _get_ordered_items_debug(self): """ BaseFinancialComponent._get_ordered_items_debug() -> list of tuples Return a list of _detail dictionary items in order of relative position. Items are key-value pairings contained in list of tuples. """ items = self._details.items() def item_sorter(item): line = item[1] return line.position result = sorted(items, key=item_sorter) return result def _repair_order(self, starting=0, recur=False): """ BaseFinancialComponent._repair_order() -> list Build an ordered list of details, then adjust their positions so that get_ordered()[0].position == starting and any two items are POSITION_SPACING apart. Sort by name in case of conflict. If ``starting`` is 0 and position spacing is 1, positions will match item index in self.get_ordered(). Repeats all the way down on recur. """ # Build table by position ordered = list() by_position = dict() if bb_settings.DEBUG_MODE: pool = self.get_ordered() else: pool = self._details.values() for line in pool: entry = by_position.setdefault(line.position, list()) entry.append(line) # Now, go through the table and build a list #can then just assign order to the list for position in sorted(by_position): lines = by_position[position] lines = sorted(lines, lambda x: x.tags.name) ordered.extend(lines) # Now can assign positions for i in range(len(ordered)): line = ordered[i] new_position = starting + (i * self.POSITION_SPACING) line.position = new_position if recur: line._repair_order(starting=starting, recur=recur) # Changes lines in place. return ordered
def from_database(cls, portal_data, financials): """ Statement.from_database(portal_data) -> Statement **CLASS METHOD** Method extracts a Statement from portal_data. """ new = cls(parent=financials) new.tags = Tags.from_database(portal_data['tags']) if portal_data['bbid'] is not None: new.id.bbid = ID.from_database(portal_data['bbid']).bbid # deserialize all LineItems catalog = dict() for row in portal_data['lines']: if row.get('link'): new_line = Link.from_database(row, new) elif 'driver_id' in row: new_line = LineItem.from_database(row, new) else: new_line = Step.from_database(row) parent_id = row.get('parent_bbid', None) if parent_id is None and new.id.bbid is not None: # no parent id, top-level line belongs to statement parent_id = new.id.bbid.hex if parent_id: par_id = ID.from_database(parent_id).bbid else: par_id = None sub_lines = catalog.setdefault(par_id, list()) sub_lines.append(new_line) if catalog: def build_line_structure(seed, catalog): details = catalog.pop(seed.id.bbid, list()) for line in details: old_bbid = line.id.bbid position = getattr(line, 'position', None) seed.add_line(line, position=position, noclear=True) line.id.bbid = old_bbid build_line_structure(line, catalog) build_line_structure(new, catalog) # DISPLAY TYPE stmt_type = portal_data.get("display_type", None) if stmt_type and stmt_type != "null": new.display_type = stmt_type if new.display_type == "regular" and new.name: if "covenant" in new.name.casefold(): new.display_type = new.COVENANT_TYPE if "kpi" in new.name.casefold(): new.display_type = new.KPI_TYPE # Visible attribute visible = portal_data.get("visible", None) if isinstance(visible, bool): new.visible = visible # Behavioral settings compute = portal_data.get("compute", None) if compute != "null" and compute is not None: new.compute = compute balance_sheet = portal_data.get("balance_sheet", None) if balance_sheet != "null" and balance_sheet is not None: new.balance_sheet = balance_sheet return new
class Financials: """ A class that organizes and operates on financial statements. ==================== ====================================================== Attribute Description ==================== ====================================================== DATA: cash p; returns cash flow Statement for instance ending p; returns ending BalanceSheet for instance full_order p; returns list of instance statements in display order full_ordered p; returns list of Statements in order of full_order has_valuation p; returns bool on whether instance has valuation data id instance of ID object, holds unique BBID for instance income p; returns income Statement for instance overview p; returns overview Statement for instance period p; returns TimePeriod that instance belongs to relationships instance of Relationships class starting p; returns starting BalanceSheet for instance update_statements list; holds onto state info in monitoring interviews valuation p; returns valuation Statement for instance CLASS DATA: CASH_NAME str; default name of the cash flow statement DEFAULT_ORDER list; default ordering of statements ENDING_BAL_NAME str; default name of ending balance sheet INCOME_NAME str; default name of income statement OVERVIEW_NAME str; default name of overview statement START_BAL_NAME str; default name of starting balance sheet VALUATION_NAME str; default name of valuation statement FUNCTIONS: to_database creates a flattened version of Financials for database add_statement adds a statement to the instance check_balance_sheet ensures matching structure between start and end balance copy returns deep copy find_line uses provided information to locate a LineItem and returns it get_covenant_statements returns list of instances covenant statements get_kpi_statements returns list of instances kpi statements get_regular_statements returns list of instances regular statements get_statement returns a statement with the given name populate_from_stored_values populates LineItems with values from .period register registers instance and components in a namespace reset resets instance and all components restrict restricts structural changes to instance and components set_order sets statement order on the instance CLASS METHODS: from_database() class method, extracts Financials out of API-format ==================== ====================================================== """ OVERVIEW_NAME = "Overview" INCOME_NAME = "Income Statement" CASH_NAME = "Cash Flow Statement" VALUATION_NAME = "Valuation" START_BAL_NAME = "Starting Balance Sheet" ENDING_BAL_NAME = "Ending Balance Sheet" DEFAULT_ORDER = [OVERVIEW_NAME, INCOME_NAME, CASH_NAME, START_BAL_NAME, ENDING_BAL_NAME, VALUATION_NAME] def __init__(self, parent=None, period=None): self.id = ID() # does not get its own bbid, just holds namespace # parent for Financials is BusinessUnit self.relationships = Relationships(self, parent=parent) self._period = period self._restricted = False self._full_order = self.DEFAULT_ORDER.copy() statements = [Statement(name=self.OVERVIEW_NAME, parent=self, period=period), Statement(name=self.INCOME_NAME, parent=self, period=period), CashFlow(name=self.CASH_NAME, parent=self, period=period), Statement(name=self.VALUATION_NAME, parent=self, period=period, compute=False), BalanceSheet(name=self.START_BAL_NAME, parent=self, period=period), BalanceSheet(name=self.ENDING_BAL_NAME, parent=self, period=period)] self._statement_directory = dict() for stmt in statements: self._statement_directory[stmt.name.casefold()] = stmt self.update_statements = list() @property def overview(self): return self.get_statement(self.OVERVIEW_NAME) @overview.setter def overview(self, value): self._statement_directory[self.OVERVIEW_NAME.casefold()] = value @property def income(self): return self.get_statement(self.INCOME_NAME) @income.setter def income(self, value): self._statement_directory[self.INCOME_NAME.casefold()] = value @property def cash(self): return self.get_statement(self.CASH_NAME) @cash.setter def cash(self, value): self._statement_directory[self.CASH_NAME.casefold()] = value @property def valuation(self): return self.get_statement(self.VALUATION_NAME) @valuation.setter def valuation(self, value): self._statement_directory[self.VALUATION_NAME.casefold()] = value @property def starting(self): return self.get_statement(self.START_BAL_NAME) @starting.setter def starting(self, value): self._statement_directory[self.START_BAL_NAME.casefold()] = value @property def ending(self): return self.get_statement(self.ENDING_BAL_NAME) @ending.setter def ending(self, value): self._statement_directory[self.ENDING_BAL_NAME.casefold()] = value @property def full_order(self): return self._full_order.copy() @property def full_ordered(self): result = [] for name in self._full_order: statement = self.get_statement(name) result.append(statement) return result @property def has_valuation(self): return not self.valuation == Statement(self.VALUATION_NAME) @property def period(self): return self._period @period.setter def period(self, value): self._period = value for statement in self._statement_directory.values(): if statement: if statement.relationships.parent is self: statement.set_period(value) def __str__(self): header = '' period = self.period if period: if Equalities.multi_getattr(self, "relationships.parent", None): header = ( '{begin:^{width}}\n\n' '{start:^{width}}\n' '{close:^{width}}\n\n' ).format( width=bb_settings.SCREEN_WIDTH, begin='Financial statements for {}'.format( self.relationships.parent.tags.name ), start='Period starting: {}'.format(period.start), close='Period ending: {}'.format(period.end), ) content = [] for statement in self.full_ordered: if statement is not None: content.append(str(statement)) result = ( '{header}' '{content}\n' '{border:^{width}}\n\n' ).format( width=bb_settings.SCREEN_WIDTH, header=header, content=''.join(content), border="***", ) return result @classmethod def from_database(cls, portal_data, company, **kargs): """ Financials.from_database(portal_data) -> Financials **CLASS METHOD** Method extracts Financials from portal_data. """ period = kargs['period'] new = cls(parent=company, period=period) new.register(company.id.bbid) us = portal_data['update_statements'] new.update_statements = us # new._chef_order = portal_data['chef_order'] # new._compute_order = portal_data['compute_order'] # new._exclude_statements = portal_data['exclude_statements'] new._full_order = portal_data['full_order'] for data in portal_data['statements']: statement = Statement.from_database( data, financials=new ) new._statement_directory[statement.name.casefold()] = statement return new def to_database(self): """ Financials.to_database() -> dict Method yields a serialized representation of self. """ self.check_balance_sheets() statements = [] for name in self._full_order: statement = self.get_statement(name) if statement: data = statement.to_database() data['attr_name'] = name statements.append(data) result = { 'statements': statements, # 'chef_order': self._chef_order, # 'compute_order': self._compute_order, # 'exclude_statements': self._exclude_statements, 'full_order': self._full_order, 'update_statements': self.update_statements, } return result def add_statement(self, name, statement=None, title=None, position=None, compute=True, overwrite=False): """ Financials.add_statement() -> None --``name`` is the string name for the statement attribute --``statement`` is optionally the statement to insert, if not provided a blank statement will be added --``title`` is optionally the name to assign to the statement object, if not provided ``name`` will be used --``position`` is optionally the index at which to insert the statement in instance.full_order --``compute`` bool; default is True, whether or not to include the statement in computation order during fill_out (will be computed between starting and ending balance sheets) or whether to compute manually after Method adds a new statement to the instance and inserts it at specified position in instance.full_order. If no position is provided, the new statement will be added at the end. """ if name.casefold() in self._statement_directory and not overwrite: c = "%s already exists as a statement!" % name raise bb_exceptions.BlackbirdError(c) if not self._restricted: if not statement: use_name = title or name statement = Statement(use_name, period=self.period, compute=compute) statement.relationships.set_parent(self) self._statement_directory[name.casefold()] = statement if position: self._full_order.insert(position, name) else: self._full_order.append(name) if self.id.namespace: statement.register(self.id.namespace) def check_balance_sheets(self): """ Financials.check_balance_sheets() -> None Method ensures that starting and ending balance sheets have matching structures. """ start_lines = self.starting.get_full_ordered() start_names = [line.name for line in start_lines] end_lines = self.ending.get_full_ordered() end_names = [line.name for line in end_lines] if start_names != end_names: # only run the increments if there is a mismatch self.starting.increment(self.ending, consolidating=False, over_time=True) self.ending.increment(self.starting, consolidating=False, over_time=True) def copy(self, clean=False): """ Financials.copy() -> Financials Return a deep copy of instance. Method starts with a shallow copy and then substitutes deep copies for the values of each attribute in instance.ORDER """ new_instance = Financials() new_instance._full_order = self._full_order.copy() for key, stmt in self._statement_directory.items(): new_statement = stmt.copy(clean=clean) new_statement.relationships.set_parent(new_instance) new_instance._statement_directory[key] = new_statement new_instance.id = ID() new_instance.register(self.id.namespace) return new_instance def find_line(self, line_id, statement_attr): """ Financials.find_line() -> LineItem --``line_id`` bbid of line Finds a LineItem across all statements by its bbid. """ if isinstance(line_id, str): line_id = ID.from_database(line_id).bbid statement = self.get_statement(statement_attr) if statement: for line in statement.get_full_ordered(): if line.id.bbid == line_id: return line raise bb_exceptions.StructureError( 'Could not find line with id {}'.format(line_id) ) def get_statement(self, name): """ Financials.get_statement() -> Statement or None Method searches for a statement with the specified name (caseless search) in instance directory. To maintain backwards compatibility with legacy Excel uploads, we also search for statements with names containing the provided name (e.g. name="income" will generally return the income statement, whose proper name is "income statement"). If no exact name match is found and multiple partial matches are found an error is raised. """ if isinstance(name, str): name = name.casefold() if name in self._statement_directory: return self._statement_directory[name] else: outs = list() for k in self._statement_directory.keys(): if name in k: outs.append(self._statement_directory[k]) if len(outs) == 1: return outs[0] elif len(outs) > 1: b = "ENTRY IS NOT A STATEMENT" names = [s.name if isinstance(s, Statement) else b for s in outs] c = "Statement with exact name not found. " \ "Multiple statements with partial matching" \ " name were found: " + ', '.join(names) raise KeyError(c) return None def populate_from_stored_values(self, period): """ Financials.populate_from_stored_values() -> None --``period`` is the TimePeriod from which to retrieve values Method uses financials data (values and excel info) stored in the period to fill in the line values in the instance. """ tl = period.relationships.parent if len(tl.keys()) > 0: min_dt = min(tl.keys()) first = tl[min_dt] else: first = None for statement in self.full_ordered: if statement is not None and (statement is not self.starting or period is first): for line in statement.get_full_ordered(): line.xl_data = LineData(line) if not line._details or not line.sum_details: value = period.get_line_value(line.id.bbid.hex) line._local_value = value hc = period.get_line_hc(line.id.bbid.hex) line._hardcoded = hc buid = self.relationships.parent.id.bbid past = period.past future = period.future # Now check if fins exist in period.past if past: if buid in past.financials: # past financials already exist as rich objects, # so we can just link to existing ending balance sheet past_fins = past.financials[buid] self.starting = past_fins.ending else: # past financials have not yet been re-inflated, so we have to # make an Ending Balance Sheet and pretend it belongs to the # preceding period self.starting = self.ending.copy() self.starting.set_period(past) for line in self.starting.get_full_ordered(): line.xl_data = LineData(line) if not line._details or not line.sum_details: value = past.get_line_value(line.id.bbid.hex) line._local_value = value hc = past.get_line_hc(line.id.bbid.hex) line._hardcoded = hc # And if fins exist in period.future if future: if buid in future.financials: future_fins = future.financials[buid] future_fins.starting = self.ending def register(self, namespace): """ Financials.register() -> None --``namespace`` is the namespace to assign to instance Method sets namespace of instance, does not assign an actual ID. Registers statements on instance. """ self.id.set_namespace(namespace) for statement in self._statement_directory.values(): if statement: statement.register(self.id.namespace) def reset(self): """ Financials.reset() -> None Reset each defined statement. """ for statement in self.full_ordered: if statement: if statement.compute and statement is not self.starting: statement.reset() def restrict(self): """ Financials.restrict() -> None Restrict instance and each defined statement from altering their structure (no adding lines or removing lines). This is used for period financials which should show SSOT structure. """ self._restricted = True for statement in self._statement_directory.values(): if statement: statement.restrict() def set_order(self, new_order): """ Financials.set_order() -> None --``new_order`` is a list of strings representing the names of the statements on the instance in the order they should be displayed Method looks for default statement names in the list and replaces them with their standard cased versions. Method ensures that starting balance sheet is placed appropriately (immediately preceding ending balance sheet). Method ensures that all default statements are represented; statements not included in the upload are appended at the end of the order. """ new_order = [name.casefold() for name in new_order] for idx, stmt in enumerate(new_order): for entry in self.DEFAULT_ORDER: if stmt in entry.casefold(): new_order[idx] = entry break try: idx = new_order.index(self.ENDING_BAL_NAME) except ValueError: pass else: if new_order[idx-1] != self.START_BAL_NAME: new_order.insert(idx, self.START_BAL_NAME) for name in self.DEFAULT_ORDER: if name not in new_order: stmt = self.get_statement(name) stmt.visible = False new_order.append(name) self._full_order = new_order
class TimeLine(dict): """ A TimeLine is a dictionary of TimePeriod objects keyed by ending date. The TimeLine helps manage, configure, and search TimePeriods. Unless otherwise specified, class expects all dates as datetime.date objects and all periods as datetime.timedelta objects. ==================== ====================================================== Attribute Description ==================== ====================================================== DATA: current_period P; pointer to the period that represents the present id instance of PlatformComponents.ID class, for interface master TimePeriod; unit templates that fall outside of time name str; corresponds with Model.time_line key parameters Parameters object, specifies shared parameters ref_date datetime.date; reference date for the model resolution string; 'monthly', 'annual'..etc. Model.time_line key FUNCTIONS: build() populates instance with adjacent time periods clear() delete content from past and future periods clear_future() delete content from future periods extrapolate() use seed to fill out all future periods in instance extrapolate_dates() use seed to fill out a range of dates find_period() returns period that contains queried time point get_segments() split time line into past, present, and future get_ordered() returns list of periods ordered by end point link() connect adjacent periods revert_current() go back to the prior current period update_current() updates current_period for reference or actual date ==================== ====================================================== """ DEFAULT_PERIODS_FORWARD = 60 DEFAULT_PERIODS_BACK = 1 def __init__(self, model, resolution='monthly', name='default', interval=1): dict.__init__(self) self.id = ID() # Timeline objects support the id interface and pass the model's id # down to time periods. The Timeline instance itself does not get # its own bbid. self.model = model self.resolution = resolution self.name = name self.interval = interval self.master = None self.parameters = Parameters() self.has_been_extrapolated = False self.ref_date = None self.id.set_namespace(model.id.bbid) @property def current_period(self): """ **property** Getter returns instance._current_period. Setter stores old value for reversion, then sets new value. Deleter sets value to None. """ if len(self): cp = self.find_period(self.model.ref_date) return cp @property def first_period(self): if not self.keys(): per = None else: min_date = min(self.keys()) per = self[min_date] return per def __str__(self, lines=None): """ Components.__str__(lines = None) -> str Method concatenates each line in ``lines``, adds a new-line character at the end, and returns a string ready for printing. If ``lines`` is None, method calls pretty_print() on instance. """ if not lines: lines = views.view_as_time_line(self) line_end = "\n" result = line_end.join(lines) return result @classmethod def from_database(cls, portal_data, model, **kargs): """ TimeLine.from_database(portal_data) -> TimeLine **CLASS METHOD** Method extracts a TimeLine from portal_data. """ key = tuple(portal_data[k] for k in ('resolution', 'name')) new = cls( model, resolution=portal_data['resolution'], name=portal_data['name'], ) new.master = model.taxo_dir if portal_data['interval'] is not None: new.interval = portal_data['interval'] if portal_data['ref_date']: new.ref_date = portal_data['ref_date'] if isinstance(new.ref_date, str): new.ref_date = date_from_iso(new.ref_date) if portal_data['has_been_extrapolated'] is not None: new.has_been_extrapolated = portal_data['has_been_extrapolated'] if portal_data['parameters'] is not None: new.parameters = Parameters.from_database( portal_data['parameters']) for data in portal_data['periods']: period = TimePeriod.from_database( data, model=model, time_line=new, ) return new def to_database(self): """ TimeLine.to_database() -> dict Method yields a serialized representation of self. """ periods = [period.to_database() for period in self.values()] result = { 'periods': periods, 'interval': self.interval, 'ref_date': format(self.ref_date) if self.ref_date else None, 'has_been_extrapolated': self.has_been_extrapolated, 'parameters': list(self.parameters.to_database()), } return result def copy_structure(self): """ TimeLine.copy_structure() -> TimeLine Method returns a copy of self linked to parent model and with the same layout. """ result = type(self)(self.model) result.ref_date = copy.copy(self.ref_date) result.parameters = self.parameters.copy() for old_period in self.iter_ordered(): new_period = old_period.copy(clean=True) result.add_period(new_period) if self.master: result.master = result[self.master.end] return result def build( self, ref_date=None, fwd=DEFAULT_PERIODS_FORWARD, back=DEFAULT_PERIODS_BACK, year_end=True, ): """ TimeLine.build() -> None --``ref_date`` is datetime.date to use as the reference date for the timeline --``fwd`` is int number of periods to build forward of the ref_date --``back`` is int number of period to build before the ref_date --``year_end`` is bool for whether to build through the end of the year Method creates a chain of TimePeriods with adjacent start and end points. The chain is at least ``fwd`` periods long into the future and ``back`` periods long into the past. Forward chain ends on a Dec. Method expects ``ref_date`` to be a datetime.date object. Method sets instance.current_period to the period covering the reference date. Method also sets master to a copy of the current period. """ if not ref_date: ref_date = self.model.ref_date self.ref_date = ref_date ref_month = ref_date.month ref_year = ref_date.year current_start_date = date(ref_year, ref_month, 1) # Make reference period fwd_start_date = self._get_fwd_start_date(ref_date) current_end_date = fwd_start_date - timedelta(1) current_period = TimePeriod(current_start_date, current_end_date, model=self.model) self.add_period(current_period) # Add master period self.master = self.model.taxo_dir # Now make the chain back_end_date = current_start_date - timedelta(1) # Save known starting point for back chain build before fwd changes it. # Make fwd chain i = 0 while fwd or year_end: # pick up where ref period analysis leaves off curr_start_date = fwd_start_date fwd_start_date = self._get_fwd_start_date(curr_start_date) curr_end_date = fwd_start_date - timedelta(1) fwd_period = TimePeriod(curr_start_date, curr_end_date, model=self.model) self.add_period(fwd_period) i += 1 if i >= fwd and (not year_end or fwd_period.end.month == 12): break # first line picks up last value in function scope, so loop # should be closed. # Make back chain for i in range(back): curr_end_date = back_end_date curr_start_date = date(curr_end_date.year, curr_end_date.month, 1) back_period = TimePeriod(curr_start_date, curr_end_date, model=self.model) self.add_period(back_period) # close loop: back_end_date = curr_start_date - timedelta(1) def clear(self): """ TimeLine.clear() -> None Clear content from past and future, preserve current_period. """ for period in self.iter_ordered(): if period.end != self.current_period.end: period.clear() # have to dereference history # have to do so recursively, to make sure that none of the objects # retain their external pointers. def clear_future(self, seed=None): """ TimeLine.clear_future() -> None Clear content from all periods after seed. Method expects a period as ``seed``, will use instance.current_period if seed is None. """ if seed is None: seed = self.current_period for period in self.iter_ordered(): if period.end > seed.end: period.clear() def copy(self): """ TimeLine.copy() -> TimeLine Method returns a copy of the instance. """ result = copy.copy(self) for key, value in self.items(): result[key] = value.copy() result[key].relationships.set_parent(result) result.has_been_extrapolated = self.has_been_extrapolated return result def add_period(self, period): """ Timeline.add_period() -> None --``period`` is a TimePeriod object Method configures period and records it in the instance under the period's end_date. """ period = self._configure_period(period) self[period.end] = period def iter_ordered(self, open=None, exit=None, shut=None): """ Timeline.iter_ordered() -> iter --``open`` date, soft start, if falls in period, iteration starts --``exit`` date, soft stop, if falls in period, last shown --``shut`` date, hard stop, if not exact period end, iteration stops Method iterates over periods in order, starting with the one in which ``open`` falls, and ending with the one including ``exit`` and not following ``shut``. """ for end_date, period in sorted(self.items()): if open and open > period.end: continue if exit and exit < period.start: break if shut and shut < period.end: break yield period def get_ordered(self): """ Timeline.get_ordered() -> list Method returns list of periods in instance, ordered from earliest to latest endpoint. """ return list(self.iter_ordered()) def extrapolate(self, seed=None, calc_summaries=True): """ TimeLine.extrapolate() -> None Extrapolate current period to future dates. Make quarterly and annual financial summaries. Updates all summaries contained in instance.summaries. """ print('--------EXTRAPOLATE----------') if seed is None: seed = self.current_period company = self.model.get_company() company.consolidate_fins_structure() if seed.past: company.recalculate(period=seed.past, adjust_future=False) company.recalculate(period=seed, adjust_future=False) summary_maker = SummaryMaker(self.model) for period in self.iter_ordered(open=seed.end): if period.end > seed.end: logger.info(period.end) # reset content and directories period.clear() # combine tags period.tags = seed.tags.extrapolate_to(period.tags) # propagate parameters from past to current period.combine_parameters() # copy and fill out content company.fill_out(period=period) if bb_settings.MAKE_ANNUAL_SUMMARIES and calc_summaries: if period.end >= self.current_period.end: summary_maker.parse_period(period) # drop future periods that have been used up to keep size low if bb_settings.DYNAMIC_EXTRAPOLATION: if period.past and period.past.past: if period.past.past.end > self.current_period.end: period.past.past.financials.clear() if bb_settings.MAKE_ANNUAL_SUMMARIES and calc_summaries: summary_maker.wrap() # import devhooks # devhooks.picksize(self) self.has_been_extrapolated = True def extrapolate_dates(self, seed, dates, backward=False): """ TimeLine.extrapolate_dates() -> None Method extrapolates seed to the first date in dates, then sequentially extrapolates remaining dates from each other. Method expects ``seed`` to be an instance of TimePeriod. Seed can be external to the caller TimeLine. Method expects ``dates`` to be a series of endpoints for the periods in instance the caller is targeting. In other words, instance must contain a period corresponding to each date in ``dates``. Dates can contain gaps. Method will always extrapolate from one date to its neighbor. Extrapolation works by requesting that each object in a content structure extrapolate itself and any of its subordinates. For time-sensitive objects like BusinessUnits, that process should automatically adjust to the target date regardless of how far away that date is from the instance's reference date. If ``work_backward`` is True, method will go through dates last-one-first. """ # if backward: dates = dates[::-1] # Reverse order, so go from newest to oldest for i in range(len(dates)): date = dates[i] # With default arguments, start work at the period immediately # prior to the current period target_period = self[date] updated_period = seed.extrapolate_to(target_period) # extrapolate_to() always does work on an external object and leaves # the target untouched. Manually swap the old period for the new # period. if i == 0: updated_period = self._configure_period(updated_period) # On i == 0, extrapolating from the original seed. seed can be # external (come from a different model), in which case it would # use a different model namespace id for unit tracking. # # Accordingly, when extrapolating from the may-be-external seed, # use configure_period() to conform output to current model. # # Subsequent iterations of the loop will start w periods that are # already in the model, so method can leave their namespace id # configuration as is. self[date] = updated_period seed = updated_period def extrapolate_statement(self, statement_name, seed=None): """ TimeLine.extrapolate_statement() -> None Extrapolates a single statement forward in time. DOES NOT MAKE SUMMARIES. """ if seed is None: seed = self.current_period company = self.model.get_company() orig_fins = company.get_financials(period=seed) orig_statement = getattr(orig_fins, statement_name) orig_statement.reset() company.compute(statement_name, period=seed) for period in self.iter_ordered(open=seed.end): if period.end > seed.end: new_fins = company.get_financials(period=period) new_stat = new_fins.get_statement(statement_name) if new_stat is None: # need to add statement new_stat = orig_statement.copy(clean=True) new_fins.add_statement(statement_name, statement=new_stat) company.compute(statement_name, period=period) else: # compute what is already there company.compute(statement_name, period=period) def find_period(self, query): """ TimeLine.find_period() -> TimePeriod Method returns a time period that includes query. ``query`` can be a POSIX timestamp (int or float), datetime.date object, or string in "YYYY-MM-DD" format. """ if isinstance(query, date): q_date = query else: try: q_date = date.fromtimestamp(query) except TypeError: num_query = [int(x) for x in query.split("-")] # query is a string, split it q_date = date(*num_query) end_date = self._get_ref_end_date(q_date) result = self.get(end_date) return result def get_segments(self, ref_date=None): """ TimeLine.get_segments() -> list Method returns a list of the past, present, and future segments of the instance, with respect to the ref_date. If ``ref_date`` is None, method counts current period as the present. output[0] = list of keys for periods before ref_date output[1] = list of ref period (len output[1] == 1) output[2] = list of keys for periods after ref_date """ if not ref_date: ref_date = self.current_period.end ref_end = self._get_ref_end_date(ref_date) # dates = sorted(self.keys()) ref_spot = dates.index(ref_end) future_dates = dates[(ref_spot + 1):] past_dates = dates[:ref_spot] result = [past_dates, [ref_end], future_dates] return result # *************************************************************************# # NON-PUBLIC METHODS # # *************************************************************************# def _get_fwd_start_date(self, ref_date): """ TimeLine.get_fwd_start_date() -> datetime.date Method returns the starting date of the next month. """ ref_month = ref_date.month ref_year = ref_date.year if ref_month == 12: result = date(ref_year + 1, 1, 1) else: result = date(ref_year, ref_month + 1, 1) return result def _get_ref_end_date(self, ref_date): """ TimeLine._get_ref_end_date() -> datetime.date Method returns the last date of the month that contains ref_date. """ result = None fwd_start_date = self._get_fwd_start_date(ref_date) ref_end_date = fwd_start_date - timedelta(1) result = ref_end_date return result def _configure_period(self, period): """ Timeline._configure_period() -> period Method sets period's namespace id to that of the TimeLine, then returns period. """ model_namespace = self.id.namespace period.id.set_namespace(model_namespace) # Period has only a pointer to the Model.namespace_id; periods don't # have their own bbids. period.relationships.set_parent(self) # end dates of the past and future periods try: period.past_end = max( (day for day in self.keys() if day < period.end)) except: period.past_end = None try: period.next_end = min( (day for day in self.keys() if day > period.end)) except: period.next_end = None # link adjacent periods if period.past_end: past_period = self[period.past_end] past_period.next_end = period.end if period.next_end: next_period = self[period.next_end] next_period.past_end = period.end return period