예제 #1
0
    def __init__(self, parent=None, period=None):
        self.id = ID()  # does not get its own bbid, just holds namespace

        # parent for Financials is BusinessUnit
        self.relationships = Relationships(self, parent=parent)

        self._period = period
        self._restricted = False
        self._full_order = self.DEFAULT_ORDER.copy()

        statements = [Statement(name=self.OVERVIEW_NAME, parent=self, 
                                period=period),
                      Statement(name=self.INCOME_NAME, parent=self, 
                                period=period),
                      CashFlow(name=self.CASH_NAME, parent=self, 
                               period=period),
                      Statement(name=self.VALUATION_NAME, parent=self, 
                                period=period, compute=False),
                      BalanceSheet(name=self.START_BAL_NAME, parent=self, 
                                   period=period),
                      BalanceSheet(name=self.ENDING_BAL_NAME, parent=self, 
                                   period=period)]

        self._statement_directory = dict()
        for stmt in statements:
            self._statement_directory[stmt.name.casefold()] = stmt

        self.update_statements = list()
예제 #2
0
    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
예제 #3
0
    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
예제 #4
0
    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()
예제 #5
0
    def from_database(cls, data, statement):
        """


        LineItem.from_database() -> None

        **CLASS METHOD**

        Method deserializes all LineItems belonging to ``statement``.
        """
        # first pass: create a dict of lines

        new = cls(parent=None, )
        new.tags = Tags.from_database(data['tags'])

        id_str = data['driver_id']
        if id_str:
            new._driver_id = ID.from_database(id_str).bbid

        # defer resolution of .xl
        new.xl_data = xl_mgmt.LineData(new)
        new.xl_format = xl_mgmt.LineFormat.from_database(data['xl_format'])

        new.summary_type = data['summary_type']
        new.summary_count = data['summary_count']
        new._consolidate = data['consolidate']
        new._replica = data['replica']
        new._include_details = data['include_details']
        new._sum_details = data['sum_details']
        new.log = data['log']

        new.guide = Guide.from_database(data['guide'])
        new.id.bbid = ID.from_database(data['bbid']).bbid

        position = data['position']
        position = int(position) if position else None
        new.position = position

        workspace = data.get('workspace', None)
        if workspace and workspace != 'null':
            new.workspace.update(workspace)

        usage = data.get('usage', None)
        if usage and usage != 'null':
            new.usage = LineItemUsage.from_database(usage)

        old_magic_keys = {
            "kpi", "covenants", "financials", "overall", "business summary"
        }
        if 'show on report' in new.tags.all or (new.tags.all & old_magic_keys):
            new.usage.show_on_report = True

        if "business summary" in new.tags.all:
            new.usage.show_on_card = True

        if 'monitor' in new.tags.all:
            new.usage.monitor = True

        return new
예제 #6
0
    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')
예제 #7
0
    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
예제 #8
0
    def __init__(self,
                 name=None,
                 priority=DEFAULT_PRIORITY_LEVEL,
                 quality=DEFAULT_QUALITY_REQUIREMENT):
        PrintAsLine.__init__(self)
        TagsMixIn.__init__(self, name)

        self.guide = Guide(priority, quality)
        self.relationships = Relationships(self)
        self.id = ID()
예제 #9
0
class Step(PrintAsLine, TagsMixIn):
    """

    Class for tracking logical steps. Has the tags and guide interface of
    LineItem but doesn't commit to a numeric value. Pretty lightweight and
    flexible.
    ====================  ======================================================
    Attribute             Description
    ====================  ======================================================

    DATA:
    guide                 instance of Guide
    relationships         instance of Relationships class

    FUNCTIONS:
    pre_format()          sets instance.formatted to a line with a checkbox
    ====================  ======================================================
    """
    DEFAULT_PRIORITY_LEVEL = 1
    DEFAULT_QUALITY_REQUIREMENT = 5

    def __init__(self,
                 name=None,
                 priority=DEFAULT_PRIORITY_LEVEL,
                 quality=DEFAULT_QUALITY_REQUIREMENT):
        PrintAsLine.__init__(self)
        TagsMixIn.__init__(self, name)

        self.guide = Guide(priority, quality)
        self.relationships = Relationships(self)
        self.id = ID()

    def to_database(self, **kwargs):
        result = dict()
        result['tags'] = self.tags.to_database()
        result['guide'] = self.guide.to_database()

        return result

    @classmethod
    def from_database(cls, data):
        result = cls()
        result.tags = Tags.from_database(data['tags'])
        result.guide = Guide.from_database(data['guide'])

        return result

    def pre_format(self, **kargs):
        #custom formatting logic
        if self.tags.name:
            kargs["name"] = self.tags.name
        self.formatted = printing_tools.format_completed(self, **kargs)

    def register(self, namespace):
        self.id.set_namespace(namespace)
예제 #10
0
    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
예제 #11
0
    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()
예제 #12
0
    def from_database(cls, portal_data, taxo_dir):
        """


        Taxonomy.from_database() -> TaxoDir

        --``portal_data`` is a dictionary containing serialized TaxoDir data
        --``model`` is the Model instance the new TaxoDir will be attached to

        Method deserializes TaxoDir into a rich object from flat portal data.
        """
        new = cls(taxo_dir)

        for taxo_unit in portal_data:
            key_list = taxo_unit.pop('keys')
            temp = new
            for k in key_list:
                this_dict = temp
                if k in this_dict:
                    temp = dict.__getitem__(this_dict, k)
                else:
                    dict.__setitem__(this_dict, k, cls(taxo_dir))
                    temp = dict.__getitem__(this_dict, k)

            id_hex = taxo_unit['bbid']
            bbid = ID.from_database(id_hex).bbid if id_hex else None
            bu = new.taxo_dir.get(bbid)
            dict.__setitem__(this_dict, k, bu)

        return new
예제 #13
0
    def __init__(self, start_date, end_date, parent=None, **kargs):
        # TimePeriodBase.__init__(self, start_date, end_date, model=model)
        TagsMixIn.__init__(self)

        self.start = start_date
        self.end = end_date

        self.financials = dict()

        self.id = ID()
        self.relationships = Relationships(self)
        self.relationships.set_parent(parent)

        self.past_end = None
        self.next_end = None

        self.parameters = Parameters()
        self.unit_parameters = Parameters()

        self._line_item_storage = dict()
        # {"value": value of any primitive type,
        #  "xl_data": flat LineData object without styles info,
        #  "hardcoded": bool}

        self.complete = True
        self.periods_used = 1
예제 #14
0
    def __init__(self, name=None, value=None, parent=None, period=None):
        BaseFinancialsComponent.__init__(self,
                                         name=name,
                                         parent=parent,
                                         period=period)

        self._local_value = None

        self.guide = Guide(priority=3, quality=1)
        self.log = []
        self.position = None

        # summary_type determines how the line is summarized
        self.summary_type = 'sum'

        # summary_count is used for summary_type == 'average'
        self.summary_count = 0

        self._consolidate = True
        self._replica = False
        self._hardcoded = False
        self._include_details = True
        self._sum_details = True
        self.id = ID()
        self._driver_id = None

        if value is not None:
            self.set_value(value, self.SIGNATURE_FOR_CREATION)

        self.workspace = dict()
        self.usage = LineItemUsage()
        self.xl_data = xl_mgmt.LineData(self)
        self.xl_format = xl_mgmt.LineFormat()
예제 #15
0
    def from_database(cls, portal_data, statement):
        target = portal_data.pop('target')
        line_item = LineItem.from_database(portal_data, statement)

        new = cls(None)
        new.__dict__.update(line_item.__dict__)
        new.target = ID.from_database(target).bbid

        return new
예제 #16
0
    def __init__(self,
                 model,
                 resolution='monthly',
                 name='default',
                 interval=1):
        dict.__init__(self)
        self.id = ID()
        # Timeline objects support the id interface and pass the model's id
        # down to time periods. The Timeline instance itself does not get
        # its own bbid.

        self.model = model
        self.resolution = resolution
        self.name = name
        self.interval = interval
        self.master = None
        self.parameters = Parameters()
        self.has_been_extrapolated = False
        self.ref_date = None

        self.id.set_namespace(model.id.bbid)
예제 #17
0
    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
예제 #18
0
    def process(self, message, *pargs, **kargs):
        """


        Analyst.process(message) -> message


        Method works to improve the model until it's either (i) good enough to
        stop work altogether or (ii) a question for the user comes up and the
        Engine needs to pause work.
        """
        n = 0
        message = self.choose_direction(message, *pargs, **kargs)
        # use choose_direction() to for substantive work. method also weeds
        # out messages that are ready for portal delivery right away.
        yenta.diary.clear()
        while self.status in [TOPIC_NEEDED, PENDING_RESPONSE]:
            #
            model = message[0]
            #
            if self.status == PENDING_RESPONSE:
                topic_bbid_hex = model.transcript[-1][0]["topic_bbid"]
                topic_bbid = ID.from_database(topic_bbid_hex).bbid
                topic = yenta.TM.local_catalog.issue(topic_bbid)
                logger.info('{} {}'.format(self.status, topic.source))
                message = topic.process(message)
            #
            elif self.status == TOPIC_NEEDED:
                topic = yenta.select_topic(model)
                if topic:
                    logger.info('{} {}'.format(self.status, topic.source))
                    message = topic.process(message)
                else:
                    pass
                    # Yenta.select_topic() returned None for Topic, which means
                    # it couldn't find any matches in the Topic Catalog. In such
                    # an event, Yenta notes dry run on focal point and IC shifts
                    # to the next focal point
            message = self.choose_direction(message, *pargs, **kargs)
            # the engine has done more work on the model. use choose_direction()
            # to see if it can stop or needs to continue.
            #
            n = n + 1
            if n > self.max_cycles:
                break
            # circuit-breaker logic
        #
        return message
예제 #19
0
    def find_line(self, line_id, statement_attr):
        """


        Financials.find_line() -> LineItem

        --``line_id`` bbid of line

        Finds a LineItem across all statements by its bbid.
        """
        if isinstance(line_id, str):
            line_id = ID.from_database(line_id).bbid

        statement = self.get_statement(statement_attr)
        if statement:
            for line in statement.get_full_ordered():
                if line.id.bbid == line_id:
                    return line

        raise bb_exceptions.StructureError(
            'Could not find line with id {}'.format(line_id)
        )
예제 #20
0
    def copy(self, clean=False):
        """


        Financials.copy() -> Financials


        Return a deep copy of instance.

        Method starts with a shallow copy and then substitutes deep copies
        for the values of each attribute in instance.ORDER
        """
        new_instance = Financials()
        new_instance._full_order = self._full_order.copy()

        for key, stmt in self._statement_directory.items():
            new_statement = stmt.copy(clean=clean)
            new_statement.relationships.set_parent(new_instance)
            new_instance._statement_directory[key] = new_statement

        new_instance.id = ID()
        new_instance.register(self.id.namespace)

        return new_instance
예제 #21
0
    def from_database(cls, portal_data, model, **kargs):
        """

        TimeLine.from_database(portal_data) -> TimeLine

        **CLASS METHOD**

        Method extracts a TimeLine from portal_data.
        """
        if isinstance(portal_data['period_start'], str):
            period_start = date_from_iso(portal_data['period_start'])
            period_end = date_from_iso(portal_data['period_end'])
        else:
            period_start = portal_data['period_start']
            period_end = portal_data['period_end']

        new = cls(period_start, period_end)

        new.complete = portal_data.get('complete') or False
        new.periods_used = portal_data.get('periods_used') or 1

        new.parameters.add(
            Parameters.from_database(portal_data['parameters'],
                                     target='parameters'))

        new._inflate_line_storage(portal_data['financials_values'])

        # convert unit_parameters keys to UUID
        for k, v in Parameters.from_database(portal_data['unit_parameters'],
                                             target='unit_parameters').items():
            new.unit_parameters.add({ID.from_database(k).bbid: v})

        time_line = kargs['time_line']
        time_line.add_period(new)

        return new
예제 #22
0
 def __init__(self):
     self.id = ID()
     self.directory = dict()
     self.by_name = dict()
예제 #23
0
    def from_database(cls, portal_data):
        new = cls()
        new.__dict__.update(portal_data)
        new.used = [ID.from_database(id).bbid for id in portal_data['used']]

        return new
예제 #24
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)
예제 #25
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
예제 #26
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)
예제 #27
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
예제 #28
0
    def from_database(cls, portal_data, financials):
        """

        Statement.from_database(portal_data) -> Statement

        **CLASS METHOD**

        Method extracts a Statement from portal_data.
        """
        new = cls(parent=financials)
        new.tags = Tags.from_database(portal_data['tags'])

        if portal_data['bbid'] is not None:
            new.id.bbid = ID.from_database(portal_data['bbid']).bbid

        # deserialize all LineItems
        catalog = dict()
        for row in portal_data['lines']:
            if row.get('link'):
                new_line = Link.from_database(row, new)
            elif 'driver_id' in row:
                new_line = LineItem.from_database(row, new)
            else:
                new_line = Step.from_database(row)

            parent_id = row.get('parent_bbid', None)
            if parent_id is None and new.id.bbid is not None:
                # no parent id, top-level line belongs to statement
                parent_id = new.id.bbid.hex

            if parent_id:
                par_id = ID.from_database(parent_id).bbid
            else:
                par_id = None

            sub_lines = catalog.setdefault(par_id, list())
            sub_lines.append(new_line)

        if catalog:

            def build_line_structure(seed, catalog):
                details = catalog.pop(seed.id.bbid, list())
                for line in details:
                    old_bbid = line.id.bbid
                    position = getattr(line, 'position', None)
                    seed.add_line(line, position=position, noclear=True)
                    line.id.bbid = old_bbid

                    build_line_structure(line, catalog)

            build_line_structure(new, catalog)

        # DISPLAY TYPE
        stmt_type = portal_data.get("display_type", None)
        if stmt_type and stmt_type != "null":
            new.display_type = stmt_type

        if new.display_type == "regular" and new.name:
            if "covenant" in new.name.casefold():
                new.display_type = new.COVENANT_TYPE

            if "kpi" in new.name.casefold():
                new.display_type = new.KPI_TYPE

        # Visible attribute
        visible = portal_data.get("visible", None)
        if isinstance(visible, bool):
            new.visible = visible

        # Behavioral settings
        compute = portal_data.get("compute", None)
        if compute != "null" and compute is not None:
            new.compute = compute

        balance_sheet = portal_data.get("balance_sheet", None)
        if balance_sheet != "null" and balance_sheet is not None:
            new.balance_sheet = balance_sheet

        return new
예제 #29
0
class Financials:
    """

    A class that organizes and operates on financial statements.
    ====================  ======================================================
    Attribute             Description
    ====================  ======================================================

    DATA:
    cash                  p; returns cash flow Statement for instance
    ending                p; returns ending BalanceSheet for instance
    full_order            p; returns list of instance statements in display order
    full_ordered          p; returns list of Statements in order of full_order
    has_valuation         p; returns bool on whether instance has valuation data
    id                    instance of ID object, holds unique BBID for instance
    income                p; returns income Statement for instance
    overview              p; returns overview Statement for instance
    period                p; returns TimePeriod that instance belongs to
    relationships         instance of Relationships class
    starting              p; returns starting BalanceSheet for instance
    update_statements     list; holds onto state info in monitoring interviews
    valuation             p; returns valuation Statement for instance

    CLASS DATA:
    CASH_NAME             str; default name of the cash flow statement
    DEFAULT_ORDER         list; default ordering of statements
    ENDING_BAL_NAME       str; default name of ending balance sheet
    INCOME_NAME           str; default name of income statement
    OVERVIEW_NAME         str; default name of overview statement
    START_BAL_NAME        str; default name of starting balance sheet
    VALUATION_NAME        str; default name of valuation statement

    FUNCTIONS:
    to_database           creates a flattened version of Financials for database
    add_statement         adds a statement to the instance
    check_balance_sheet   ensures matching structure between start and end balance
    copy                  returns deep copy
    find_line             uses provided information to locate a LineItem and returns it
    get_covenant_statements returns list of instances covenant statements
    get_kpi_statements      returns list of instances kpi statements
    get_regular_statements  returns list of instances regular statements
    get_statement           returns a statement with the given name
    populate_from_stored_values populates LineItems with values from .period
    register              registers instance and components in a namespace
    reset                 resets instance and all components
    restrict              restricts structural changes to instance and components
    set_order             sets statement order on the instance

    CLASS METHODS:
    from_database()       class method, extracts Financials out of API-format
    ====================  ======================================================
    """
    OVERVIEW_NAME = "Overview"
    INCOME_NAME = "Income Statement"
    CASH_NAME = "Cash Flow Statement"
    VALUATION_NAME = "Valuation"
    START_BAL_NAME = "Starting Balance Sheet"
    ENDING_BAL_NAME = "Ending Balance Sheet"

    DEFAULT_ORDER = [OVERVIEW_NAME, INCOME_NAME, CASH_NAME, START_BAL_NAME,
                     ENDING_BAL_NAME, VALUATION_NAME]

    def __init__(self, parent=None, period=None):
        self.id = ID()  # does not get its own bbid, just holds namespace

        # parent for Financials is BusinessUnit
        self.relationships = Relationships(self, parent=parent)

        self._period = period
        self._restricted = False
        self._full_order = self.DEFAULT_ORDER.copy()

        statements = [Statement(name=self.OVERVIEW_NAME, parent=self, 
                                period=period),
                      Statement(name=self.INCOME_NAME, parent=self, 
                                period=period),
                      CashFlow(name=self.CASH_NAME, parent=self, 
                               period=period),
                      Statement(name=self.VALUATION_NAME, parent=self, 
                                period=period, compute=False),
                      BalanceSheet(name=self.START_BAL_NAME, parent=self, 
                                   period=period),
                      BalanceSheet(name=self.ENDING_BAL_NAME, parent=self, 
                                   period=period)]

        self._statement_directory = dict()
        for stmt in statements:
            self._statement_directory[stmt.name.casefold()] = stmt

        self.update_statements = list()

    @property
    def overview(self):
        return self.get_statement(self.OVERVIEW_NAME)

    @overview.setter
    def overview(self, value):
        self._statement_directory[self.OVERVIEW_NAME.casefold()] = value

    @property
    def income(self):
        return self.get_statement(self.INCOME_NAME)

    @income.setter
    def income(self, value):
        self._statement_directory[self.INCOME_NAME.casefold()] = value

    @property
    def cash(self):
        return self.get_statement(self.CASH_NAME)

    @cash.setter
    def cash(self, value):
        self._statement_directory[self.CASH_NAME.casefold()] = value

    @property
    def valuation(self):
        return self.get_statement(self.VALUATION_NAME)

    @valuation.setter
    def valuation(self, value):
        self._statement_directory[self.VALUATION_NAME.casefold()] = value

    @property
    def starting(self):
        return self.get_statement(self.START_BAL_NAME)

    @starting.setter
    def starting(self, value):
        self._statement_directory[self.START_BAL_NAME.casefold()] = value

    @property
    def ending(self):
        return self.get_statement(self.ENDING_BAL_NAME)

    @ending.setter
    def ending(self, value):
        self._statement_directory[self.ENDING_BAL_NAME.casefold()] = value

    @property
    def full_order(self):
        return self._full_order.copy()

    @property
    def full_ordered(self):
        result = []
        for name in self._full_order:
            statement = self.get_statement(name)
            result.append(statement)

        return result

    @property
    def has_valuation(self):
        return not self.valuation == Statement(self.VALUATION_NAME)

    @property
    def period(self):
        return self._period

    @period.setter
    def period(self, value):
        self._period = value
        for statement in self._statement_directory.values():
            if statement:
                if statement.relationships.parent is self:
                    statement.set_period(value)

    def __str__(self):
        header = ''

        period = self.period
        if period:
            if Equalities.multi_getattr(self, "relationships.parent", None):
                header = (
                    '{begin:^{width}}\n\n'
                    '{start:^{width}}\n'
                    '{close:^{width}}\n\n'
                ).format(
                    width=bb_settings.SCREEN_WIDTH,
                    begin='Financial statements for {}'.format(
                        self.relationships.parent.tags.name
                    ),
                    start='Period starting: {}'.format(period.start),
                    close='Period ending:   {}'.format(period.end),
                )

        content = []
        for statement in self.full_ordered:
            if statement is not None:
                content.append(str(statement))

        result = (
            '{header}'
            '{content}\n'
            '{border:^{width}}\n\n'
        ).format(
            width=bb_settings.SCREEN_WIDTH,
            header=header,
            content=''.join(content),
            border="***",
        )

        return result

    @classmethod
    def from_database(cls, portal_data, company, **kargs):
        """


        Financials.from_database(portal_data) -> Financials

        **CLASS METHOD**

        Method extracts Financials from portal_data.
        """
        period = kargs['period']
        new = cls(parent=company, period=period)
        new.register(company.id.bbid)

        us = portal_data['update_statements']
        new.update_statements = us

        # new._chef_order = portal_data['chef_order']
        # new._compute_order = portal_data['compute_order']
        # new._exclude_statements = portal_data['exclude_statements']
        new._full_order = portal_data['full_order']

        for data in portal_data['statements']:
            statement = Statement.from_database(
                data, financials=new
            )
            new._statement_directory[statement.name.casefold()] = statement

        return new

    def to_database(self):
        """


        Financials.to_database() -> dict

        Method yields a serialized representation of self.
        """
        self.check_balance_sheets()

        statements = []
        for name in self._full_order:
            statement = self.get_statement(name)
            if statement:
                data = statement.to_database()
                data['attr_name'] = name
                statements.append(data)

        result = {
            'statements': statements,
            # 'chef_order': self._chef_order,
            # 'compute_order': self._compute_order,
            # 'exclude_statements': self._exclude_statements,
            'full_order': self._full_order,
            'update_statements': self.update_statements,
        }
        return result

    def add_statement(self, name, statement=None, title=None, position=None,
                      compute=True, overwrite=False):
        """


        Financials.add_statement() -> None

        --``name`` is the string name for the statement attribute
        --``statement`` is optionally the statement to insert, if not provided
                        a blank statement will be added
        --``title`` is optionally the name to assign to the statement object,
                    if not provided ``name`` will be used
        --``position`` is optionally the index at which to insert the statement
                       in instance.full_order
        --``compute`` bool; default is True, whether or not to include the
                      statement in computation order  during fill_out (will be
                      computed between starting and ending balance sheets) or
                      whether to compute manually after

        Method adds a new statement to the instance and inserts it at specified
        position in instance.full_order.  If no position is provided, the
        new statement will be added at the end.
        """
        if name.casefold() in self._statement_directory and not overwrite:
            c = "%s already exists as a statement!" % name
            raise bb_exceptions.BlackbirdError(c)

        if not self._restricted:
            if not statement:
                use_name = title or name
                statement = Statement(use_name, period=self.period,
                                      compute=compute)

            statement.relationships.set_parent(self)

            self._statement_directory[name.casefold()] = statement

            if position:
                self._full_order.insert(position, name)
            else:
                self._full_order.append(name)

            if self.id.namespace:
                statement.register(self.id.namespace)

    def check_balance_sheets(self):
        """


        Financials.check_balance_sheets() -> None

        Method ensures that starting and ending balance sheets have matching
        structures.
        """

        start_lines = self.starting.get_full_ordered()
        start_names = [line.name for line in start_lines]

        end_lines = self.ending.get_full_ordered()
        end_names = [line.name for line in end_lines]

        if start_names != end_names:
            # only run the increments if there is a mismatch
            self.starting.increment(self.ending, consolidating=False,
                                    over_time=True)
            self.ending.increment(self.starting, consolidating=False,
                                  over_time=True)

    def copy(self, clean=False):
        """


        Financials.copy() -> Financials


        Return a deep copy of instance.

        Method starts with a shallow copy and then substitutes deep copies
        for the values of each attribute in instance.ORDER
        """
        new_instance = Financials()
        new_instance._full_order = self._full_order.copy()

        for key, stmt in self._statement_directory.items():
            new_statement = stmt.copy(clean=clean)
            new_statement.relationships.set_parent(new_instance)
            new_instance._statement_directory[key] = new_statement

        new_instance.id = ID()
        new_instance.register(self.id.namespace)

        return new_instance

    def find_line(self, line_id, statement_attr):
        """


        Financials.find_line() -> LineItem

        --``line_id`` bbid of line

        Finds a LineItem across all statements by its bbid.
        """
        if isinstance(line_id, str):
            line_id = ID.from_database(line_id).bbid

        statement = self.get_statement(statement_attr)
        if statement:
            for line in statement.get_full_ordered():
                if line.id.bbid == line_id:
                    return line

        raise bb_exceptions.StructureError(
            'Could not find line with id {}'.format(line_id)
        )

    def get_statement(self, name):
        """


        Financials.get_statement() -> Statement or None

        Method searches for a statement with the specified name (caseless
        search) in instance directory.  To maintain backwards compatibility
        with legacy Excel uploads, we also search for statements with names
        containing the provided name (e.g. name="income" will generally
        return the income statement, whose proper name is "income statement").

        If no exact name match is found and multiple partial matches are found
        an error is raised.
        """
        if isinstance(name, str):
            name = name.casefold()
            if name in self._statement_directory:
                return self._statement_directory[name]
            else:
                outs = list()
                for k in self._statement_directory.keys():
                    if name in k:
                        outs.append(self._statement_directory[k])

                if len(outs) == 1:
                    return outs[0]
                elif len(outs) > 1:
                    b = "ENTRY IS NOT A STATEMENT"
                    names = [s.name if isinstance(s, Statement) else b for s in outs]

                    c = "Statement with exact name not found. " \
                        "Multiple statements with partial matching" \
                        " name were found: " + ', '.join(names)
                    raise KeyError(c)

        return None

    def populate_from_stored_values(self, period):
        """


        Financials.populate_from_stored_values() -> None

        --``period`` is the TimePeriod from which to retrieve values

        Method uses financials data (values and excel info) stored in the
        period to fill in the line values in the instance.
        """
        tl = period.relationships.parent
        if len(tl.keys()) > 0:
            min_dt = min(tl.keys())
            first = tl[min_dt]
        else:
            first = None

        for statement in self.full_ordered:
            if statement is not None and (statement is not self.starting or period is first):
                for line in statement.get_full_ordered():
                    line.xl_data = LineData(line)

                    if not line._details or not line.sum_details:
                        value = period.get_line_value(line.id.bbid.hex)
                        line._local_value = value

                    hc = period.get_line_hc(line.id.bbid.hex)
                    line._hardcoded = hc

        buid = self.relationships.parent.id.bbid
        past = period.past
        future = period.future

        # Now check if fins exist in period.past
        if past:
            if buid in past.financials:
                # past financials already exist as rich objects,
                # so we can just link to existing ending balance sheet
                past_fins = past.financials[buid]
                self.starting = past_fins.ending
            else:
                # past financials have not yet been re-inflated, so we have to
                # make an Ending Balance Sheet and pretend it belongs to the
                # preceding period
                self.starting = self.ending.copy()
                self.starting.set_period(past)

                for line in self.starting.get_full_ordered():
                    line.xl_data = LineData(line)

                    if not line._details or not line.sum_details:
                        value = past.get_line_value(line.id.bbid.hex)
                        line._local_value = value

                    hc = past.get_line_hc(line.id.bbid.hex)
                    line._hardcoded = hc

        # And if fins exist in period.future
        if future:
            if buid in future.financials:
                future_fins = future.financials[buid]
                future_fins.starting = self.ending

    def register(self, namespace):
        """


        Financials.register() -> None

        --``namespace`` is the namespace to assign to instance

        Method sets namespace of instance, does not assign an actual ID.
        Registers statements on instance.
        """
        self.id.set_namespace(namespace)

        for statement in self._statement_directory.values():
            if statement:
                statement.register(self.id.namespace)

    def reset(self):
        """


        Financials.reset() -> None


        Reset each defined statement.
        """
        for statement in self.full_ordered:
            if statement:
                if statement.compute and statement is not self.starting:
                    statement.reset()

    def restrict(self):
        """


        Financials.restrict() -> None


        Restrict instance and each defined statement from altering their
        structure (no adding lines or removing lines).  This is used for
        period financials which should show SSOT structure.
        """
        self._restricted = True
        for statement in self._statement_directory.values():
            if statement:
                statement.restrict()

    def set_order(self, new_order):
        """


        Financials.set_order() -> None

        --``new_order`` is a list of strings representing the names of the
        statements on the instance in the order they should be displayed

        Method looks for default statement names in the list and replaces them
        with their standard cased versions.  Method ensures that starting
        balance sheet is placed appropriately (immediately preceding ending
        balance sheet).  Method ensures that all default statements are
        represented; statements not included in the upload are appended at the
        end of the order.
        """
        new_order = [name.casefold() for name in new_order]

        for idx, stmt in enumerate(new_order):
            for entry in self.DEFAULT_ORDER:
                if stmt in entry.casefold():
                    new_order[idx] = entry
                    break

        try:
            idx = new_order.index(self.ENDING_BAL_NAME)
        except ValueError:
            pass
        else:
            if new_order[idx-1] != self.START_BAL_NAME:
                new_order.insert(idx, self.START_BAL_NAME)

        for name in self.DEFAULT_ORDER:
            if name not in new_order:
                stmt = self.get_statement(name)
                stmt.visible = False
                new_order.append(name)

        self._full_order = new_order
예제 #30
0
class TimeLine(dict):
    """

    A TimeLine is a dictionary of TimePeriod objects keyed by ending date.
    The TimeLine helps manage, configure, and search TimePeriods.

    Unless otherwise specified, class expects all dates as datetime.date objects
    and all periods as datetime.timedelta objects.

    ====================  ======================================================
    Attribute             Description
    ====================  ======================================================

    DATA:
    current_period        P; pointer to the period that represents the present
    id                    instance of PlatformComponents.ID class, for interface
    master                TimePeriod; unit templates that fall outside of time
    name                  str; corresponds with Model.time_line key
    parameters            Parameters object, specifies shared parameters
    ref_date              datetime.date; reference date for the model
    resolution            string; 'monthly', 'annual'..etc. Model.time_line key

    FUNCTIONS:
    build()               populates instance with adjacent time periods
    clear()               delete content from past and future periods
    clear_future()        delete content from future periods
    extrapolate()         use seed to fill out all future periods in instance
    extrapolate_dates()   use seed to fill out a range of dates
    find_period()         returns period that contains queried time point
    get_segments()        split time line into past, present, and future
    get_ordered()         returns list of periods ordered by end point
    link()                connect adjacent periods
    revert_current()      go back to the prior current period
    update_current()      updates current_period for reference or actual date
    ====================  ======================================================
    """
    DEFAULT_PERIODS_FORWARD = 60
    DEFAULT_PERIODS_BACK = 1

    def __init__(self,
                 model,
                 resolution='monthly',
                 name='default',
                 interval=1):
        dict.__init__(self)
        self.id = ID()
        # Timeline objects support the id interface and pass the model's id
        # down to time periods. The Timeline instance itself does not get
        # its own bbid.

        self.model = model
        self.resolution = resolution
        self.name = name
        self.interval = interval
        self.master = None
        self.parameters = Parameters()
        self.has_been_extrapolated = False
        self.ref_date = None

        self.id.set_namespace(model.id.bbid)

    @property
    def current_period(self):
        """


        **property**


        Getter returns instance._current_period. Setter stores old value for
        reversion, then sets new value. Deleter sets value to None.
        """
        if len(self):
            cp = self.find_period(self.model.ref_date)
            return cp

    @property
    def first_period(self):
        if not self.keys():
            per = None
        else:
            min_date = min(self.keys())
            per = self[min_date]

        return per

    def __str__(self, lines=None):
        """


        Components.__str__(lines = None) -> str


        Method concatenates each line in ``lines``, adds a new-line character at
        the end, and returns a string ready for printing. If ``lines`` is None,
        method calls pretty_print() on instance.
        """
        if not lines:
            lines = views.view_as_time_line(self)
        line_end = "\n"
        result = line_end.join(lines)
        return result

    @classmethod
    def from_database(cls, portal_data, model, **kargs):
        """

        TimeLine.from_database(portal_data) -> TimeLine

        **CLASS METHOD**

        Method extracts a TimeLine from portal_data.
        """
        key = tuple(portal_data[k] for k in ('resolution', 'name'))
        new = cls(
            model,
            resolution=portal_data['resolution'],
            name=portal_data['name'],
        )
        new.master = model.taxo_dir

        if portal_data['interval'] is not None:
            new.interval = portal_data['interval']
        if portal_data['ref_date']:
            new.ref_date = portal_data['ref_date']
            if isinstance(new.ref_date, str):
                new.ref_date = date_from_iso(new.ref_date)
        if portal_data['has_been_extrapolated'] is not None:
            new.has_been_extrapolated = portal_data['has_been_extrapolated']
        if portal_data['parameters'] is not None:
            new.parameters = Parameters.from_database(
                portal_data['parameters'])

        for data in portal_data['periods']:
            period = TimePeriod.from_database(
                data,
                model=model,
                time_line=new,
            )

        return new

    def to_database(self):
        """

        TimeLine.to_database() -> dict

        Method yields a serialized representation of self.
        """
        periods = [period.to_database() for period in self.values()]
        result = {
            'periods': periods,
            'interval': self.interval,
            'ref_date': format(self.ref_date) if self.ref_date else None,
            'has_been_extrapolated': self.has_been_extrapolated,
            'parameters': list(self.parameters.to_database()),
        }
        return result

    def copy_structure(self):
        """


        TimeLine.copy_structure() -> TimeLine

        Method returns a copy of self linked to parent model and with the same
        layout.
        """
        result = type(self)(self.model)
        result.ref_date = copy.copy(self.ref_date)
        result.parameters = self.parameters.copy()
        for old_period in self.iter_ordered():
            new_period = old_period.copy(clean=True)
            result.add_period(new_period)

        if self.master:
            result.master = result[self.master.end]

        return result

    def build(
        self,
        ref_date=None,
        fwd=DEFAULT_PERIODS_FORWARD,
        back=DEFAULT_PERIODS_BACK,
        year_end=True,
    ):
        """


        TimeLine.build() -> None

         --``ref_date`` is datetime.date to use as the reference date for the
         timeline
        --``fwd`` is int number of periods to build forward of the ref_date
        --``back`` is int number of period to build before the ref_date
        --``year_end`` is bool for whether to build through the end of the year

        Method creates a chain of TimePeriods with adjacent start and end
        points. The chain is at least ``fwd`` periods long into the future
        and ``back`` periods long into the past. Forward chain ends on a Dec.

        Method expects ``ref_date`` to be a datetime.date object.

        Method sets instance.current_period to the period covering the reference
        date. Method also sets master to a copy of the current period.
        """
        if not ref_date:
            ref_date = self.model.ref_date
        self.ref_date = ref_date

        ref_month = ref_date.month
        ref_year = ref_date.year

        current_start_date = date(ref_year, ref_month, 1)

        # Make reference period
        fwd_start_date = self._get_fwd_start_date(ref_date)
        current_end_date = fwd_start_date - timedelta(1)
        current_period = TimePeriod(current_start_date,
                                    current_end_date,
                                    model=self.model)
        self.add_period(current_period)

        # Add master period
        self.master = self.model.taxo_dir

        # Now make the chain
        back_end_date = current_start_date - timedelta(1)
        # Save known starting point for back chain build before fwd changes it.

        # Make fwd chain
        i = 0
        while fwd or year_end:
            # pick up where ref period analysis leaves off
            curr_start_date = fwd_start_date
            fwd_start_date = self._get_fwd_start_date(curr_start_date)
            curr_end_date = fwd_start_date - timedelta(1)
            fwd_period = TimePeriod(curr_start_date,
                                    curr_end_date,
                                    model=self.model)
            self.add_period(fwd_period)
            i += 1
            if i >= fwd and (not year_end or fwd_period.end.month == 12):
                break
            # first line picks up last value in function scope, so loop
            # should be closed.

        # Make back chain
        for i in range(back):
            curr_end_date = back_end_date
            curr_start_date = date(curr_end_date.year, curr_end_date.month, 1)
            back_period = TimePeriod(curr_start_date,
                                     curr_end_date,
                                     model=self.model)
            self.add_period(back_period)
            # close loop:
            back_end_date = curr_start_date - timedelta(1)

    def clear(self):
        """


        TimeLine.clear() -> None


        Clear content from past and future, preserve current_period.
        """
        for period in self.iter_ordered():
            if period.end != self.current_period.end:
                period.clear()
        # have to dereference history
        # have to do so recursively, to make sure that none of the objects
        # retain their external pointers.

    def clear_future(self, seed=None):
        """


        TimeLine.clear_future() -> None


        Clear content from all periods after seed. Method expects a period as
        ``seed``, will use instance.current_period if seed is None.
        """
        if seed is None:
            seed = self.current_period
        for period in self.iter_ordered():
            if period.end > seed.end:
                period.clear()

    def copy(self):
        """


        TimeLine.copy() -> TimeLine


        Method returns a copy of the instance.
        """
        result = copy.copy(self)
        for key, value in self.items():
            result[key] = value.copy()
            result[key].relationships.set_parent(result)
        result.has_been_extrapolated = self.has_been_extrapolated
        return result

    def add_period(self, period):
        """


        Timeline.add_period() -> None

        --``period`` is a TimePeriod object

        Method configures period and records it in the instance under the
        period's end_date.
        """
        period = self._configure_period(period)
        self[period.end] = period

    def iter_ordered(self, open=None, exit=None, shut=None):
        """


        Timeline.iter_ordered() -> iter

        --``open`` date, soft start, if falls in period, iteration starts
        --``exit`` date, soft stop, if falls in period, last shown
        --``shut`` date, hard stop, if not exact period end, iteration stops

        Method iterates over periods in order, starting with the one in which
        ``open`` falls, and ending with the one including ``exit`` and
        not following ``shut``.
        """
        for end_date, period in sorted(self.items()):
            if open and open > period.end:
                continue
            if exit and exit < period.start:
                break
            if shut and shut < period.end:
                break
            yield period

    def get_ordered(self):
        """


        Timeline.get_ordered() -> list


        Method returns list of periods in instance, ordered from earliest to
        latest endpoint.
        """
        return list(self.iter_ordered())

    def extrapolate(self, seed=None, calc_summaries=True):
        """


        TimeLine.extrapolate() -> None


        Extrapolate current period to future dates.  Make quarterly and annual
        financial summaries.  Updates all summaries contained in
        instance.summaries.
        """
        print('--------EXTRAPOLATE----------')
        if seed is None:
            seed = self.current_period

        company = self.model.get_company()

        company.consolidate_fins_structure()

        if seed.past:
            company.recalculate(period=seed.past, adjust_future=False)
        company.recalculate(period=seed, adjust_future=False)

        summary_maker = SummaryMaker(self.model)

        for period in self.iter_ordered(open=seed.end):
            if period.end > seed.end:
                logger.info(period.end)

                # reset content and directories
                period.clear()

                # combine tags
                period.tags = seed.tags.extrapolate_to(period.tags)

                # propagate parameters from past to current
                period.combine_parameters()

                # copy and fill out content
                company.fill_out(period=period)

            if bb_settings.MAKE_ANNUAL_SUMMARIES and calc_summaries:
                if period.end >= self.current_period.end:
                    summary_maker.parse_period(period)

            # drop future periods that have been used up to keep size low
            if bb_settings.DYNAMIC_EXTRAPOLATION:
                if period.past and period.past.past:
                    if period.past.past.end > self.current_period.end:
                        period.past.past.financials.clear()

        if bb_settings.MAKE_ANNUAL_SUMMARIES and calc_summaries:
            summary_maker.wrap()

        # import devhooks
        # devhooks.picksize(self)
        self.has_been_extrapolated = True

    def extrapolate_dates(self, seed, dates, backward=False):
        """


        TimeLine.extrapolate_dates() -> None


        Method extrapolates seed to the first date in dates, then sequentially
        extrapolates remaining dates from each other.

        Method expects ``seed`` to be an instance of TimePeriod. Seed can be
        external to the caller TimeLine.

        Method expects ``dates`` to be a series of endpoints for the periods in
        instance the caller is targeting. In other words, instance must contain
        a period corresponding to each date in ``dates``.

        Dates can contain gaps. Method will always extrapolate from one date to
        its neighbor. Extrapolation works by requesting that each object in a
        content structure extrapolate itself and any of its subordinates.
        For time-sensitive objects like BusinessUnits, that process should
        automatically adjust to the target date regardless of how far away that
        date is from the instance's reference date.

        If ``work_backward`` is True, method will go through dates
        last-one-first.
        """
        #
        if backward:
            dates = dates[::-1]
            # Reverse order, so go from newest to oldest

        for i in range(len(dates)):

            date = dates[i]
            # With default arguments, start work at the period immediately
            # prior to the current period

            target_period = self[date]
            updated_period = seed.extrapolate_to(target_period)
            # extrapolate_to() always does work on an external object and leaves
            # the target untouched. Manually swap the old period for the new
            # period.

            if i == 0:
                updated_period = self._configure_period(updated_period)

                # On i == 0, extrapolating from the original seed. seed can be
                # external (come from a different model), in which case it would
                # use a different model namespace id for unit tracking.
                #
                # Accordingly, when extrapolating from the may-be-external seed,
                # use configure_period() to conform output to current model.
                #
                # Subsequent iterations of the loop will start w periods that are
                # already in the model, so method can leave their namespace id
                # configuration as is.

            self[date] = updated_period
            seed = updated_period

    def extrapolate_statement(self, statement_name, seed=None):
        """


        TimeLine.extrapolate_statement() -> None


        Extrapolates a single statement forward in time. DOES NOT MAKE
        SUMMARIES.
        """
        if seed is None:
            seed = self.current_period

        company = self.model.get_company()
        orig_fins = company.get_financials(period=seed)
        orig_statement = getattr(orig_fins, statement_name)
        orig_statement.reset()
        company.compute(statement_name, period=seed)

        for period in self.iter_ordered(open=seed.end):
            if period.end > seed.end:
                new_fins = company.get_financials(period=period)
                new_stat = new_fins.get_statement(statement_name)
                if new_stat is None:
                    # need to add statement
                    new_stat = orig_statement.copy(clean=True)
                    new_fins.add_statement(statement_name, statement=new_stat)
                    company.compute(statement_name, period=period)
                else:
                    # compute what is already there
                    company.compute(statement_name, period=period)

    def find_period(self, query):
        """


        TimeLine.find_period() -> TimePeriod


        Method returns a time period that includes query. ``query`` can be a
        POSIX timestamp (int or float), datetime.date object, or string in
        "YYYY-MM-DD" format.
        """
        if isinstance(query, date):
            q_date = query
        else:
            try:
                q_date = date.fromtimestamp(query)
            except TypeError:
                num_query = [int(x) for x in query.split("-")]
                # query is a string, split it
                q_date = date(*num_query)
        end_date = self._get_ref_end_date(q_date)
        result = self.get(end_date)
        return result

    def get_segments(self, ref_date=None):
        """


        TimeLine.get_segments() -> list


        Method returns a list of the past, present, and future segments of the
        instance, with respect to the ref_date. If ``ref_date`` is None, method
        counts current period as the present.

        output[0] = list of keys for periods before ref_date
        output[1] = list of ref period (len output[1] == 1)
        output[2] = list of keys for periods after ref_date
        """
        if not ref_date:
            ref_date = self.current_period.end
        ref_end = self._get_ref_end_date(ref_date)
        #
        dates = sorted(self.keys())
        ref_spot = dates.index(ref_end)
        future_dates = dates[(ref_spot + 1):]
        past_dates = dates[:ref_spot]
        result = [past_dates, [ref_end], future_dates]
        return result

    # *************************************************************************#
    #                           NON-PUBLIC METHODS                             #
    # *************************************************************************#

    def _get_fwd_start_date(self, ref_date):
        """


        TimeLine.get_fwd_start_date() -> datetime.date


        Method returns the starting date of the next month.
        """
        ref_month = ref_date.month
        ref_year = ref_date.year
        if ref_month == 12:
            result = date(ref_year + 1, 1, 1)
        else:
            result = date(ref_year, ref_month + 1, 1)
        return result

    def _get_ref_end_date(self, ref_date):
        """


        TimeLine._get_ref_end_date() -> datetime.date


        Method returns the last date of the month that contains ref_date.
        """
        result = None
        fwd_start_date = self._get_fwd_start_date(ref_date)
        ref_end_date = fwd_start_date - timedelta(1)
        result = ref_end_date
        return result

    def _configure_period(self, period):
        """


        Timeline._configure_period() -> period


        Method sets period's namespace id to that of the TimeLine, then returns
        period.
        """
        model_namespace = self.id.namespace
        period.id.set_namespace(model_namespace)
        # Period has only a pointer to the Model.namespace_id; periods don't
        # have their own bbids.
        period.relationships.set_parent(self)

        # end dates of the past and future periods
        try:
            period.past_end = max(
                (day for day in self.keys() if day < period.end))
        except:
            period.past_end = None
        try:
            period.next_end = min(
                (day for day in self.keys() if day > period.end))
        except:
            period.next_end = None
        # link adjacent periods
        if period.past_end:
            past_period = self[period.past_end]
            past_period.next_end = period.end
        if period.next_end:
            next_period = self[period.next_end]
            next_period.past_end = period.end

        return period