class Part(BaseRecord): __tablename__ = 'parts' part_identifier = Column(db.Integer, nullable=False) name = Column(db.String, nullable=True) components = relationship('PartComponent', foreign_keys='PartComponent.parent_id') current_best_estimate = Column(db.Float, default=0.0, nullable=False) uncertainty = Column(db.Float, default=0.0, nullable=False) predicted_best_estimate = Column(db.Float, default=0.0, nullable=False) design_id = reference_col('designs') design = relationship('Design', back_populates='parts') material_id = reference_col('materials', nullable=True) material = relationship('Material') material_specification_id = reference_col('material_specifications', nullable=True) material_specification = relationship('MaterialSpecification') inseparable_component = Column(db.Boolean, default=False) procedures = relationship('Procedure', secondary='procedures_parts') __table_args__ = (db.UniqueConstraint( 'part_identifier', 'design_id', name='part_identifier_design_unique'), ) def __init__(self, **kwargs): super().__init__() db.Model.__init__(self, **kwargs) @property def part_number(self): return '{0}-{1}'.format(self.design.design_number, self.part_identifier) @property def revision(self): return self.design.revision @classmethod def get_all_parts(cls): results = cls.query.all() return results @classmethod def typeahead_search(cls, query, part_id): query = '%{0}%'.format( query) # Pad query for an ILIKE search for design_number and # Search in parts for design_numbers, part_identifiers or associated design names that match # Union with inseparable components for this specific revision of design sql_part_ids = ''' WITH matching_designs AS ( SELECT d.design_number AS dn, MAX(d.revision) AS rev FROM parts p, designs d WHERE p.design_id = d.id AND (SELECT CONCAT(d.design_number, '-', cast(p.part_identifier as text), ' ', d.name) ILIKE :query) GROUP BY d.design_number ) SELECT p.id FROM designs d, parts p, matching_designs md WHERE p.design_id = d.id AND d.design_number = md.dn AND d.revision = md.rev AND NOT inseparable_component UNION SELECT p.id FROM parts p WHERE inseparable_component AND design_id = (SELECT design_id FROM parts WHERE id = :part_id) ''' # Get ids of parts belonging to different revs of same design number sql_design_number_part_ids = 'SELECT id FROM designs WHERE design_number = (SELECT design_number FROM designs WHERE id = (SELECT design_id FROM parts WHERE id = :part_id))' # Get ids of parts belonging to inseparable components for this specific rev of design, EXCEPT sql_inseparable_components_part_ids = 'SELECT id FROM parts WHERE inseparable_component AND design_id = (SELECT design_id FROM parts WHERE id = :part_id)' # Get ids of parts already added as part component to this part, UNION sql_already_added_part_ids = 'SELECT part_id FROM part_components WHERE parent_id = :part_id AND part_id IS NOT NULL' # Get ids of parts where this part is an NLA, UNION sql_nla_part_ids = 'SELECT parent_id FROM part_components WHERE part_id = :part_id' # Use the above selects to get part ids to exclude sql_exclude_part_ids = 'SELECT id FROM parts WHERE design_id IN ({0}) EXCEPT {1} UNION {2} UNION {3}'.format( sql_design_number_part_ids, sql_inseparable_components_part_ids, sql_already_added_part_ids, sql_nla_part_ids) # Search in parts for matches excluding self and already added parts sql = 'SELECT * FROM parts WHERE id != :part_id AND id NOT in ({0}) AND id IN ({1})'.format( sql_exclude_part_ids, sql_part_ids) results = db.session.query(cls).from_statement( db.text(sql).params(query=query, part_id=part_id)).all() return results @classmethod def typeahead_search_all_but_self(cls, query, part_id): query = '%{0}%'.format( query) # Pad query for an ILIKE search for design_number and # Search in parts for design_numbers, part_identifiers or associated design names that match, excluding self sql_part_ids = "SELECT DISTINCT ON (d.design_number, p.part_identifier) p.id FROM parts p, designs d WHERE p.design_id = d.id AND (SELECT CONCAT(d.design_number, '-', cast(p.part_identifier as text), ' ', d.name) ILIKE :query) ORDER BY d.design_number, p.part_identifier, d.revision DESC" sql = 'SELECT * FROM parts p WHERE p.id != :part_id AND p.id IN ({0})'.format( sql_part_ids) results = db.session.query(cls).from_statement( db.text(sql).params(query=query, part_id=part_id)).all() return results @classmethod def typeahead_search_all(cls, query): query = '%{0}%'.format( query) # Pad query for an ILIKE search for design_number and # Search in parts for design_numbers, part_identifiers or associated design names that match sql = "SELECT DISTINCT ON (d.design_number, p.part_identifier) p.* FROM parts p, designs d WHERE p.design_id = d.id AND (SELECT CONCAT(d.design_number, '-', cast(p.part_identifier as text), ' ', d.name) ILIKE :query) ORDER BY d.design_number, p.part_identifier, d.revision DESC" results = db.session.query(cls).from_statement( db.text(sql).params(query=query)).all() return results def get_nlas_for_part(self): sql_design_ids = 'SELECT DISTINCT ON (design_number) id FROM designs ORDER BY design_number, revision DESC' # Unique design_number, highest revision sql_pc_ids = 'SELECT parent_id FROM part_components WHERE part_id = :part_id' sql = 'SELECT * FROM parts WHERE id IN ({0}) AND design_id IN ({1})'.format( sql_pc_ids, sql_design_ids) parents = db.session.query(Part).from_statement( db.text(sql).params(part_id=self.id)).all() return parents def get_products_for_part(self): # TODO: Remove this and change to for_design / for_design_number from pid.product.models import Product results = Product.query.filter_by(part_id=self.id).all() return results def get_parts_for_design_revisions(self): from pid.design.models import Design revisions = Design.query.filter_by( design_number=self.design.design_number).all() revisions.sort(key=lambda x: (len(x.revision), x.revision) ) # Sort them first alphabetically, then by length design_ids = [x.id for x in revisions] results = Part.query.filter( and_(Part.design_id.in_(design_ids), Part.part_identifier == self.part_identifier)).all() return results def get_builds_for_design_number_and_part_identifier(self): from pid.product.models import Build sql = 'SELECT b.* FROM builds b, parts p, designs d WHERE b.part_id = p.id AND p.design_id = d.id AND d.design_number = :design_number AND p.part_identifier = :part_identifier ORDER BY b.build_identifier' results = db.session.query(Build).from_statement( db.text(sql).params(design_number=self.design.design_number, part_identifier=self.part_identifier)).all() return results @classmethod def get_nlas_for_vendor_part(cls, vendor_part): sql_design_ids = 'SELECT DISTINCT ON (design_number) id FROM designs ORDER BY design_number, revision DESC' # Unique design_number, highest revision sql_pc_ids = 'SELECT parent_id FROM part_components WHERE vendor_part_id = :vendor_part_id' sql = 'SELECT * FROM parts WHERE id IN ({0}) AND design_id IN ({1})'.format( sql_pc_ids, sql_design_ids) parents = db.session.query(cls).from_statement( db.text(sql).params(vendor_part_id=vendor_part.id)).all() return parents def add_component(self, part_component): self.components.append(part_component) db.session.add(self) db.session.commit() def update_mass(self): # %Unc = (PBE/CBE) - 1 (*100) sum_cbe = 0.0 sum_pbe = 0.0 if self.components: for component in self.components: if component.part: sum_cbe += (component.part.current_best_estimate * component.quantity) sum_pbe += (component.part.predicted_best_estimate * component.quantity) elif component.vendor_part: sum_cbe += (component.vendor_part.current_best_estimate * component.quantity) sum_pbe += (component.vendor_part.predicted_best_estimate * component.quantity) if sum_cbe == 0.0: unc = 0.0 else: unc = ((sum_pbe / sum_cbe) - 1) * 100 else: sum_cbe = self.current_best_estimate unc = self.uncertainty sum_pbe = self.predicted_best_estimate self.update(current_best_estimate=sum_cbe, uncertainty=unc, predicted_best_estimate=sum_pbe) # TODO: In a large tree, this might take a while, should figure out a queue to do this with self.update_parents_mass( ) # In case there are parts depending on this as a part_component def update_parents_mass(self): """ Update the mass of all parents of this part. Call this when updating mass of a part """ part_components = PartComponent.query.filter_by(part=self).all() for part_component in part_components: part_component.parent.update_mass() def can_user_edit(self, field_name): return self.design.can_user_edit(field_name) def get_name(self): if self.name: return self.name return self.design.name def get_unique_identifier(self): return self.part_number def get_url(self): return url_for('design.view_design', design_number=self.design.design_number, revision=self.design.revision) def __str__(self): return '{0}-{1} {2}'.format(self.design.design_number, self.design.revision, self.part_identifier) def __repr__(self): """Represent instance as a unique string.""" return '<Part({0})>'.format(self.part_number)
class Procedure(RevisionRecord): __tablename__ = 'procedures' descriptor = 'Procedure' procedure_number = Column(db.String, default=get_next_procedure_number, nullable=False) summary = Column(db.String) approvers = relationship('Approver', secondary='procedures_approvers', order_by='asc(Approver.id)', backref='procedure') parts = relationship('Part', secondary='procedures_parts') vendor_parts = relationship('VendorPart', secondary='procedures_vendor_parts') documents = relationship('Document', secondary='procedures_documents') links = relationship('Link', secondary='procedures_links') images = relationship('Image', secondary='procedures_images') as_runs = relationship('AsRun', back_populates='procedure') project_id = reference_col('projects') project = relationship('Project') workflow = ProcedureWorkflow() state = Column(db.String, default=workflow.initial_state) permissions = ProcedurePermissions() __table_args__ = (db.UniqueConstraint( 'procedure_number', 'revision', name='procedure_number_revision_unique'), ) __mapper_args__ = {"order_by": procedure_number} def __init__(self, **kwargs): super().__init__() db.Model.__init__(self, **kwargs) @property def identifier(self): return '{0}-{1}'.format(self.procedure_number, self.revision) @property def references_by(self): # Override base method due to revisions sql_revision_ids = 'SELECT id FROM procedures WHERE procedure_number = :procedure_number ORDER BY revision' sql = 'SELECT * FROM "references" WHERE to_id IN ({0}) AND to_class = :class_name'.format( sql_revision_ids) query_results = db.session.query(Reference).from_statement( db.text(sql).params(procedure_number=self.procedure_number, class_name=self.get_class_name())).all() results = {r.get_url_by(): r for r in query_results}.values() return results @classmethod def get_by_procedure_number(cls, procedure_number): return cls.query.filter_by(procedure_number=procedure_number).first() @classmethod def get_by_procedure_number_and_revision(cls, procedure_number, revision): return cls.query.filter_by(procedure_number=procedure_number, revision=revision).first() @classmethod def find_all_procedures_for_user(cls, user): results = cls.query.filter_by(owner=user).order_by( cls.procedure_number).all() return results @classmethod def find_all_distinct_procedures_for_user(cls, user): results = cls.query.filter_by(owner=user).distinct( cls.procedure_number).order_by(cls.procedure_number, cls.revision.desc()).all() return results @classmethod def typeahead_search(cls, query): query = '%{0}%'.format(query) # Pad query for an ILIKE search sql = "SELECT DISTINCT ON (procedure_number) * FROM {0} WHERE (SELECT CONCAT(procedure_number, ' ', name) ILIKE :query) ORDER BY procedure_number, revision DESC".format( cls.__tablename__) results = db.session.query(cls).from_statement( db.text(sql).params(query=query)).all() return results @classmethod def advanced_search(cls, params): from pid.part.models import Part from pid.design.models import Design from pid.vendorpart.models import VendorPart query = cls.query columns = cls.__table__.columns.keys() for attr in params: if params[attr] != "" and attr in columns: query = query.filter(getattr(cls, attr) == params[attr]) elif params[attr] != "": if attr == 'proc_number_query': formatted_query = Utils.format_match_query( params['proc_number_query_type'], params['proc_number_query']) query = query.filter( cls.procedure_number.ilike(formatted_query)) elif attr == 'part_number_query': formatted_query = Utils.format_match_query( params['part_number_query_type'], params['part_number_query']) query = query.filter( cls.parts.any( Part.design.has( Design.design_number.ilike(formatted_query))) | cls.vendor_parts.any( VendorPart.part_number.ilike(formatted_query))) elif attr == 'text_fields_query': formatted_query = Utils.format_match_query( 'includes', params['text_fields_query']) query = query.filter( cls.name.ilike(formatted_query) | cls.summary.ilike(formatted_query)) elif attr == 'created_on_start': query = query.filter( cls.created_at >= params['created_on_start']) elif attr == 'created_on_end': query = query.filter( cls.created_at <= params['created_on_end']) elif attr == 'in_open_state': query = query.filter( cls.state.in_(cls.workflow.open_states)) elif attr == 'exclude_obsolete': query = query.filter( cls.state != cls.workflow.obsolete_state) return query.distinct(cls.procedure_number).order_by( cls.procedure_number.desc(), cls.revision.desc()).all() def find_all_revisions(self): results = Procedure.query.filter_by( procedure_number=self.procedure_number).all() return Utils.find_all_revisions(results) def find_latest_revision(self): results = Procedure.query.with_entities(Procedure.revision).filter_by( procedure_number=self.procedure_number).all() return Utils.find_latest_revision(results) def find_next_revision(self): results = Procedure.query.with_entities(Procedure.revision).filter_by( procedure_number=self.procedure_number).order_by( Procedure.revision).all() return Utils.find_next_revision(results) def find_all_as_runs_numbers(self): from pid.asrun.models import AsRun as_runs = AsRun.query.filter_by( procedure_number=self.procedure_number).all() as_run_numbers = [] for as_run in as_runs: as_run_numbers.append({ 'id': as_run.id, 'number': str(as_run.as_run_number).zfill(3) }) return as_run_numbers def get_approval_errors(self): approval_errors = [] if self.state == self.workflow.get_approval_state(): # Already in approval state, no need to do further checks return approval_errors # Check if not self_approved and either no approvers added or all approvers have already approved somehow. if not self.self_approved: if not self.approvers: approval_errors.append('You must add at least one approver.') elif all([approver.approved_at for approver in self.approvers]): approval_errors.append('You must add at least one approver.') return approval_errors def get_latest_revision_unique_identifier(self): return '{0}-{1}'.format(self.procedure_number, self.find_latest_revision()) def get_latest_revision_url(self): # BEWARE: This function will always point to latest revision of design return url_for('procedure.view_procedure', procedure_number=self.procedure_number, revision=self.find_latest_revision()) def get_unique_identifier(self): return '{0}-{1}'.format(self.procedure_number, self.revision) def get_url(self, external=False): return url_for('procedure.view_procedure', procedure_number=self.procedure_number, revision=self.revision, _external=external) def find_all_as_runs(self): from pid.asrun.models import AsRun return AsRun.query.filter_by( procedure_number=self.procedure_number).all() def __repr__(self): """Represent instance as a unique string.""" return '<Procedure({id!r},{procedure_number!r})>'.format( id=self.id, procedure_number=self.procedure_number)
class VendorPart(Record): __tablename__ = 'vendor_parts' descriptor = 'Vendor Part' part_number = Column(db.String, nullable=False) current_best_estimate = Column(db.Float, default=0.0, nullable=False) uncertainty = Column(db.Float, default=0.0, nullable=False) predicted_best_estimate = Column(db.Float, default=0.0, nullable=False) material_id = reference_col('materials', nullable=True) material = relationship('Material') material_specification_id = reference_col('material_specifications', nullable=True) material_specification = relationship('MaterialSpecification') approvers = relationship('Approver', secondary='vendor_parts_approvers', order_by='asc(Approver.id)', backref='vendor_part') summary = Column(db.String, nullable=True) notes = Column(db.Text, nullable=True) project_id = reference_col('projects') project = relationship('Project') vendor_id = reference_col('companies') vendor = relationship('Company') procedures = relationship('Procedure', secondary='procedures_vendor_parts') documents = relationship('Document', secondary='vendor_parts_documents') images = relationship('Image', secondary='vendor_parts_images') links = relationship('Link', secondary='vendor_parts_links') anomalies = relationship('Anomaly', secondary='vendor_parts_anomalies', order_by='desc(Anomaly.created_at)', back_populates='vendor_parts') workflow = VendorPartWorkflow() state = Column(db.String, default=workflow.initial_state) permissions = VendorPartPermissions() __table_args__ = (db.UniqueConstraint('part_number', name='part_number_unique'),) __mapper_args__ = { "order_by": part_number } def __init__(self, **kwargs): super().__init__() db.Model.__init__(self, **kwargs) @property def design_number(self): return self.part_number @classmethod def get_by_part_number(cls, part_number): return cls.query.filter_by(part_number=part_number).first() @classmethod def get_all_vendor_parts(cls): results = cls.query.all() return results @classmethod def find_all_vendor_parts_for_user(cls, user): results = cls.query.filter_by(owner=user).order_by(cls.part_number).all() return results @classmethod def typeahead_search(cls, query, part_id): query = '%{0}%'.format(query) # Pad query for an ILIKE search # Search in vendor parts for part_number or name that matches sql_vendor_ids = "SELECT id FROM {0} WHERE (SELECT CONCAT(part_number, ' ', name) ILIKE :query)".format(cls.__tablename__) # Get ids of vendor parts already added as part_components, excluding part_components made up of parts sql_pc_ids = 'SELECT vendor_part_id FROM part_components WHERE parent_id = :part_id AND vendor_part_id IS NOT NULL' # Search in vendor parts, excluding self and already added parts sql = 'SELECT * FROM {0} WHERE id NOT in ({1}) AND id IN ({2})'.format(cls.__tablename__, sql_pc_ids, sql_vendor_ids) results = db.session.query(cls).from_statement(db.text(sql).params(query=query, part_id=part_id)).all() # results = VendorPart.query.whooshee_search(query).all() return results @classmethod def advanced_search(cls, params): from pid.anomaly.models import Anomaly query = cls.query columns = cls.__table__.columns.keys() for attr in params: if params[attr] != "" and attr in columns: query = query.filter(getattr(cls, attr) == params[attr]) elif params[attr] != "": if attr == 'part_number_query': formatted_query = format_match_query(params['part_number_query_type'], params['part_number_query']) query = query.filter(cls.part_number.ilike(formatted_query)) elif attr == 'text_fields_query': formatted_query = format_match_query('includes', params['text_fields_query']) query = query.filter(cls.name.ilike(formatted_query) | cls.notes.ilike(formatted_query) | cls.summary.ilike(formatted_query)) elif attr == 'open_anomalies': query = query.filter(cls.anomalies.any(Anomaly.state.in_(Anomaly.workflow.open_states))) elif attr == 'created_on_start': query = query.filter(cls.created_at >= params['created_on_start']) elif attr == 'created_on_end': query = query.filter(cls.created_at <= params['created_on_end']) elif attr == 'in_open_state': query = query.filter(cls.state.in_(cls.workflow.open_states)) elif attr =='exclude_obsolete': query = query.filter(cls.state != cls.workflow.obsolete_state) if 'open_anomalies' not in params: query = query.distinct(cls.part_number) return query.order_by(cls.part_number.desc()).all() def get_vendor_builds_for_vendor_part(self): from pid.vendorproduct.models import VendorBuild results = VendorBuild.query.filter_by(vendor_part=self).all() return results def update_parents_mass(self): """ Update the mass of all parents of this part. Call this when updating mass of a part """ from pid.part.models import PartComponent # TODO: rename part_components = PartComponent.query.filter_by(vendor_part=self).all() for part_component in part_components: part_component.parent.update_mass() def get_procedures(self): return self.get_distinct_procedures() def get_distinct_procedures(self): from pid.procedure.models import Procedure procedures = Procedure.query.filter(Procedure.vendor_parts.contains(self))\ .order_by(Procedure.procedure_number, Procedure.revision.desc()).distinct(Procedure.procedure_number).all() procedures.sort(key=lambda x: x.created_at, reverse=True) # Sort by newest first return procedures def get_products_for_part(self): from pid.product.models import VendorProduct results = VendorProduct.query.filter_by(vendor_part_id=self.id).all() return results def get_nlas_for_vendor_part(self): return Part.get_nlas_for_vendor_part(self) def get_approval_errors(self): approval_errors = [] if self.state == self.workflow.get_approval_state(): # Already in approval state, no need to do further checks return approval_errors # Check if not self_approved and either no approvers added or all approvers have already approved somehow. if not self.self_approved: if not self.approvers: approval_errors.append('You must add at least one approver.') elif all([approver.approved_at for approver in self.approvers]): approval_errors.append('You must add at least one approver.') # Check if open anomalies for anomaly in self.anomalies: if anomaly.is_open(): approval_errors.append('{0} must be resolved.'.format(anomaly.get_unique_identifier())) return approval_errors def get_unique_identifier(self): return self.part_number def get_url(self, external=False): return url_for('vendorpart.view_vendor_part', part_number=self.part_number, _external=external) def __str__(self): return self.part_number def __repr__(self): """Represent instance as a unique string.""" return '<VendorPart({0})>'.format(self.part_number)
class VendorProduct(NamelessRecord): __tablename__ = 'vendor_products' descriptor = 'Vendor Product' serial_number = Column(db.String, nullable=False) vendor_part_id = reference_col('vendor_parts') vendor_part = relationship('VendorPart') summary = Column(db.String) notes = Column(db.Text) approvers = relationship('Approver', secondary='vendor_products_approvers', order_by='asc(Approver.id)', backref='vendor_product') allowed_types = ['SN', 'LOT', 'STOCK'] product_type = Column(db.String, default='SN') measured_mass = Column(db.Float, default=0.0) hardware_type_id = reference_col('hardware_types') hardware_type = relationship('HardwareType') project_id = reference_col('projects') project = relationship('Project') vendor_build_id = reference_col('vendor_builds') vendor_build = relationship('VendorBuild', back_populates='vendor_products') documents = relationship('Document', secondary='vendor_products_documents') images = relationship('Image', secondary='vendor_products_images') links = relationship('Link', secondary='vendor_products_links') discrepancies = relationship('Discrepancy', secondary='vendor_products_discrepancies') as_runs = relationship('AsRun', secondary='as_runs_vendor_products', order_by='desc(AsRun.created_at)') workflow = ProductWorkflow() state = Column(db.String, default=workflow.initial_state) permissions = VendorProductPermissions() __table_args__ = (db.UniqueConstraint('serial_number', 'vendor_part_id', name='serial_number_vendor_part_unique'),) __mapper_args__ = { "order_by": serial_number } def __init__(self, **kwargs): super().__init__() db.Model.__init__(self, **kwargs) @property def product_number(self): return '{0} {1}'.format(self.vendor_part.part_number, self.serial_number) @property def discrepancy_number(self): return '{0}-{1}'.format(self.vendor_part.part_number, self.serial_number) @classmethod def get_next_lot_number_for_vendor_part(cls, vendor_part): results = cls.query.with_entities(cls.serial_number).filter_by(vendor_part=vendor_part, product_type='LOT').distinct().all() if len(results) == 0: lot_number = 1 else: resultset = [row[0] for row in results] resultset.sort(reverse=True) lot_number = None index = 0 while not lot_number and index < len(resultset): if resultset[index].replace('L', '').isdigit(): lot_number = int(resultset[index].replace('L', '')) + 1 index = index + 1 if not lot_number: lot_number = 1 return 'L{0:03d}'.format(lot_number) @classmethod def get_serial_numbers_for_vendor_part(cls, vendor_part): results = cls.query.with_entities(cls.serial_number).filter_by(vendor_part=vendor_part).distinct().all() resultset = [row[0] for row in results] resultset.sort() return resultset @classmethod def get_vendor_product_by_product_number(cls, part_number, serial_number): sql = 'SELECT vprod.* FROM vendor_products vprod, vendor_parts vpart WHERE vprod.vendor_part_id = vpart.id AND vpart.part_number = :part_number AND vprod.serial_number = :serial_number' results = db.session.query(cls).from_statement(db.text(sql).params(part_number=part_number, serial_number=serial_number)).first() return results @classmethod def find_all_vendor_products_for_user(cls, user): results = cls.query.filter_by(owner=user).join(cls.vendor_part).order_by(VendorPart.part_number, cls.serial_number).all() return results @classmethod def typeahead_search(cls, query): query = '%{0}%'.format(query) # Pad query for an ILIKE search sql = "SELECT vprod.* FROM vendor_products vprod, vendor_parts vpart WHERE vprod.vendor_part_id = vpart.id AND (SELECT CONCAT(vpart.part_number, '-', vprod.serial_number, ' ', vpart.name) ILIKE :query)" results = db.session.query(cls).from_statement(db.text(sql).params(query=query)).all() return results @classmethod def advanced_search(cls, params): from pid.product.models import Discrepancy query = cls.query columns = cls.__table__.columns.keys() for attr in params: if params[attr] != "" and attr in columns: query = query.filter(getattr(cls, attr) == params[attr]) elif params[attr] != "": if attr == 'vprod_part_number_query': formatted_query = format_match_query(params['vprod_part_number_query_type'], params['vprod_part_number_query']) query = query.filter(cls.vendor_part.has(VendorPart.part_number.ilike(formatted_query))) elif attr == 'vprod_serial_number_query': formatted_query = format_match_query(params['vprod_serial_number_query_type'], params['vprod_serial_number_query']) query = query.filter(cls.serial_number.ilike(formatted_query)) elif attr == 'text_fields_query': formatted_query = format_match_query('includes', params['text_fields_query']) query = query.filter(cls.summary.ilike(formatted_query) | cls.notes.ilike(formatted_query)) elif attr == 'open_discrepancies': query = query.filter(cls.discrepancies.any(Discrepancy.state.in_(['Open']))) elif attr == 'created_on_start': query = query.filter(cls.created_at >= params['created_on_start']) elif attr == 'created_on_end': query = query.filter(cls.created_at <= params['created_on_end']) elif attr == 'in_open_state': query = query.filter(cls.state.in_(cls.workflow.open_states)) elif attr =='exclude_obsolete': query = query.filter(cls.state != cls.workflow.obsolete_state) elif attr == 'vendor_id': query = query.filter(cls.vendor_part.has(VendorPart.vendor_id == params[attr])) elif attr == 'material_id': query = query.filter(cls.vendor_part.has(VendorPart.material_id == params[attr])) return query.all() def get_installed_ins(self): from pid.product.models import ProductComponent, ExtraProductComponent results = ProductComponent.query.filter_by(vendor_product=self).all() results.extend(ExtraProductComponent.query.filter_by(vendor_product=self).all()) return results def get_approval_errors(self): approval_errors = [] if self.state == self.workflow.get_approval_state(): # Already in approval state, no need to do further checks return approval_errors # Check if not self_approved and either no approvers added or all approvers have already approved somehow. if not self.self_approved: if not self.approvers: approval_errors.append('You must add at least one approver.') elif all([approver.approved_at for approver in self.approvers]): approval_errors.append('You must add at least one approver.') # Check if open discrepancies for discrepancy in self.discrepancies: if discrepancy.is_open(): approval_errors.append('Discrepancy {0} must be resolved.'.format(discrepancy.discrepancy_number)) return approval_errors def get_name(self): return self.vendor_part.name def get_unique_identifier(self): return self.product_number def get_url(self, external=False): return url_for('vendorproduct.view_vendor_product', part_number=self.vendor_part.part_number, serial_number=self.serial_number, _external=external) def __str__(self): return self.product_number def __repr__(self): return '<VendorProduct({0})>'.format(self.product_number)
class VendorBuild(BaseRecord): __tablename__ = 'vendor_builds' build_identifier = Column(db.String, nullable=False, default='001') vendor_part_id = reference_col('vendor_parts') vendor_part = relationship('VendorPart') notes = Column(db.Text) purchase_order = Column(db.String) vendor_products = relationship('VendorProduct', back_populates='vendor_build', order_by="VendorProduct.serial_number") documents = relationship('Document', secondary='vendor_builds_documents') discrepancies = relationship('Discrepancy', secondary='vendor_builds_discrepancies') vendor_id = reference_col('companies') vendor = relationship('Company', foreign_keys=[vendor_id]) manufacturer_id = reference_col('companies') manufacturer = relationship('Company', foreign_keys=[manufacturer_id]) permissions = VendorBuildPermissions() __table_args__ = (db.UniqueConstraint('build_identifier', 'vendor_part_id', name='build_identifier_vendor_part_unique'),) def __init__(self, **kwargs): super().__init__() db.Model.__init__(self, **kwargs) @property def build_number(self): return '{0}.{1}'.format(self.vendor_part.part_number, self.build_identifier) @property def discrepancy_number(self): return '{0}-{1}'.format(self.vendor_part.part_number, self.build_identifier) @classmethod def get_next_build_identifier_for_vendor_part(cls, vendor_part): results = cls.query.with_entities(cls.build_identifier).filter_by(vendor_part=vendor_part).all() if len(results) == 0: build_identifier = 1 else: resultset = [row[0] for row in results] resultset.sort() build_identifier = int(resultset[-1]) + 1 return '{0:03d}'.format(build_identifier) def find_all_build_identifiers(self): results = VendorBuild.query.with_entities(VendorBuild.build_identifier).order_by(VendorBuild.build_identifier.asc()).filter_by(vendor_part=self.vendor_part).all() resultset = [row[0] for row in results] return resultset def can_user_edit(self, field_name): # Build state is always open, so need to override can_user_edit if current_user.is_admin(): return True # Admins can do anything always role = 'all' if current_user == self.owner: role = 'owner' elif current_user.is_superuser(): role = 'superuser' state = 'open' return self.permissions.get_permissions().get(state, False).get(role, False).get(field_name) def __str__(self): return self.build_number def __repr__(self): return '<VendorBuild({0})>'.format(self.build_number)
class Design(RevisionRecord): __tablename__ = 'designs' descriptor = 'Design' design_number = Column(db.String, nullable=False) summary = Column(db.String) notes = Column(db.Text) documents = relationship('Document', secondary='designs_documents') anomalies = relationship('Anomaly', secondary='designs_anomalies', order_by='desc(Anomaly.created_at)', back_populates='designs') ecos = relationship('ECO', secondary='designs_ecos', order_by='desc(ECO.created_at)', back_populates='designs') approvers = relationship('Approver', secondary='designs_approvers', order_by='asc(Approver.id)', backref='design') project_id = reference_col('projects') project = relationship('Project') parts = relationship('Part', back_populates='design', order_by="Part.part_identifier") links = relationship('Link', secondary='designs_links') images = relationship('Image', secondary='designs_images') export_control = Column(db.Boolean(), default=False) workflow = DesignWorkflow() state = Column(db.String, default=workflow.initial_state) permissions = DesignPermissions() __table_args__ = (db.UniqueConstraint('design_number', 'revision', name='design_number_revision_unique'),) __mapper_args__ = { "order_by": design_number } def __init__(self, **kwargs): super().__init__() db.Model.__init__(self, **kwargs) @property def references_by(self): # Override base method due to revisions sql_revision_ids = 'SELECT id FROM designs WHERE design_number = :design_number ORDER BY revision' sql = 'SELECT * FROM "references" WHERE to_id IN ({0}) AND to_class = :class_name'.format(sql_revision_ids) query_results = db.session.query(Reference).from_statement(db.text(sql).params(design_number=self.design_number, class_name=self.get_class_name())).all() results = {r.get_url_by(): r for r in query_results}.values() return results @classmethod def find_all_design_numbers(cls): results = cls.query.with_entities(cls.design_number).order_by(cls.design_number.asc()).distinct().all() resultset = [int(row[0]) for row in results] return resultset @classmethod def find_all_designs_for_user(cls, user): results = cls.query.filter_by(owner=user).distinct(cls.design_number).order_by(cls.design_number, cls.revision.desc()).all() return results @classmethod def get_by_design_number_and_revision(cls, design_number, revision): return cls.query.filter_by(design_number=design_number, revision=revision).first() @classmethod def typeahead_search(cls, query): query = '%{0}%'.format(query) # Pad query for an ILIKE search sql = "SELECT DISTINCT ON (design_number) * FROM {0} WHERE (SELECT CONCAT(design_number, ' ', name) ILIKE :query) ORDER BY design_number, revision DESC".format(cls.__tablename__) results = db.session.query(cls).from_statement(db.text(sql).params(query=query)).all() return results @classmethod def advanced_search(cls, params): query = cls.query columns = cls.__table__.columns.keys() for attr in params: if params[attr] != "" and attr in columns: query = query.filter(getattr(cls, attr) == params[attr]) elif params[attr] != "": if attr == 'design_number_query': formatted_query = format_match_query(params['design_number_query_type'], params['design_number_query']) query = query.filter(cls.design_number.ilike(formatted_query)) elif attr == 'text_fields_query': formatted_query = format_match_query('includes', params['text_fields_query']) query = query.filter(cls.name.ilike(formatted_query) | cls.notes.ilike(formatted_query) | cls.summary.ilike(formatted_query)) elif attr == 'open_anomalies': from pid.anomaly.models import Anomaly query = query.filter(cls.anomalies.any(Anomaly.state.in_(Anomaly.workflow.open_states))) elif attr == 'open_ecos': from pid.eco.models import ECO query = query.filter(cls.ecos.any(ECO.state.in_(ECO.workflow.open_states))) elif attr == 'created_on_start': query = query.filter(cls.created_at >= params['created_on_start']) elif attr == 'created_on_end': query = query.filter(cls.created_at <= params['created_on_end']) elif attr == 'in_open_state': query = query.filter(cls.state.in_(cls.workflow.open_states)) elif attr =='exclude_obsolete': query = query.filter(cls.state != cls.workflow.obsolete_state) elif attr == 'material_id': from pid.part.models import Part query = query.filter(cls.parts.any(Part.material_id == params['material_id'])) if 'open_anomalies' not in params and 'open_ecos' not in params: query = query.distinct(cls.design_number) return query.order_by(cls.design_number.desc(), cls.revision.desc()).all() def find_all_revisions(self): results = Design.query.filter_by(design_number=self.design_number).all() # Sort them first alphabetically, then by length results.sort(key=lambda x: (len(x.revision), x.revision)) return results def find_latest_revision(self): results = Design.query.with_entities(Design.revision).filter_by(design_number=self.design_number).all() # Sort them first alphabetically, then by length, in reverse. First element will have the highest revision. resultset = [row[0] for row in results] resultset.sort(key=lambda x: (len(x), x), reverse=True) return resultset[0] def find_next_revision(self): # Doing separate lists that we then concat, due to sorting issues all_possible_single_revisions = [] all_possible_double_revisions = [] # See: https://stackoverflow.com/questions/23686398/iterate-a-to-zzz-in-python for chars in AUC: all_possible_single_revisions.append(''.join(chars)) for chars in product(AUC, repeat=2): all_possible_double_revisions.append(''.join(chars)) results = Design.query.with_entities(Design.revision).filter_by(design_number=self.design_number).order_by(Design.revision).all() used_revisions = [str(row[0]) for row in results] free_single_revisions = list(set(all_possible_single_revisions) - set(used_revisions) - set(FORBIDDEN_REVISIONS)) free_double_revisions = list(set(all_possible_double_revisions) - set(used_revisions)) return (sorted(free_single_revisions) + sorted(free_double_revisions))[0] def find_next_part_number(self): # https://stackoverflow.com/a/16974075 part_numbers = [int(part.part_identifier) for part in self.parts] difference = sorted(set(range(part_numbers[0], part_numbers[-1] + 1)).difference(part_numbers)) next_part_number = difference[0] if difference else part_numbers[-1] + 1 return next_part_number def find_next_inseparable_part_number(self): part_numbers = [int(part.part_identifier) for part in self.parts] start = part_numbers[0] if part_numbers[0] > 100 else 101 end = part_numbers[-1] if part_numbers[-1] > 100 else 102 difference = sorted(set(range(start, end)).difference(part_numbers)) next_part_number = difference[0] if difference else part_numbers[-1] + 1 return next_part_number def get_approval_errors(self): approval_errors = [] if self.state == self.workflow.get_approval_state(): # Already in approval state, no need to do further checks return approval_errors # Check if not self_approved and either no approvers added or all approvers have already approved somehow. if not self.self_approved: if not self.approvers: approval_errors.append('You must add at least one approver.') elif all([approver.approved_at for approver in self.approvers]): approval_errors.append('You must add at least one approver.') # Check if open anomalies for anomaly in self.anomalies: if anomaly.is_open(): approval_errors.append('{0} must be resolved.'.format(anomaly.get_unique_identifier())) # Check if open ecos for eco in self.ecos: if eco.is_open(): approval_errors.append('{0} must be resolved.'.format(eco.get_unique_identifier())) return approval_errors def get_procedures(self): return self.get_all_procedures() def get_all_procedures(self): from pid.procedure.models import Procedure procedures = [] for part in self.parts: part_procedures = defaultdict(str) for procedure in part.procedures: if part_procedures[procedure.procedure_number] < procedure.revision: part_procedures[procedure.procedure_number] = procedure.revision for key, value in part_procedures.items(): procedures.append(Procedure.get_by_procedure_number_and_revision(key, value)) procedures.sort(key=lambda x: x.created_at, reverse=True) # Sort by newest first return procedures def as_dict(self): return { 'id': self.id, 'design_number': self.design_number, 'revision': self.revision, 'url': self.get_url() } def get_latest_revision_unique_identifier(self): return '{0}-{1}'.format(self.design_number, self.find_latest_revision()) def get_latest_revision_url(self): # BEWARE: This function will always point to latest revision of design return url_for('design.view_design', design_number=self.design_number, revision=self.find_latest_revision()) def get_unique_identifier(self): return '{0}-{1}'.format(self.design_number, self.revision) def get_url(self, external=False): return url_for('design.view_design', design_number=self.design_number, revision=self.revision, _external=external) def __str__(self): return '{0}-{1}'.format(self.design_number, self.revision) def __repr__(self): """Represent instance as a unique string.""" return '<Design({0} {1})>'.format(self.design_number, self.revision)
class Product(NamelessRecord): __tablename__ = 'products' descriptor = 'Product' serial_number = Column(db.String, nullable=False) part_id = reference_col('parts') part = relationship('Part') revision = Column(db.String, nullable=False) summary = Column(db.String) notes = Column(db.Text) approvers = relationship('Approver', secondary='products_approvers', order_by='asc(Approver.id)', backref='product') allowed_types = ['SN', 'LOT', 'STOCK'] product_type = Column(db.String, default='SN') measured_mass = Column(db.Float, default=0.0) hardware_type_id = reference_col('hardware_types') hardware_type = relationship('HardwareType') project_id = reference_col('projects') project = relationship('Project') build_id = reference_col('builds') build = relationship('Build', back_populates='products') documents = relationship('Document', secondary='products_documents') images = relationship('Image', secondary='products_images') links = relationship('Link', secondary='products_links') components = relationship('ProductComponent', foreign_keys='ProductComponent.parent_id') extra_components = relationship('ExtraProductComponent', foreign_keys='ExtraProductComponent.parent_id') discrepancies = relationship('Discrepancy', secondary='products_discrepancies') as_runs = relationship('AsRun', secondary='as_runs_products', order_by='desc(AsRun.created_at)') workflow = ProductWorkflow() state = Column(db.String, default=workflow.initial_state) permissions = ProductPermissions() __table_args__ = (db.UniqueConstraint('serial_number', 'part_id', name='serial_number_part_unique'),) __mapper_args__ = { "order_by": serial_number } def __init__(self, **kwargs): super().__init__() db.Model.__init__(self, **kwargs) @property def product_number(self): return '{0} {1}'.format(self.part.part_number, self.serial_number) @property def discrepancy_number(self): return '{0}-{1}'.format(self.part.part_number, self.serial_number) @classmethod def get_product_by_product_number(cls, design_number, part_identifier, serial_number): sql = 'SELECT prod.* FROM products prod, parts p, designs d WHERE prod.part_id = p.id AND p.design_id = d.id AND d.design_number = :design_number AND p.part_identifier = :part_identifier AND prod.serial_number = :serial_number' results = db.session.query(cls).from_statement(db.text(sql).params(design_number=design_number, part_identifier=part_identifier, serial_number=serial_number)).first() return results @classmethod def get_serial_numbers_for_design_number_and_part_identifier(cls, design_number, part_identifier): sql = 'SELECT prod.* FROM products prod, parts p, designs d WHERE prod.part_id = p.id AND p.design_id = d.id AND d.design_number = :design_number AND p.part_identifier = :part_identifier' results = db.session.query(cls).from_statement(db.text(sql).params(design_number=design_number, part_identifier=part_identifier)).all() resultset = [p.serial_number for p in results] resultset.sort() return resultset @classmethod def get_next_lot_number_for_design_number_and_part_identifier(cls, design_number, part_identifier): sql = 'SELECT prod.* FROM products prod, parts p, designs d WHERE prod.part_id = p.id AND p.design_id = d.id AND d.design_number = :design_number AND p.part_identifier = :part_identifier AND prod.product_type = \'LOT\'' results = db.session.query(cls).from_statement(db.text(sql).params(design_number=design_number, part_identifier=part_identifier)).all() if len(results) == 0: lot_number = 1 else: resultset = [p.serial_number for p in results] resultset.sort() lot_number = int(resultset[-1].replace('L', '')) + 1 return 'L{0:03d}'.format(lot_number) @classmethod def find_all_products_for_user(cls, user): results = Product.query.filter_by(owner=user).join(cls.part).join(Part.design).order_by(Design.design_number, Part.part_identifier, cls.serial_number).all() return results def get_installed_ins(self): results = ProductComponent.query.filter_by(product=self).all() results.extend(ExtraProductComponent.query.filter_by(product=self).all()) return results def get_product_components(self): return ProductComponent.query.filter_by(parent_id=self.id).all() def get_extra_product_components(self): return ExtraProductComponent.query.filter_by(parent_id=self.id).all() @classmethod def typeahead_search(cls, query): query = '%{0}%'.format(query) # Pad query for an ILIKE search sql_name = "SELECT CASE WHEN p.name IS NOT NULL THEN p.name ELSE d.name END AS concat_name" sql = "SELECT prod.* FROM products prod, parts p, designs d WHERE prod.part_id = p.id AND p.design_id = d.id AND (SELECT CONCAT(d.design_number, '-', p.part_identifier, '-', prod.serial_number, ' ', ({0})) ILIKE :query)".format(sql_name) results = db.session.query(cls).from_statement(db.text(sql).params(query=query)).all() return results @classmethod def advanced_search(cls, params): query = cls.query columns = cls.__table__.columns.keys() for attr in params: if params[attr] != "" and attr in columns: query = query.filter(getattr(cls, attr) == params[attr]) elif params[attr] != "": if attr == 'product_part_number_query': formatted_query = format_match_query(params['product_part_number_query_type'], params[attr]) query = query.filter(cls.part.has(Part.design.has(Design.design_number.ilike(formatted_query)))) elif attr == 'product_serial_number_query': formatted_query = format_match_query(params['product_serial_number_query_type'], params[attr]) query = query.filter(func.cast(cls.serial_number, types.Text).ilike(formatted_query)) elif attr == 'text_fields_query': formatted_query = format_match_query('includes', params['text_fields_query']) query = query.filter(cls.summary.ilike(formatted_query) | cls.notes.ilike(formatted_query)) elif attr == 'open_discrepancies': query = query.filter(cls.discrepancies.any(Discrepancy.state.in_(['Open']))) elif attr == 'created_on_start': query = query.filter(cls.created_at >= params['created_on_start']) elif attr == 'created_on_end': query = query.filter(cls.created_at <= params['created_on_end']) elif attr == 'in_open_state': query = query.filter(cls.state.in_(cls.workflow.open_states)) elif attr =='exclude_obsolete': query = query.filter(cls.state != cls.workflow.obsolete_state) elif attr == 'material_id': query = query.filter(cls.part.has(Part.material_id == params[attr])) return query.all() def get_approval_errors(self): approval_errors = [] if self.state == self.workflow.get_approval_state(): # Already in approval state, no need to do further checks return approval_errors # Check if not self_approved and either no approvers added or all approvers have already approved somehow. if not self.self_approved: if not self.approvers: approval_errors.append('You must add at least one approver.') elif all([approver.approved_at for approver in self.approvers]): approval_errors.append('You must add at least one approver.') # Check if open discrepancies for discrepancy in self.discrepancies: if discrepancy.is_open(): approval_errors.append('Discrepancy {0} must be resolved.'.format(discrepancy.discrepancy_number)) return approval_errors def get_name(self): return self.part.get_name() def get_unique_identifier(self): return '{0}-{1}'.format(self.part.part_number, self.serial_number) def get_url(self, external=False): return url_for('product.view_product', design_number=self.part.design.design_number, part_identifier=self.part.part_identifier, serial_number=self.serial_number, _external=external) def __str__(self): return self.product_number def __repr__(self): return '<Product({0})>'.format(self.product_number)
class Build(BaseRecord): __tablename__ = 'builds' build_identifier = Column(db.String, nullable=False, default='001') part_id = reference_col('parts') part = relationship('Part') notes = Column(db.Text) purchase_order = Column(db.String) products = relationship('Product', back_populates='build', order_by="Product.serial_number") documents = relationship('Document', secondary='builds_documents') discrepancies = relationship('Discrepancy', secondary='builds_discrepancies') vendor_id = reference_col('companies') vendor = relationship('Company') permissions = BuildPermissions() __table_args__ = (db.UniqueConstraint('build_identifier', 'part_id', name='build_identifier_part_unique'),) def __init__(self, **kwargs): super().__init__() db.Model.__init__(self, **kwargs) @property def build_number(self): return '{0}.{1}'.format(self.part.part_identifier, self.build_identifier) @property def discrepancy_number(self): return '{0}-B{1}'.format(self.part.part_number, self.build_identifier) @classmethod def get_next_build_identifier_for_design_number_and_part_identifier(cls, design_number, part_identifier): sql = 'SELECT b.* FROM builds b, parts p, designs d WHERE b.part_id = p.id AND p.design_id = d.id AND d.design_number = :design_number AND p.part_identifier = :part_identifier' results = db.session.query(cls).from_statement(db.text(sql).params(design_number=design_number, part_identifier=part_identifier)).all() if len(results) == 0: build_identifier = 1 else: # resultset = [row[0] for row in results] resultset = [b.build_identifier for b in results] resultset.sort() build_identifier = int(resultset[-1]) + 1 return '{0:03d}'.format(build_identifier) @classmethod def get_build_by_build_number(cls, design_number, part_identifier, build_identifier): sql = 'SELECT b.* FROM builds b, parts p, designs d WHERE b.part_id = p.id AND p.design_id = d.id AND d.design_number = :design_number AND p.part_identifier = :part_identifier AND b.build_identifier = :build_identifier' results = db.session.query(cls).from_statement(db.text(sql).params(design_number=design_number, part_identifier=part_identifier, build_identifier=build_identifier)).first() return results def find_all_build_identifiers(self): results = Build.query.with_entities(Build.build_identifier).order_by(Build.build_identifier.asc()).filter_by(part=self.part).all() resultset = [row[0] for row in results] return resultset def can_user_edit(self, field_name): # Build state is always open, so need to override can_user_edit if current_user.is_admin(): return True # Admins can do anything always role = 'all' if current_user == self.owner: role = 'owner' elif current_user.is_superuser(): role = 'superuser' state = 'open' return self.permissions.get_permissions().get(state, False).get(role, False).get(field_name) def __str__(self): return self.build_number def __repr__(self): return '<Build({0})>'.format(self.build_number)