Source code for tendril.production.order

#!/usr/bin/env python
# encoding: utf-8

# Copyright (C) 2015 Chintalagiri Shashank
#
# This file is part of tendril.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
Docstring for order
"""

import os
import arrow
from copy import deepcopy

from tendril.dox import docstore
from tendril.entityhub import serialnos

from tendril.entityhub.modules import get_module_instance
from tendril.entityhub.modules import get_module_prototype

from tendril.boms.outputbase import DeltaOutputBom
from tendril.boms.outputbase import CompositeOutputBom
from tendril.entityhub.snomap import SerialNumberMap

from tendril.entityhub.db.controller import SerialNoNotFound
from tendril.entityhub.entitybase import EntityNotFound
from tendril.entityhub.modules import ModuleInstanceTypeMismatchError

from tendril.dox.production import gen_production_order
from tendril.dox.production import gen_delta_pcb_am
from tendril.dox.production import gen_pcb_am
from tendril.dox.production import get_production_order_manifest_set

from tendril.inventory.indent import InventoryIndent

from tendril.utils.db import get_session
from tendril.utils.terminal import TendrilProgressBar
from tendril.utils.terminal import DummyProgressBar
from tendril.utils.files import yml as yaml


[docs]class ProductionOrderNotFound(EntityNotFound): pass
[docs]class DeltaValidationError(Exception): pass
[docs]class NothingToProduceError(Exception): pass
[docs]class ProductionActionBase(object): def __init__(self, *args, **kwargs): self._is_done = None self._scaffold = False self._session = kwargs.get('session') self.setup(*args, **kwargs)
[docs] def set_session(self, session): self._session = session
[docs] def unset_session(self): self._session = None
@property def scaffold(self): return self._scaffold @scaffold.setter def scaffold(self, value): self._scaffold = value
[docs] def setup(self, *args, **kwargs): raise NotImplementedError
[docs] def commit(self, outfolder=None, indent_sno=None, prod_ord_sno=None, register=False, pb_class=None, stacked_pb=False, leaf_pb=True, session=None): raise NotImplementedError
@property def obom(self): raise NotImplementedError @property def ident(self): raise NotImplementedError @property def refdes(self): raise NotImplementedError @property def modules(self): return [get_module_instance(x, self.ident, scaffold=self._scaffold, session=self._session) for x in self.refdes] @property def order_lines(self): raise NotImplementedError
[docs]class DeltaProductionAction(ProductionActionBase): def __init__(self, delta_order, force=False): self._sno = None self._original = None self._target = None self._orig_modulename = None self._target_modulename = None super(DeltaProductionAction, self).__init__(delta_order, force)
[docs] def setup(self, delta_order, force): self._sno = delta_order['sno'] self._orig_modulename = delta_order['orig-cardname'] self._target_modulename = delta_order['target-cardname'] if self._orig_modulename == self._target_modulename: raise DeltaValidationError try: try: self._original = get_module_instance( self._sno, self._orig_modulename, session=self._session, scaffold=force ) self._target = get_module_prototype(self._target_modulename) self._is_done = False except ModuleInstanceTypeMismatchError: self._target = get_module_instance( self._sno, self._target_modulename, session=self._session, scaffold=force ) self._original = get_module_prototype(self._orig_modulename) self._is_done = True except: # TODO a second delta on the same serial number will # make the first one fail. The entire delta architecture # may need to be thought through. Additionally, this # structure only handles deltas between cardnames, and # will go to hell in a handbasket if there are any # temporal changes to cards. raise DeltaValidationError
[docs] def _generate_docs(self, manifestsfolder, indent_sno=None, prod_ord_sno=None, register=False, session=None): dmpath = gen_delta_pcb_am( self._orig_modulename, self._target_modulename, outfolder=manifestsfolder, sno=self._sno, indentsno=indent_sno, productionorderno=prod_ord_sno ) if register is True: docstore.register_document(serialno=self._sno, docpath=dmpath, doctype='DELTA ASSEMBLY MANIFEST', session=session)
[docs] def commit(self, outfolder=None, indent_sno=None, prod_ord_sno=None, register=False, pb_class=None, stacked_pb=False, leaf_pb=True, session=None): if self._is_done is True: raise NothingToProduceError self._generate_docs(outfolder, indent_sno, prod_ord_sno, register, session) if register is True: serialnos.set_serialno_efield( sno=self._sno, efield=self._target_modulename, session=session ) serialnos.link_serialno( child=self._sno, parent=prod_ord_sno, session=session ) self._original = get_module_prototype(self._orig_modulename) self._target = get_module_instance(self._sno, self._target_modulename, session=session) self._is_done = True
@property def delta_bom(self): original_obom = self._original.obom target_obom = self._target.obom delta_obom = DeltaOutputBom(original_obom, target_obom) return delta_obom.additions_bom @property def obom(self): return self.delta_bom @property def ident(self): return '{0} -> {1}'.format(self._original.ident, self._target.ident) @property def refdes(self): return [self._sno] @property def modules(self): return [get_module_instance(x, self._target_modulename, session=self._session) for x in self.refdes] @property def order_lines(self): target_prototype = get_module_prototype(self._target_modulename) ctx = target_prototype.strategy ctx['ident'] = self._target_modulename ctx['is_delta'] = True ctx['desc'] = self.ident ctx['sno'] = self._sno return [ctx] def __repr__(self): if self._is_done is True: done = 'DONE' elif self._is_done is False: done = 'NOT DONE' else: done = 'NOT DEFINED' return '<DeltaProductionAction {0} {1} {2}>'.format( self.ident, self.refdes, done )
[docs]class CardProductionAction(ProductionActionBase): def __init__(self, *args, **kwargs): self._ident = None self._qty = None self._prototype = None self._snos = None super(CardProductionAction, self).__init__(*args, **kwargs)
[docs] def setup(self, card, qty, snofunc): self._ident = card self._prototype = get_module_prototype(card) self._qty = qty self._snos = [] for idx in range(self._qty): # Registration is dependent on the snofunc, and consequently # the state of the corresponding snomap. self._snos.append(snofunc(self.ident))
[docs] def _generate_am(self, manifestsfolder, sno, prod_ord_sno, indent_sno, verbose=True, register=False, session=None): ampath = gen_pcb_am(self._ident, manifestsfolder, sno, productionorderno=prod_ord_sno, indentsno=indent_sno, scaffold=self.scaffold, verbose=verbose, session=session) if register is True: docstore.register_document(serialno=sno, docpath=ampath, doctype='ASSEMBLY MANIFEST', verbose=verbose, session=session)
[docs] def commit(self, outfolder=None, indent_sno=None, prod_ord_sno=None, register=False, pb_class=None, stacked_pb=False, leaf_pb=True, session=None): # Serial numbers will already have been written in. if self._prototype.strategy['genmanifest'] is True: if pb_class is None: pb_class = TendrilProgressBar if leaf_pb is True: pb = pb_class(max=len(self.refdes)) verbose = False else: pb = None verbose = True msg = "Generating Manifests and Linking for {0}".format(self.ident) print(msg) for card in self.modules: self._generate_am( outfolder, card.refdes, prod_ord_sno, indent_sno, verbose=verbose, register=register, session=session ) if register is True: serialnos.link_serialno( child=card.refdes, parent=prod_ord_sno, verbose=verbose, session=session ) if leaf_pb is True: pb.next(note=card.refdes) if leaf_pb is True: pb.finish()
@property def obom(self): obom = self._prototype.bom.create_output_bom(self.ident) obom.multiply(self._qty) return obom @property def ident(self): return self._ident @property def refdes(self): return self._snos @property def order_lines(self): rval = [] base_ctx = self._prototype.strategy base_ctx['ident'] = self.ident base_ctx['is_delta'] = False for card in self.modules: ctx = deepcopy(base_ctx) ctx['sno'] = card.refdes rval.append(ctx) return rval
[docs]class ProductionOrder(object): def __init__(self, sno=None, session=None): self._sno = sno self._card_actions = [] self._delta_actions = [] self._last_generated_at = None self._first_generated_at = None self._title = None self._desc = None self._indents = [] self._root_order_snos = [] self._sourcing_order_snos = [] self._snomap_path = None self._snomap = None self._cards = {} self._deltas = [] self._order_yaml_path = None self._yaml_data = None self._ordered_by = None try: self.load(session=session) self._defined = True except ProductionOrderNotFound: self._defined = False
[docs] def create(self, title=None, desc=None, cards=None, deltas=None, sourcing_order_snos=None, root_order_snos=None, ordered_by=None, order_yaml_path=None, snomap_path=None, force=False): # Load in the various parameters and such, creating the necessary # containers only. self._force = force if self._defined is True and self._force is False: raise Exception("This production order instance seems to be already " "done. You can't 'create' it again.") if order_yaml_path is not None: self._order_yaml_path = order_yaml_path with open(self._order_yaml_path, 'r') as f: self._yaml_data = yaml.load(f) self._load_order_yaml_data() if title: self._title = title if desc: self._desc = desc if cards: self._cards = cards if deltas: self._deltas = deltas if sourcing_order_snos: self._sourcing_order_snos = sourcing_order_snos if root_order_snos: self._root_order_snos = root_order_snos if ordered_by: self._ordered_by = ordered_by if snomap_path: self._snomap_path = snomap_path if len(self._cards) + len(self._deltas) == 0: raise NothingToProduceError
[docs] def process(self, session=None, **kwargs): force = self._force if self._defined is True and force is False: raise Exception("This production order instance seems to be already " "done. You can't 'create' it again.") if session is None: with get_session() as session: return self._process(session=session, **kwargs) else: return self._process(session=session, **kwargs)
[docs] def _process(self, outfolder=None, manifestsfolder=None, label_manager=None, register=False, force=False, pb_class=None, stacked_pb=False, leaf_pb=True, session=None): self._force = force if pb_class is None: pb_class = TendrilProgressBar if stacked_pb is True: pb = pb_class(max=8) else: pb = DummyProgressBar(max=8) pb.next(note="Constructing Resources for Production Order Generation") if outfolder is None: if self._order_yaml_path is not None: outfolder = os.path.split(self._order_yaml_path)[0] else: raise AttributeError('Output folder needs to be defined') if manifestsfolder is None: manifestsfolder = os.path.join(outfolder, 'manifests') if not os.path.exists(manifestsfolder): os.makedirs(manifestsfolder) if self._sno is None: self._sno = serialnos.get_serialno( series='PROD', efield=self._title, register=register, session=session ) # Create Snomap if self._snomap_path is not None: with open(self._snomap_path, 'r') as f: self._snomap = SerialNumberMap(yaml.load(f), self._sno) else: self._snomap = SerialNumberMap({}, self._sno) self._snomap.set_session(session=session) if register is True: self._snomap.enable_creation() indent_sno = self._snomap.get_sno('indentsno') if register is True: serialnos.link_serialno(child=indent_sno, parent=self.serialno, verbose=False, session=session) # Create cards and deltas and so forth pb.next(note="Constructing Production Order Actions") actions = self.card_actions + self.delta_actions pb.next(note="Executing Production Order Actions") for action in actions: if register is False: action.scaffold = True action.set_session(session=session) action.commit( outfolder=manifestsfolder, indent_sno=indent_sno, prod_ord_sno=self._sno, register=register, session=session, pb_class=pb_class, stacked_pb=stacked_pb, leaf_pb=leaf_pb, ) self._snomap.disable_creation() pb.next(note="Constructing Composite Output BOM") cobom = CompositeOutputBom(self.bomlist) # Assume Indent is non-empty. # Create indent pb.next(note="Creating Indent") indent = InventoryIndent(indent_sno, verbose=False, session=session) indent.create(cobom, title="FOR {0}".format(self.serialno), desc=None, indent_type='production', requested_by=self._ordered_by, force=force) indent.define_auth_chain(prod_order_sno=self.serialno, session=session, prod_order_scaffold=True) indent.process(outfolder=outfolder, register=register, verbose=False, session=session) self._indents.append(indent) pb.next(note="Generating Production Order Document") # Make production order doc self._last_generated_at = arrow.utcnow().isoformat() if self._first_generated_at is None: self._first_generated_at = arrow.utcnow().isoformat() self._dump_order_yaml(outfolder=outfolder, register=register, session=session) self._generate_doc(outfolder=outfolder, register=register, session=session) pb.next(note="Generating Labels") self.make_labels(label_manager=label_manager, pb_class=pb_class, stacked_pb=stacked_pb, leaf_pb=leaf_pb) pb.next(note="Finalizing Production Order") for action in actions: action.scaffold = False action.unset_session() self._snomap.dump_to_file(outfolder) self._snomap.unset_session() if register is True: docstore.register_document( serialno=self.serialno, docpath=os.path.join(outfolder, 'snomap.yaml'), doctype='SNO MAP', efield=self.title, verbose=False, session=session ) pb.finish() self._defined = True
[docs] def _generate_doc(self, outfolder=None, register=False, session=None): outpath = gen_production_order( outfolder, self.serialno, self._yaml_data, self.lines, sourcing_orders=self._sourcing_order_snos, verbose=False, root_orders=self.root_order_snos ) if register is True: docstore.register_document( serialno=self.serialno, docpath=outpath, doctype='PRODUCTION ORDER', efield=self.title, verbose=False, session=session )
[docs] def _build_yaml_data(self): if self._yaml_data is None: self._yaml_data = {} self._yaml_data['title'] = self._title self._yaml_data['desc'] = self._desc self._yaml_data['cards'] = self._cards self._yaml_data['deltas'] = self._deltas self._yaml_data['sourcing_order_snos'] = self._sourcing_order_snos self._yaml_data['root_order_snos'] = self._root_order_snos self._yaml_data['first_generated_at'] = self._first_generated_at self._yaml_data['last_generated_at'] = self._last_generated_at self._yaml_data['ordered_by'] = self._ordered_by self._yaml_data['prod_order_sno'] = self._sno
[docs] def _dump_order_yaml(self, outfolder=None, register=False, session=None): self._build_yaml_data() with open(os.path.join(outfolder, 'order.yaml'), 'w') as f: f.write(yaml.dump(self._yaml_data, default_flow_style=False)) if register is True: docstore.register_document( serialno=self.serialno, docpath=os.path.join(outfolder, 'order.yaml'), doctype='PRODUCTION ORDER YAML', verbose=False, efield=self.title, session=session )
[docs] def _load_snomap_legacy(self): # New form should construct directly from DB snomap_path = docstore.get_docs_list_for_sno_doctype( serialno=self._sno, doctype='SNO MAP', one=True ).path with docstore.docstore_fs.open(snomap_path, 'r') as f: snomap_data = yaml.load(f) self._snomap = SerialNumberMap(snomap_data, self._sno)
[docs] def _load_order_yaml(self): try: order_path = docstore.get_docs_list_for_sno_doctype( serialno=self._sno, doctype='PRODUCTION ORDER YAML', one=True ).path except SerialNoNotFound: raise ProductionOrderNotFound with docstore.docstore_fs.open(order_path, 'r') as f: self._yaml_data = yaml.load(f)
[docs] def _load_order_yaml_data(self): # Following keys are ignored / deprecated: # - register # - halt_on_shortage # - include_refbom_for_no_am # - force_labels # # All of these are more about run control than defining # the production order, and thus are left to the run control # code to work out. The order.yaml files are now only # going to define things that are not temporally transient. self._title = self._yaml_data.get('title') self._desc = self._yaml_data.get('desc', self._title) self._cards = self._yaml_data.get('cards', {}) self._deltas = self._yaml_data.get('deltas', []) self._sourcing_order_snos = self._yaml_data.get('sourcing_orders', []) self._root_order_snos = self._yaml_data.get('root_orders', []) self._last_generated_at = self._yaml_data.get('last_generated_at', None) self._first_generated_at = self._yaml_data.get('first_generated_at', None) self._ordered_by = self._yaml_data.get('ordered_by', None) self._sno = self._yaml_data.get('prod_order_sno', self._sno)
[docs] def _load_legacy(self): if self._sno is None: raise ProductionOrderNotFound self._load_order_yaml() self._load_order_yaml_data() self._load_snomap_legacy()
[docs] def _load_from_db(self, session): raise ProductionOrderNotFound
[docs] def load_from_db(self, session=None): if not session: with get_session() as session: self._load_from_db(session) else: self._load_from_db(session)
[docs] def load(self, session=None): # Retrieve old production orders. If process is called on a loaded # production order, it'll overwrite whatever came before. try: self.load_from_db(session=session) except ProductionOrderNotFound: pass self._load_legacy()
@property def card_orders(self): return self._cards @property def card_actions(self): # This first len(..) bit is a litte dicey. if not len(self._card_actions): for card, qty in self.card_orders.iteritems(): self._card_actions.append( CardProductionAction(card, qty, self._snomap.get_sno) ) return self._card_actions @property def card_boms(self): return [x.obom for x in self.card_actions] @property def cards(self): rval = [] for action in self.card_actions: rval.extend(action.modules) return rval @property def card_lines(self): rval = [] for action in self.card_actions: rval.extend(action.order_lines) return rval @property def delta_orders(self): return self._deltas @property def delta_actions(self): # This first len(..) bit is a litte dicey. if not len(self._delta_actions): for delta in self._deltas: self._delta_actions.append( DeltaProductionAction(delta, force=self._force) ) return self._delta_actions @property def delta_boms(self): return [x.obom for x in self.delta_actions] @property def deltas(self): # This will return the original card if the delta hasn't yet been # processed. Consider the inconsistency of this behavior and see what # to do about it. rval = [] for action in self.delta_actions: rval.extend(action.modules) return rval @property def delta_lines(self): rval = [] for action in self.delta_actions: rval.extend(action.order_lines) return rval @property def bomlist(self): return self.card_boms + self.delta_boms @property def lines(self): return self.card_lines + self.delta_lines @property def collated_manifests_pdf(self): return get_production_order_manifest_set(self.serialno) @property def title(self): return self._title @property def desc(self): if self._desc is None: return 'Production Order for {0}'.format(self._title) else: return self._desc @property def serialno(self): return self._sno @property def root_orders(self): pass @property def root_order_snos(self): if isinstance(self._root_order_snos, list): return self._root_order_snos else: return [self._root_order_snos] @property def indent_snos(self): if 'indentsno' in self._snomap.map_keys(): return self._snomap.mapped_snos('indentsno') else: return [] @property def indents(self): if self._indents is None: from tendril.inventory.indent import InventoryIndent return [InventoryIndent(x) for x in self.indent_snos] else: return self._indents @property def docs(self): return docstore.get_docs_list_for_serialno(serialno=self.serialno) @property def status(self): return
[docs] def make_labels(self, label_manager=None, include_all_indents=False, include_main_indent=False, pb_class=None, stacked_pb=False, leaf_pb=True): cards = self.cards deltas = self.deltas if pb_class is None: pb_class = TendrilProgressBar if leaf_pb is True: pbmax = len(cards) + len(deltas) if include_all_indents is True: pbmax += len(self.indent_snos) elif include_main_indent is True: pbmax += 1 pb = pb_class(max=pbmax) else: pb = None if include_all_indents is True: for indent in self.indents: if leaf_pb is True: pb.next( note='Labels for Indent {0}'.format(indent.serialno) ) indent.make_labels(label_manager=label_manager) elif include_main_indent is True: if len(self.indent_snos): indent = self.indents[-1] if leaf_pb is True: pb.next( note='Labels for Indent {0}'.format(indent.serialno) ) indent.make_labels(label_manager=label_manager) for card in cards: if leaf_pb is True: pb.next(note='Label for Card {0}'.format(card.refdes)) card.make_labels(label_manager=label_manager) for delta in deltas: if leaf_pb is True: pb.next(note='Label for Delta {0}'.format(delta.refdes)) delta.make_labels(label_manager=label_manager) if leaf_pb is True: pb.finish()
def __repr__(self): return '<Production Order {0} {1}>'.format(self.serialno, self.title)