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
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 Model(TagsMixIn): """ This class provides a form of a standard time model of business performance. A Model object revolves around a Timeline, which consists of a list of snapshots of the business at different points in wall time. Each such snapshot is an instance of a TimePeriod object. Each Model instance has its own namespace_id derived from the origin Blackbird UUID. The Model's namespace_id is the source of truth for business units within that model. In other words, business units have ids that are unique within the Model. If a business unit has an id that's equal to that of another business unit, they represent the same real life reference within a given model. A Model should contain a single instance of the same business unit for each unit's bbid. Each TimePeriod in the TimeLine should contain a instance of Financials that are keyed by that business unit's bbid. ==================== ====================================================== Attribute Description ==================== ====================================================== DATA: bu_directory dict; key = bbid, val = business units drivers instance of DriverContainer; stores Driver objects fiscal_year_end P (date); fiscal year end, default is 12/31 id instance of ID object, carries bbid for model interview P; points to target BusinessUnit.interview portal_data dict; stores data from Portal related to the instance processing_status P (str); name of processing stage ("intake", etc.) ref_date P (date); reference date for Model, specifies current period report_summary dict; stores data that Portal reads for reporting scenarios dict; stores alternate parameter values stage P; points to target BusinessUnit.stage started P (bool); tracks whether engine has begun work summary P; pointer to current period summary target P; pointer to target BusinessUnit taxo_dir instance of TaxoDir, has a dict {bbid: taxonomy units} taxonomy instance of Taxonomy; holds BU templates and links to taxo_dir topic_list list of topic names run on model time_line P; pointer to default TimeLine object time_lines list of TimeLine objects transcript list of entries that tracks Engine processing ty_directory dict; key = strings, val = sets of bbids valuation P; pointer to current period valuation FUNCTIONS: to_database() creates a flattened version of model for Portal calc_summaries() creates or updates standard summaries for model change_ref_date() updates timeline to use new reference date clear_fins_storage() clears financial data from non SSOT financials copy() returns a copy of Model instance create_timeline() creates a timeline with the specified attributes get_company() method to get top-level company unit get_financials() method to get financials for a given unit and time get_line() finds LineItem from specified parameters get_lowest_units() return list of units w/o components from bbid pool get_tagged_units() return dict of units (by bbid) with specified tags get_timeline() method to get timeline at specific resolution (m,q,a) get_units() return list of units from bbid pool populate_xl_data() method populates xl attr on all line items pre-chop prep_for_monitoring_interview() sets up path entry point for reporting prep_for_revision_interview() sets up path entry point for revision register() registers item in model namespace set_company() method sets BusinessUnit as top-level company set_timeline() method sets default timeline start() sets _started and started to True transcribe() append message and timestamp to transcript CLASS METHODS: from_database() class method, extracts model out of API-format ==================== ====================================================== ``P`` indicates attributes decorated as properties. See attribute-level doc string for more information. """ 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() # DYNAMIC ATTRIBUTES @property def fiscal_year_end(self): """ Model.fiscal_year_end() -> date Return self._fiscal_year_end or calendar year end. """ if not self._fiscal_year_end: time_line = self.get_timeline() year = time_line.current_period.end.year fye = datetime.date(year, 12, 31) else: fye = self._fiscal_year_end return fye @fiscal_year_end.setter def fiscal_year_end(self, fye): """ Model.fiscal_year_end() -> date Set self._fiscal_year_end. """ # maybe make fiscal_year_end a property and do this on assignment last_day = calendar.monthrange(fye.year, fye.month)[1] if last_day - fye.day > fye.day: # closer to the beginning of the month, use previous month # for fiscal_year_end temp = fye - relativedelta(months=1) last_month = temp.month last_day = calendar.monthrange(fye.year, last_month)[1] fye = datetime.date(fye.year, last_month, last_day) else: # use end of current month last_day = calendar.monthrange(fye.year, fye.month)[1] fye = datetime.date(fye.year, fye.month, last_day) self._fiscal_year_end = fye @property def interview(self): return self.target.interview @property def processing_status(self): return self._processing_status @property def ref_date(self): return self._ref_date @property def stage(self): return self.target.stage @property def started(self): """ **read-only property** Once True, difficult to undo (a toggle that sticks). """ return self._started @property def valuation(self): """ **read-only property** Pointer to company valuation on current period. """ company = self.get_company() if company: # catch periods with empty content return company.valuation @valuation.setter def valuation(self, value): c = ("Assignment prohibited. " "``model.valuation`` serves only as a pointer " "to the current period company valuation.") raise bb_exceptions.ManagedAttributeError(c) @property def time_line(self): return self.get_timeline() @time_line.setter def time_line(self, time_line): return self.set_timeline(time_line) # METHODS @classmethod def from_database(cls, portal_model): """ Model.from_database(portal_model) -> Model **CLASS METHOD** Method extracts a Model from portal_model. Method expects ``portal_model`` to be nested dictionary containing all necessary information for rebuilding a Model instance. """ name = portal_model.pop('name', None) M = cls(name) M.portal_data.update(portal_model) business_name = portal_model.get("business_name", None) del portal_model tags = M.portal_data.pop('tags') if tags: M.tags = Tags.from_database(tags) # set basic attributes M._processing_status = M.portal_data.pop('processing_status', 'intake') M._ref_date = M.portal_data.pop('ref_date') if isinstance(M._ref_date, str): M._ref_date = date_from_iso(M._ref_date) M._started = M.portal_data.pop('started') M.topic_list = M.portal_data.pop('topic_list', list()) M.transcript = M.portal_data.pop('transcript', list()) M.report_summary = M.portal_data.pop('report_summary', None) M._fiscal_year_end = M.portal_data.pop('fiscal_year_end') scen = M.portal_data.pop('scenarios') if scen is not None: M.scenarios = scen link_list = list() # first deserialize BusinessUnits into directory temp_directory = dict() bu_list = M.portal_data.pop('business_units', list()) reg_bus = [bu for bu in bu_list if not bu.get('taxonomy', False)] taxo_bus = [bu for bu in bu_list if bu.get('taxonomy', False)] for flat_bu in reg_bus: rich_bu = BusinessUnit.from_database(flat_bu, link_list) rich_bu.relationships.set_model(M) bbid = ID.from_database(flat_bu['bbid']).bbid temp_directory[bbid] = rich_bu # now rebuild structure company_id = M.portal_data.pop('company', None) if company_id: company_id = ID.from_database(company_id).bbid def build_bu_structure(seed, directory): component_list = seed.components seed.components = None seed._set_components() for component_id in component_list: sub_bu = directory[component_id] seed.add_component(sub_bu) build_bu_structure(sub_bu, directory) top_bu = temp_directory[company_id] build_bu_structure(top_bu, temp_directory) M.set_company(top_bu) # TaxoDir if taxo_bus: M.taxo_dir = TaxoDir.from_database(taxo_bus, M, link_list) # Fix Links if link_list: for link in link_list: targ_id = link.target if targ_id in M.bu_directory: link.target = M.bu_directory[targ_id] elif targ_id in M.taxo_dir.bu_directory: link.target = M.taxo_dir.bu_directory[targ_id] else: c = "ERROR: Cannot locate link target: " + targ_id.hex raise LookupError(c) # Taxonomy data = M.portal_data.pop('taxonomy', None) if data: M.taxonomy = Taxonomy.from_database(data, M.taxo_dir) # Target target_id = M.portal_data.pop('target', None) if target_id: target_id = ID.from_database(target_id).bbid try: M.target = M.bu_directory[target_id] except KeyError: M.target = M.taxo_dir.bu_directory[target_id] # reinflate timelines timeline_data = M.portal_data.pop('timelines', list()) if timeline_data: timelines = {} for data in timeline_data: key = (data['resolution'], data['name']) timelines[key] = TimeLine.from_database(data, model=M) M.timelines = timelines # reinflate drivers drivers = M.portal_data.pop('drivers', list()) if drivers: M.drivers = DriverContainer.from_database(drivers) if business_name and business_name != M.title: M.set_name(business_name) M.portal_data.pop('title') M.portal_data.pop('bbid') return M def to_database(self): """ Model.to_database() -> dict Method returns a serialized representation of self. """ result = dict() for k, v in self.portal_data.items(): result[k] = v result[ 'company'] = self._company.id.bbid.hex if self._company else None result['ref_date'] = self._ref_date result['started'] = self._started result['target'] = self.target.id.bbid.hex if self.target else None result['topic_list'] = self.topic_list result['transcript'] = self.transcript result['scenarios'] = self.scenarios result['tags'] = self.tags.to_database() result['name'] = self.name result['report_summary'] = self.report_summary # pre-process financials in the current period, make sure they get # serialized in th database to maintain structure data bus = [bu.to_database() for bu in self.bu_directory.values()] bus.extend(self.taxo_dir.to_database()) result['business_units'] = bus result['taxonomy'] = self.taxonomy.to_database() # serialized representation has a list of timelines attached # with (resolution, name) as properties timelines = [] for (resolution, name), time_line in self.timelines.items(): data = { 'resolution': resolution, 'name': name, } # add serialized periods data.update(time_line.to_database()) timelines.append(data) result['timelines'] = timelines result['drivers'] = self.drivers.to_database() result['fiscal_year_end'] = self._fiscal_year_end # One-way attributes (will not be used in de-serialization): result['bbid'] = self.id.bbid.hex result['title'] = self.title return result def calc_summaries(self): """ Model.calc_summaries() -> None Method deletes existing summaries and recalculates. """ self.timelines.pop(('quarterly', 'default'), None) self.timelines.pop(('annual', 'default'), None) summary_builder = SummaryMaker(self) tl = self.get_timeline('monthly', 'default') seed = tl.current_period for period in tl.iter_ordered(open=seed.end): if period.end >= seed.end: summary_builder.parse_period(period) summary_builder.wrap() def change_ref_date(self, ref_date, build=False): """ Model.change_ref_date() -> None --``ref_date`` is datetime.date to use as the reference date for updated timeline Method updates time_line to use adjusted ref_date. """ ntls = len(self.timelines.values()) all_periods_exist = True for tl in self.timelines.values(): per = tl.find_period(ref_date) if not per: all_periods_exist = False if build and ntls == 1: new_tl = TimeLine(self) new_tl.parameters = self.time_line.parameters.copy() new_tl.master = self.time_line.master new_tl.build(ref_date=ref_date) new_tl.id.set_namespace(self.id.bbid) self.set_timeline(new_tl, overwrite=True) elif build and ntls > 1: c = "ERROR: Cannot build arbitrary timelines." raise (ValueError(c)) elif not all_periods_exist and not build: c = "ERROR: TimePeriod corresponding to ref_date does not exist " \ "in all timelines." raise (ValueError(c)) self._ref_date = ref_date def clear_fins_storage(self): """ Model.clear_fins_storage() --> None Method clears financial values and xl data storage after modification to SSOT financials. """ for tl in self.timelines.values(): for per in tl.values(): per.clear() def copy(self): """ Model.copy() -> obj Method creates a copy of instance and returns it. Delegates to relevant classes to copy attributes. """ result = Model(self.tags.title) result._ref_date = self._ref_date result._started = self._started result.portal_data = self.portal_data.copy() result.taxonomy = self.taxonomy.copy() result.transcript = self.transcript.copy() for key, time_line in self.timelines.items(): new_tl = time_line.copy() new_tl.model = result result.set_timeline(new_tl, *key, overwrite=True) result.scenarios = self.scenarios.copy() result.target = self.target return result def create_timeline(self, resolution='monthly', name='default', add=True, overwrite=False): """ Model.create_timeline() -> TimeLine --``resolution`` is 'monthly', 'quarterly', 'annually' or any available summary resolution' Method creates a timeline and adds it to the dictionary of own timelines. """ time_line = TimeLine(self, resolution=resolution, name=name) if add: self.set_timeline(time_line, resolution=resolution, name=name, overwrite=overwrite) return time_line def get_company(self, buid=None): """ Model.get_company() -> BusinessUnit --``buid`` is the bbid of the BusinessUnit to return Method returns model.company or a business unit with a specific bbid, or the company if none is provided. """ if buid: return self.bu_directory[buid] else: return self._company def get_financials(self, bbid, period): """ Model.get_financials() -> Financials --``bbid`` is the ID.bbid for the BusinessUnit whose financials you are seeking --``period`` is an instance of TimePeriod Method returns the specified version of financials. """ if period and bbid in period.financials: fins = period.financials[bbid] else: unit = self.bu_directory[bbid] fins = unit.get_financials(period) return fins 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 get_lowest_units(self, pool=None, run_on_empty=False): """ Model.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. """ 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_units(self, *tags, pool=None): """ Model.get_tagged_units() -> dict Return a dictionary of units (by bbid) that carry the specified tags. If ``pool`` is None, uses bu_directory. 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 = list(pool) pool.sort(key=lambda bu: bu.id.bbid) tagged_dict = tools.for_tag_operations.get_tagged(pool, *tags) return tagged_dict def get_timeline(self, resolution='monthly', name='default'): """ Model.get_timeline() -> TimeLine --``resolution`` is 'monthly', 'quarterly', 'annually' or any available summary resolution' --``name`` is 'default', 'actual', forecast', 'budget' Method returns the timeline for specified resolution (if any). """ key = (resolution, name) if key in self.timelines: return self.timelines[key] def get_units(self, pool): """ Model.get_units() -> list Method returns a list of objects from instance.bu_directory that correspond to each bbid in ``pool``. Method sorts pool prior to processing. Method expects ``pool`` to be an iterable of bbids. """ pool = sorted(pool) # make sure to sort pool for stable output order units = [] for bbid in pool: u = self.bu_directory[bbid] units.append(u) return units def populate_xl_data(self): """ Model.populate_xl_data() -> None Method populates "xl" attributes on all line items in preparation for chopping. """ # once all LineItems have been reconstructed, rebuild links among them for time_line in self.timelines.values(): for period in time_line.iter_ordered(): for bu in self.bu_directory.values(): fins = bu.get_financials(period) for statement in fins.full_ordered: if statement: for line in statement.get_full_ordered(): if not line.xl_data.built: id = line.id.bbid.hex new_data = period.get_xl_data(id, line) new_data.built = True line.xl_data = new_data def prep_for_monitoring_interview(self): """ Model.prep_monitoring_interview() -> None Function sets path for monitoring interview after projections are set. Function runs after pressing the "update" button on the model card. """ # set company as target co = self.get_company() co._stage = None self.target = co # preserve existing path and set fresh BU.used and BU.stage.path for bu in self.bu_directory.values(): bu.financials.update_statements = bu.financials.full_order starting = bu.financials.START_BAL_NAME idx = bu.financials.update_statements.index(starting) bu.financials.update_statements.pop(idx) bu.archive_path() bu.archive_used() for bu in self.taxo_dir.bu_directory.values(): bu.archive_path() bu.archive_used() # set monitoring path: new_line = LineItem("monitoring path") self.target.stage.path.append(new_line) if not self.target.stage.focal_point: self.target.stage.set_focal_point(new_line) def prep_for_revision_interview(self): """ prep_revision_interview() -> None Function sets path for monitoring interview after projections are set. Function runs after pressing the "update" button on the model card. """ # set company as target co = self.get_company() co._stage = None self.target = co # preserve existing path and set fresh BU.used and BU.stage.path co.archive_path() co.archive_used() # set monitoring path: new_line = LineItem("revision path") self.target.stage.path.append(new_line) if not self.target.stage.focal_point: self.target.stage.set_focal_point(new_line) def register(self, bu, update_id=True, overwrite=False, recur=True): """ Model.register() -> None Manually add unit to bu_directory and ty_directory. Typically will only be used by set_company() After the company is set, the best way to add units to a model is to run bu.add_component(new_unit). If ``update_id`` is True, method will assign unit a new id in the model's namespace. Parameter should be True when adding a top-level unit, False when adding child units. """ # Make sure unit has an id in the right namespace. if update_id: bu._update_id(namespace=self.id.namespace, 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 = ("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=bu.id.bbid, name=self.bu_directory[bu.id.bbid].tags.name, mine=bu.tags.name, ) raise bb_exceptions.IDCollisionError(c) 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) if recur: for child_bu in bu.components.values(): self.register(child_bu, update_id=False, overwrite=overwrite, recur=recur) def set_company(self, company): """ Model.set_company() -> None Method sets the company as the top-level unit. """ self.bu_directory.clear() self.ty_directory.clear() self.register(company, update_id=True, overwrite=False, recur=True) self._company = company def set_timeline(self, time_line, resolution='monthly', name='default', overwrite=False): """ Model.set_timeline() -> None --``resolution`` is 'monthly', 'quarterly', 'annually' or any available summary resolution' --``name`` is 'default', 'actual', forecast', 'budget' Method adds the timeline for specified resolution (if any). """ key = (resolution, name) if key in self.timelines and not overwrite: c = ("TimeLine (resolution='{}', name='{}') " "already exists".format(*key)) raise KeyError(c) time_line.resolution = resolution time_line.name = name self.timelines[key] = time_line def start(self): """ Model.start() -> None Method sets instance._started (and ``started`` property) to True. """ self._started = True def transcribe(self, message): """ Model.transcribe(message) -> None Appends a tuple of (message ,time of call) to instance.transcript. """ time_stamp = time.time() # flatten message['topic_bbid'] = message['topic_bbid'].hex if message['q_out'] is not None: message['q_out'] = message['q_out'].to_database() record = (message, time_stamp) self.transcript.append(record)