示例#1
0
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)
示例#2
0
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
示例#3
0
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)
示例#4
0
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
示例#5
0
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)