class PurchaseBaseItem(doc.FieldSpecAware): material = doc.FieldTypedCode(codes.StockCode, none=False) uom = doc.FieldUom(none=False, default='pc') quantity = doc.FieldNumeric(default=1) revision = doc.FieldNumeric(none=True) size = doc.FieldString(none=True) location = doc.FieldString(default=Location.locations['STORE'].code, none=False) delivery_date = doc.FieldDateTime() net_price = doc.FieldNumeric()
class SalesOrderEntry(doc.Doc): material = doc.FieldTypedCode(codes.StockCode, none=False) revision = doc.FieldNumeric(none=True) quantity = doc.FieldNumeric(default=1, none=False) uom = doc.FieldUom(none=False) location = doc.FieldString(default=Location.locations['STORE'].code, none=False) size = doc.FieldString(none=True) # TODO: weight? net_price = doc.FieldNumeric(none=False, default=0) remark = doc.FieldString(none=True)
class PurchaseBase(doc.Authored): CURRENCY_THB = 'THB' CURRENCY_USD = 'USD' CURRENCY = ( (CURRENCY_THB, _('THB_LABEL')), (CURRENCY_USD, _('US_LABEL')), ) STATUS_OPEN = 0 STATUS_APPROVED = 1 STATUS_CLOSED = 2 STATUS_CANCELLED = 3 DOC_STATUSES = ( (STATUS_OPEN, _('PURCHASING_STATUS_OPEN')), (STATUS_APPROVED, _('PURCHASING_STATUS_APPROVED')), (STATUS_CLOSED, _('PURCHASING_STATUS_CLOSED')), (STATUS_CANCELLED, _('PURCHASING_STATUS_CANCELLED')), ) status = doc.FieldNumeric(choices=DOC_STATUSES, default=STATUS_OPEN, none=False) cancelled = doc.FieldDoc('event', none=True) doc_no = doc.FieldString() vendor = doc.FieldString(none=True) currency = doc.FieldString(choices=CURRENCY, default=CURRENCY_THB) items = doc.FieldList(doc.FieldNested(PurchaseBaseItem)) mrp_session = doc.FieldDoc('mrp-session', none=True) def cancel(self, user, **kwargs): if self.status == self.STATUS_CLOSED: raise ValidationError(_("ERROR_CANNOT_CANCEL_CLOSED_PR")) if self.status == self.STATUS_CANCELLED: raise ValidationError(_("ERROR_PR_ALREADY_CANCELLED")) self.status = self.STATUS_CANCELLED self.cancelled = doc.Event.create(doc.Event.CANCELLED, user, against=self) self.touched(user, **kwargs) def touched(self, user, **kwargs): # Check permission if not kwargs.pop("automated", False): self.assert_permission(user, self.PERM_W) super(PurchaseBase, self).touched(user, **kwargs)
class TaskComponent(doc.FieldSpecAware, decorators.JsonSerializable): # assign from front end material = doc.FieldTypedCode(codes.StockCode) # TypedCode quantity = doc.FieldNumeric(default=1, none=False) uom = doc.FieldUom(none=False) revision = doc.FieldNumeric(none=True) size = doc.FieldString(none=True) @classmethod def factory(cls, material, revision, size, quantity, uom=None, **kwargs): c = cls() c.material = material c.revision = revision c.size = size c.quantity = quantity c.uom = uom if uom is not None else MaterialMaster.get(material).uom return c def as_json(self): return { 'material': str(self.material), 'quantity': self.quantity, 'uom': self.uom, 'revision': self.revision, 'size': self.size } def material_key(self): return "%sr%s-%s" % (self.material, self.revision, self.size) def __repr__(self): return "TaskComponent material_key=[%s]" % self.material_key()
class PurchaseOrder(PurchaseBase): purchasing_group = doc.FieldString() payment_term = doc.FieldString() items = doc.FieldList(doc.FieldNested(PurchaseOrderItem)) # TODO: implement factory function to group PR into PO def save(self): if not self.doc_no: self.doc_no = doc.RunningNumberCenter.new_number(PO_NUMBER_KEY) super(PurchaseOrder, self).save() class Meta: collection_name = 'purchase_order' require_permission = True doc_no_prefix = PO_NUMBER_PREFIX
class TestExtendDoc(docs.Validatable, TestBaseDoc): """ Also use multiple inheritance """ field_a = docs.FieldNumeric(default=2) field_b = docs.FieldString(default="default_value2") class Meta: collection_name = ':extend'
class QualityDocument(doc.Authored): ref_doc = doc.FieldAnyDoc() cancelled = doc.FieldDoc('event') doc_no = doc.FieldString(none=True) items = doc.FieldList(doc.FieldNested(QualityItem)) class Meta: collection_name = 'quality_document' require_permission = True doc_no_prefix = QUALITY_DOC_PREFIX
class Authentication(doc.Doc): """ Authentication token Parameter meanings User (requester) requested action (action) override authentication by (authorizer) on date (doc) Token = object_id """ requester = doc.FieldIntraUser(none=False) authorizer = doc.FieldIntraUser(none=False) target_permissions = doc.FieldList(doc.FieldString(), none=False) created = doc.FieldDateTime(none=False) @classmethod def create(cls, requester, authenticated_by, authentication_challenge, target_permissions): """ :param IntraUser requester: :param IntraUser authenticated_by: :param basestring authentication_challenge: :param basestring|list target_permissions: :raise ValidationError: :return Authentication: """ # sanitize input if isinstance(target_permissions, basestring): target_permissions = [target_permissions] # Validate if target user has enough permission allow such task to happen? if any(not authenticated_by.can(p) for p in target_permissions): raise ValidationError( _("ERR_AUTHENTICATE_USER_DOES_NOT_HAVE_TARGET_PERMISSION: %(target_permissions)s" ) % {'target_permissions': ",".join(target_permissions)}) # Validate password challenge if not authenticated_by.check_password(authentication_challenge): raise ValidationError(_("ERR_AUTHENTICATION_FAILED")) # Validated, success create the token o = cls() o.target_permissions = target_permissions o.requester = requester o.authorizer = authenticated_by o.created = NOW() o.save() return o class Meta: collection_name = 'authentication'
class OffHours(docs.Doc): start_time = docs.FieldDateTime() end_time = docs.FieldDateTime() uid = docs.FieldString() def __init__(self, object_id=None): super(OffHours, self).__init__(object_id) def __lt__(self, other): return self.start_time < other.start_time def __gt__(self, other): return self.start_time > other.start_time def __repr__(self): return "%s [%s - %s]" % (self.uid, self.start_time, self.end_time) @staticmethod def sync(ics_url=None, verbose=False): ics_url = ics_url if ics_url is not None else OFFHOURS_ICS_URL if ics_url is None: raise ProhibitedError("unable to sync() ics_url must not be null") if verbose: print("Performing sync with URL=%s" % ics_url) # download from ics_url p = parser.CalendarParser(ics_url=ics_url) bulk = OffHours.manager.o.initialize_ordered_bulk_op() for event in p.parse_calendar(): uid = "%s/%s" % (event.recurrence_id, event.uid) bulk.find({'uid': uid}).upsert().update({'$set': { 'uid': uid, 'start_time': event.start_time, 'end_time': event.end_time }}) r = bulk.execute() if verbose: print("Sync result:") for o in r.iteritems(): print("\t%s = %s" % o) OffHoursRange.sync() return r class Meta: collection_name = "offhours" indices = [ ([("uid", 1)], {"unique": True, "sparse": False}), ([("start_time", 1)], {"background": True, "sparse": False}) # improve search performance ]
class MRPSessionExecutionRecordEntry(doc.FieldSpecAware): marker = doc.FieldDateTime(none=False) quantity = doc.FieldNumeric() ref_docs = doc.FieldAnyDoc() original = doc.FieldBoolean(default=False) remark = doc.FieldString(none=True) @classmethod def create(cls, marker, quantity, ref_docs, original, remark=None): o = MRPSessionExecutionRecordEntry() o.marker = marker o.quantity = quantity o.ref_docs = ref_docs o.original = original o.remark = remark return o @staticmethod def initial_balance_marker(): return datetime.fromtimestamp(0) @staticmethod def safety_stock_marker(): return datetime.fromtimestamp(1) def __repr__(self): name = self.marker.strftime('%Y-%m-%d %H:%M') if self.marker == MRPSessionExecutionRecordEntry.initial_balance_marker( ): name = "Initial Balance" elif self.marker == MRPSessionExecutionRecordEntry.safety_stock_marker( ): name = "Safety Stock" return "[%16s] %s%4s" % (name, '-' if self.quantity < 0 else '+', abs(self.quantity)) def __cmp__(self, other): if self.marker == other.marker: return cmp(self.quantity, other.quantity) return cmp(self.marker, other.marker)
class DefectItem(doc.FieldSpecAware): defect = doc.FieldString() quantity = doc.FieldNumeric()
class TestExtenderDoc(TestExtendDoc): field_b = docs.FieldString(default="default_value3") field_c = docs.FieldString(default="exclusive_value") class Meta: collection_name = ':extender'
class TestTupleFieldDoc(docs.Doc): tuple_items = docs.FieldTuple(docs.FieldNumeric(), docs.FieldNumeric(), docs.FieldString()) class Meta: collection_name = 'test-tuple'
class TaskDoc(doc.Authored): task = doc.FieldTask(none=False) """:type : Task""" planned_duration = doc.FieldNumeric(none=True) # save in minutes actual_start = doc.FieldDateTime(none=True) actual_duration = doc.FieldNumeric(none=True, default=0) actual_end = doc.FieldDateTime(none=True) assignee = doc.FieldAssignee() confirmations = doc.FieldList(doc.FieldNested(Confirmation)) ref_doc = doc.FieldAnyDoc() cancelled = doc.FieldDoc('event') details = doc.FieldSpec(dict, none=True) doc_no = doc.FieldString(none=True) def validate(self): if not self.task: raise ValidationError(_("ERROR_TASK_IS_REQUIRED")) if self.assignee is None: self.assignee = self.task.default_assignee() def assert_assignee(self, assignee): """ Validate if assignee as IntraUser is applicable for the task. :param assignee: :return: """ if isinstance(assignee, IntraUser): if not assignee.can("write", self.task.code): raise ValidationError( _("ERR_INVALID_ASSIGNEE: %(user)s %(task_code)s") % { 'user': self.assignee, 'task_code': self.task.code }) elif isinstance(assignee, TaskGroup): if self.task.default_assignee() is not self.assignee: raise ValidationError( _("ERR_INVALID_ASSIGNEE: %(group)s %(task_code)s") % { 'task_code': self.task.code, 'group': self.assignee }) def cancel_confirmation(self, user, index, **kwargs): self.confirmations[index].cancelled = doc.Event.create( doc.Event.CANCELLED, user) self.touched(user, **kwargs) def cancel(self, user, **kwargs): if self.cancelled: raise ValidationError(_("ERROR_TASK_IS_ALREADY_CANCELLED")) self.cancelled = doc.Event.create(doc.Event.CANCELLED, user, against=self) self.touched(user, **kwargs) @classmethod def confirmation_class(cls): return Confirmation def confirm(self, user, confirmation, **kwargs): if not isinstance(confirmation, Confirmation): raise BadParameterError( _("ERROR_CONFIRMATION_MUST_BE_INSTANCE_OF_CONFIRMATION")) # for first confirmation, init array. otherwise, append it if len(self.confirmations) > 0: self.confirmations.append(confirmation) else: self.confirmations = [confirmation] # dispatch event signals.task_confirmed.send(self.__class__, instance=self, code=str(task.code), confirmation=confirmation) self.touched(user, **kwargs) def touched(self, user, **kwargs): bypass = kwargs.pop("bypass", None) if not kwargs.pop("automated", False) and not bypass: self.assert_permission(user, self.PERM_W, self.task.code) super(TaskDoc, self).touched(user, **kwargs) def save(self): if not self.doc_no: self.doc_no = doc.RunningNumberCenter.new_number(TASK_NUMBER_KEY) super(TaskDoc, self).save() class Meta: collection_name = 'task' require_permission = True permission_write = task.Task.get_task_list() + [ TASK_PERM_OVERRIDE_ASSIGNEE ] doc_no_prefix = TASK_NUMBER_PREFIX
class TestBaseDoc(docs.Doc): field_a = docs.FieldString(default="default_value") class Meta: collection_name = 'test'
class SalesOrder(doc.Authored): CURRENCY_THB = 'THB' CURRENCY_USD = 'USD' CURRENCY = ( (CURRENCY_THB, _('THB_LABEL')), (CURRENCY_USD, _('US_LABEL')), ) STATUS_OPEN = 'OPEN' STATUS_CLOSED = 'CLOSED' SALES_ORDER_STATUSES = ( (STATUS_OPEN, _('SALES_ORDER_STATUS_OPEN')), (STATUS_CLOSED, _('SALES_ORDER_STATUS_CLOSED')), ) doc_no = doc.FieldString(none=True) # Running Number customer = doc.FieldTypedCode(codes.CustomerCode, none=False) delivery_date = doc.FieldDateTime(none=False) status = doc.FieldString(none=False, choices=SALES_ORDER_STATUSES, default=STATUS_OPEN) sales_rep = doc.FieldIntraUser(none=True) customer_po = doc.FieldString(none=True) customer_po_date = doc.FieldDateTime(none=True) customer_currency = doc.FieldString(none=False, choices=CURRENCY, default=CURRENCY_THB) items = doc.FieldList(doc.FieldNested(SalesOrderEntry)) remark = doc.FieldString(none=True) def __init__(self, object_id=None): super(SalesOrder, self).__init__(object_id) if object_id is None: self.items = [] def validate(self): # FIXME: Check UOM for index, item in enumerate(self.items): material = MaterialMaster.factory(item.material) revisions = material.revisions() if revisions: revision_list = [rev.rev_id for rev in revisions] if item.revision not in revision_list: raise ValidationError( _("ERROR_REVISION_DOES_NOT_EXIST: %(material)s in item %(itemnumber)s only has revision %(revisionlist)s" ) % { 'material': item.material.code, 'itemnumber': index + 1, 'revisionlist': revision_list }) schematic = filter(lambda r: item.revision is r.rev_id, revisions)[0] if item.size and item.size not in schematic.conf_size: raise ValidationError( _("ERROR_SIZE_DOES_NOT_EXIST: %(material)s revision %(materialrevision)s in item %(itemnumber)s only has size %(sizelist)s" ) % { 'material': item.material.code, 'materialrevision': schematic.rev_id, 'itemnumber': index + 1, 'sizelist': schematic.conf_size }) def touched(self, user, **kwargs): # Check permission if not kwargs.pop("automated", False): self.assert_permission(user, self.PERM_W) super(SalesOrder, self).touched(user, **kwargs) def save(self): if not self.doc_no: self.doc_no = doc.RunningNumberCenter.new_number( SALES_ORDER_NUMBER_KEY) super(SalesOrder, self).save() def add_entry(self, material, revision, net_price, quantity, **kwargs): entry = SalesOrderEntry() entry.material = material entry.revision = revision entry.size = kwargs.pop('size', None) entry.quantity = quantity entry.uom = kwargs.pop( 'uom', 'pc') # FIXME: populate from MaterialMaster instead. entry.remark = kwargs.pop('remark', None) entry.net_price = net_price self.items.append(entry) class Meta: collection_name = 'sales_order' require_permission = True doc_no_prefix = SALES_ORDER_NUMBER_PREFIX
class InventoryMovement(doc.Authored): GR_PD = 103 GR_BP = 531 GR_PR = 101 GR_LT = 107 GI_PD = 261 GI_SO = 601 GI_SC = 231 GI_CC = 201 ST_LL = 311 ST_LP = 312 ST_PL = 313 ST_LT = 314 ST_MM = 309 SA = 711 TYPES = ((GR_PD, _("GOOD_RECEIVED_PRODUCTION_ORDER")), (GR_BP, _("GOOD_RECEIVED_BY_PRODUCT")), (GR_PR, _("GOOD_RECEIVED_PURCHASE_ORDER")), (GR_LT, _("GOOD_RECEIVED_LOST_AND_FOUND")), (GI_PD, _("GOOD_ISSUED_PRODUCTION_ORDER")), (GI_SO, _("GOOD_ISSUED_SALES_ORDER")), (GI_SC, _("GOOD_ISSUED_SCRAP")), (GI_CC, _("GOOD_ISSUED_COST_CENTER")), (ST_LL, _("STOCK_TRANSFER_LOCATION_TO_LOCATION")), (ST_LP, _("STOCK_TRANSFER_LOCATION_TO_PRODUCTION")), (ST_LT, _("STOCK_TRANSFER_LOST_AND_FOUND")), (ST_PL, _("STOCK_TRANSFER_PRODUCTION_TO_LOCATION")), (ST_MM, _("STOCK_TRANSFER_MATERIAL_TO_MATERIAL")), (SA, _("STOCK_ADJUSTMENT"))) doc_no = doc.FieldString(none=True) # Running Number type = doc.FieldNumeric(choices=TYPES) cancel = doc.FieldDoc('inv_movement', none=True, unique=True, omit_if_none=True) """:type : InventoryMovement""" ref_ext = doc.FieldString(none=True) ref_doc = doc.FieldAnyDoc(none=True) posting_date = doc.FieldDateTime(none=True) """:type : datetime""" items = doc.FieldList(doc.FieldNested(InventoryMovementEntry)) """:type : list[InventoryMovementEntry]""" def is_good_received(self): return self.type in [ InventoryMovement.GR_PD, InventoryMovement.GR_PR, InventoryMovement.GR_BP, InventoryMovement.GR_LT ] def is_good_issued(self): return self.type in [ InventoryMovement.GI_CC, InventoryMovement.GI_PD, InventoryMovement.GI_SC, InventoryMovement.GI_SO ] def is_transfer(self): return self.type in [ InventoryMovement.ST_LL, InventoryMovement.ST_MM, InventoryMovement.ST_LP, InventoryMovement.ST_PL, InventoryMovement.ST_LT ] def is_adjust(self): return self.type in [InventoryMovement.SA] @classmethod def factory(cls, type, items, ref_doc=None): """ Create InventoryMovement :param type: :param items: array of tuple or list of (material_code, quantity) :param ref_doc: :return: """ # Sanitize 'items' based on 'type' if type in [cls.GI_CC, cls.GI_PD, cls.GI_SC, cls.GI_SO]: # Convert incoming tuple to InventoryMovementEntry pass elif type in [cls.GR_PD, cls.GR_PR, cls.GR_BP, cls.GR_LT]: # Convert incoming tuple to InventoryMovementEntry pass elif type in [cls.ST_LL, cls.ST_LP, cls.ST_PL, cls.ST_MM, cls.ST_LT]: # Let it go ~~ pass else: raise ValidationError( 'Factory method cannot handle document type of %s.' % type) o = cls() o.type = type o.items = items o.ref_doc = ref_doc return o def validate(self, user=None): if user: self.created_by = user super(InventoryMovement, self).validate() # make sure all children has batch number if our doc_type is NOT IN GR if not self.is_good_received() or self.cancel is not None: if any(i.batch is None for i in self.items): raise ValidationError(_("ERROR_BATCH_IS_REQUIRED")) if self.is_good_issued(): if any(i.quantity > 0 for i in self.items) and self.cancel is None: raise ValidationError( _("ERROR_GOOD_ISSUE_QUANTITY_MUST_BE_NEGATIVE")) if any(i.quantity < 0 for i in self.items) and self.cancel is not None: raise ValidationError( _("ERROR_CANCELLED_GOOD_ISSUE_QUANTITY_MUST_BE_POSITIVE")) if self.is_transfer(): if len(self.items) % 2 != 0: raise ValidationError( _("ERROR_TRANSFER_MUST_HAVE_EVEN_NUMBER_OF_ITEMS")) # Validate based on Good Received, Good Issued, SA, etc. logic InventoryContent.apply(self, True) def do_cancel(self, user, reason, **kwargs): """ Create Cancel Movement from Original InventoryMovement. :param user: :param reason: :param kwargs: :return: """ src = self.serialized() cancellation = InventoryMovement() # cancellation = deepcopy(self) src['_id'] = cancellation.object_id src['doc_no'] = None del src['created_by'] del src['edited_by'] cancellation.deserialized(src) cancellation.cancel = self cancellation.posting_date = NOW() for item in cancellation.items: item.quantity *= -1 item.weight *= -1 item.value *= -1 item.reason = reason cancellation.touched(user, **kwargs) return cancellation def touched(self, user, **kwargs): """ :param IntraUser user: :param kwargs: :return: """ # Check permission if not kwargs.pop("automated", False): self.assert_permission(user, self.PERM_W, self.type) # initialisation of conditional default value if self.doc_no is not None or not self.is_new(): raise ValidationError(_("MATERIAL_MOVEMENT_IS_NOT_EDITABLE")) super(InventoryMovement, self).touched(user, **kwargs) # Post the changes to InventoryContent InventoryContent.apply(self) def save(self): self.doc_no = doc.RunningNumberCenter.new_number(MOVEMENT_NUMBER_KEY) # Apply doc_no if is GR && not cancelling if self.is_good_received() and (self.cancel is False or self.cancel is None): for item in self.items: item.batch = self.doc_no # Perform Save Operation super(InventoryMovement, self).save() class Meta: collection_name = 'inv_movement' indices = [([("cancel", 1)], {"unique": True, "sparse": True})] require_permission = True permission_write = [ 103, 101, 107, 531, 261, 601, 231, 201, 311, 312, 313, 314, 309, 711 ] doc_no_prefix = MOVEMENT_NUMBER_PREFIX
class MRPSessionExecutionRecord(doc.FieldSpecAware): # Sorting affinity = doc.FieldNumeric(default=0, none=False) # type: int # ID material = doc.FieldTypedCode(codes.StockCode, none=False) # type: codes.StockCode revision = doc.FieldNumeric(default=None) # type: int size = doc.FieldString(default=None) # type: basestring # Context material_master = doc.FieldSpec(MaterialMaster, transient=True) # type: MaterialMaster mrp_session_id = doc.FieldObjectId(transient=True) # type: ObjectId # Calculation Buffer entries = doc.FieldList(doc.FieldNested(MRPSessionExecutionRecordEntry)) created_supplies = doc.FieldList( doc.FieldNested(MRPSessionExecutionRecordEntry)) # Snapshot meta from material_master reorder_point = doc.FieldNumeric(default=0, none=False) lead_time = doc.FieldNumeric(default=0, none=False) procurement_type = doc.FieldString(none=False) mrp_type = doc.FieldString(max_length=1) def create_demand_groups(self): lot_size = self.material_master.lot_size if lot_size in [ MaterialMaster.LZ_LOT_FOR_LOT, MaterialMaster.LZ_MAX_STOCK_LEVEL ]: def lot_for_lot(): r = [] for a in self.entries: if a.marker.date() in [ MRPSessionExecutionRecordEntry. initial_balance_marker(), MRPSessionExecutionRecordEntry.safety_stock_marker( ) ]: r.append(a) else: if len(r) > 0: yield None, r r = [] yield a.marker, [a] return lot_for_lot() elif lot_size in [ MaterialMaster.LZ_DAILY, MaterialMaster.LZ_MONTHLY, MaterialMaster.LZ_WEEKLY ]: def create_timed_lot_size(lz): def _group_id(dt): if lz == MaterialMaster.LZ_DAILY: return dt.date() elif lz == MaterialMaster.LZ_MONTHLY: return "%04d%02d" % (dt.year, dt.month) elif lz == MaterialMaster.LZ_WEEKLY: return "%04d%02d" % (dt.year, dt.date().isocalendar()[1]) else: raise err.ProhibitedError('Invalid lot_size value=%s' % lot_size) def timed_lot_size(): group_point = None r = [] for e in self.entries: if group_point is None: # do not yield the automatic ones group_point = _group_id(e.marker) r.append(e) elif group_point != _group_id(e.marker): yield group_point, r # renew group_point = _group_id(e.marker) r = [e] else: r.append(e) if len(r) > 0: yield group_point, r return timed_lot_size return create_timed_lot_size(lot_size)() raise err.ProhibitedError('Unknown lot_size %s' % lot_size) def populate(self, path): if path == 'material_master' and self.material_master is None and self.material is not None: key = str(self.material) mms = MaterialMaster.manager.find(cond={'code': key}, pagesize=1) if len(mms) == 0: raise err.ProhibitedError('Material Master %s is missing' % key) self.material_master = mms[0] # type: MaterialMaster self.mrp_type = self.material_master.mrp_type self.reorder_point = 0 if self.material_master == MaterialMaster.MRP else self.material_master.reorder_point self.lead_time = self.material_master.gr_processing_time self.procurement_type = self.material_master.procurement_type else: super(MRPSessionExecutionRecord, self).populate(path) return self def delete_weak_supplies(self, verbose=None): """ Logic ===== Disregard all unwanted supply (Previously generated ProductionOrder/PurchaseRequisition) - ProductionOrder * mrp_session != None * mrp_session != self.mrp_session * status = OPEN * material = focal_material - PurchaseRequisition * status = OPEN * mrp_session != None * mrp_session != self.mrp_session * PR generated by MRP has only 1 children, lookup focal_material within PurchaseRequisition items :param verbose: (sub routine to print verbose message) :return: """ ProductionOrder.manager.delete( verbose=False if verbose is None else verbose, cond={ 'status': ProductionOrder.STATUS_OPEN, 'mrp_session': { '$nin': [None, self.mrp_session_id] }, 'material': str(self.material), 'revision': self.revision, 'size': self.size }) PurchaseRequisition.manager.delete( verbose=False if verbose is None else verbose, cond={ 'status': PurchaseRequisition.STATUS_OPEN, 'mrp_session': { '$nin': [None, self.mrp_session_id] }, 'items': { '$elemMatch': { 'material': str(self.material), 'revision': self.revision, 'size': self.size } } }) def gather(self, verbose=None): self.entries = [] self._gather_demands() demand_count = len(self.entries) if verbose: verbose("demand_collected=%s" % len(self.entries), "i", level=1) self._gather_supplies() if verbose: verbose("supply_collected=%s" % (len(self.entries) - demand_count), "i", level=1) self.entries = sorted(self.entries) self.created_supplies = [] def create_supply(self, shortage, due, session, ref_doc=None, remark=None, verbose=None): """ Modify add supply to sequence, and return replenished amount Replenished amount is ... shortage + lot_size_arg if lot_size = MaterialMaster.LZ_MAX_STOCK_LEVEL shortage if otherwise Supply Sequence can be broken into lots using lot_size_max Each sub_lot_size must be greater or equals to lot_size_min lead_time is taken into consideration as a due due is given as exact moment when it will be used, therefore it will be offset by 1 hours before such exact hours. (Please, Consider exclude OffHours as optional feature) :param int shortage: :param due: :param MRPSession session: :param ref_doc: :param basestring remark: :param verbose: :return: (amount replenished) """ if shortage == 0: return 0 if self.lead_time is None or self.reorder_point is None: raise err.ProhibitedError('Cannot create supply without lead_time') offset_due = due - timedelta(hours=1, days=self.lead_time) # Compute optimal lot_size procurement_type = self.material_master.procurement_type lots = [] lot_size_max = self.material_master.lot_size_max lot_size_min = self.material_master.lot_size_min # lot_size_arg = self.material_master.lot_size_arg # Capped with lot_size_max if lot_size_max is not None and lot_size_max > 0: lot_count = int(shortage) / int(lot_size_max) supplied = 0 for i in xrange(0, lot_count - 1): lots.append(lot_size_max) supplied += lot_size_max lots.append(shortage - supplied) else: lots = [shortage] # Push to lot_size_min if lot_size_min is not None and lot_size_min > 0: lots = map(lambda a: max(lot_size_min, a), lots) # Actually create supply references = [] if procurement_type == MaterialMaster.INTERNAL: # ProductionOrder # Need to honour scrap percentage here scrap_percentage = self.material_master.scrap_percentage references.extend( map( lambda a: ProductionOrder.factory( self.material, self.revision, self.size, offset_due, a, session.issued_by, ref_doc=ref_doc, remark=remark, scrap_percentage=scrap_percentage, mrp_session_id=session.object_id), lots)) elif procurement_type == MaterialMaster.EXTERNAL: # PurchaseRequisition # FIXME: Consider adding remark/ref_doc to the output document. references.extend( map( lambda a: PurchaseRequisition.factory( self.material, self.revision, self.size, offset_due, a, session.issued_by, mrp_session_id=session.object_id), lots)) verbose("due=%s lots=%s remark=%s" % (due, lots, remark), "S", 1) return reduce(lambda o, a: o + a, lots, 0) def _gather_demands(self): demands = [] based_material = str(self.material) # (0) 'SafetyStock' - create as Demand of Session's Sequence demands.append( MRPSessionExecutionRecordEntry.create( marker=MRPSessionExecutionRecordEntry.safety_stock_marker(), quantity=-self.material_master.safety_stock, ref_docs=None, original=True, remark="Safety Stock")) def is_focal_material(o): return o.material == based_material and o.revision == self.revision and o.size == self.size # (1) SalesOrder, (status = OPEN, material=seq.material) sales_orders = SalesOrder.manager.find( cond={ 'status': SalesOrder.STATUS_OPEN, 'items': { '$elemMatch': { 'material': based_material, 'revision': self.revision, 'size': self.size } } }) for sales_order in sales_orders: for item in filter(is_focal_material, sales_order.items): o = MRPSessionExecutionRecordEntry.create( marker=sales_order.delivery_date, quantity=item.uom.convert(self.material_master.uom, -item.quantity), ref_docs=sales_order, original=True, remark="/".join( filter(lambda a: a is not None and len(a) > 0, [sales_order.remark, item.remark]))) demands.append(o) # (2) ProductionOrderOperations # BoM of Tasks tasks = ProductionOrderOperation.manager.find( cond={ 'materials': { '$elemMatch': { 'material': based_material, 'revision': self.revision, 'size': self.size } } }) for task in tasks: for item in filter(is_focal_material, task.materials): o = MRPSessionExecutionRecordEntry.create( marker=task.planned_start, quantity=item.uom.convert(self.material_master.uom, -item.quantity), ref_docs=task, original=True) demands.append(o) self.entries.extend(demands) def _gather_supplies(self): """ Get supplies from non-OPEN production orders, purchase orders, stock_content exclude [OPEN production order, purchase requisition, purchase order ...] :return: """ supplies = [] based_material = str(self.material) # (0) Stock Content amount = InventoryContent.get_value(self.material) supplies.append( MRPSessionExecutionRecordEntry.create( marker=MRPSessionExecutionRecordEntry.initial_balance_marker(), quantity=amount, ref_docs=None, original=True, remark="Current Stock Level")) # (1) Production Order, unconfirmed production order ... production_orders = ProductionOrder.manager.find( cond={ 'status': { '$lt': ProductionOrder.STATUS_CONFIRMED }, 'material': based_material, 'revision': self.revision, 'size': self.size }) for po in production_orders: o = MRPSessionExecutionRecordEntry.create( marker=po.planned_end, quantity=po.uom.convert(self.material_master.uom, po.supply_quantity()), ref_docs=po, original=True) supplies.append(o) def is_focal_material(o): return o.material == based_material and o.revision == self.revision and o.size == self.size # (2) Purchase Requisition (partially converted - consider as supply) purchase_requisitions = PurchaseRequisition.manager.find( cond={ 'status': { '$lte': PurchaseRequisition.STATUS_PARTIAL_CONVERTED }, 'items': { '$elemMatch': { 'material': based_material, 'revision': self.revision, 'size': self.size } } }) # Translate purchase_requisitions => Supply for pr in purchase_requisitions: for item in filter(is_focal_material, pr.items): o = MRPSessionExecutionRecordEntry.create( marker=item.delivery_date, quantity=item.open_quantity if item.uom is None else item.uom.convert( self.material_master.uom, item.open_quantity), ref_docs=pr, original=True) supplies.append(o) # (3) Purchase Orders ... # TODO: Query Purchase Order = PurchaseOrder already exist, but not finished. (Wait for complete of Document) self.entries.extend(supplies) def records(self): """ Merged generators :return: """ def next_demands(): for d in self.demands: yield d def next_supplies(): for s in self.supplies: yield s def next_or_none(generator): try: return next(generator) except StopIteration: return None demands = next_demands() supplies = next_supplies() next_d = None next_s = None while True: next_d = next_or_none(demands) if next_d is None else next_d next_s = next_or_none(supplies) if next_s is None else next_s # compare for next one # -> Escape case if next_d is None and next_s is None: break # compare case if next_d is not None and next_s is not None: # compare which one is lesser if next_d > next_s: current = next_s next_s = None else: current = next_d next_d = None elif next_d is None: current = next_s next_s = None else: current = next_d next_d = None yield current for o in (self.demands + self.supplies): yield o def __cmp__(self, other): return other.affinity - self.affinity def __repr__(self): return "SEQ %6s ~> %s revision=%s size=%s" % ( self.affinity, self.material, self.revision, self.size)
class MRPSession(doc.Doc): """ Contains report of MRP session ran """ start = doc.FieldDateTime(none=False) end = doc.FieldDateTime(none=True) issued_by = doc.IntraUser() sequence = doc.FieldList(doc.FieldNested(MRPSessionExecutionRecord)) target_materials = doc.FieldList(doc.FieldTuple( doc.FieldTypedCode(codes.StockCode), doc.FieldNumeric(none=True), doc.FieldString(none=True)), none=True) errors = doc.FieldList(doc.FieldString()) processes = {} skip_indices = {} def __init__(self, object_id=None): super(MRPSession, self).__init__(object_id) if object_id is None: self.sequence_cache = {} self.sequence = [] self.target_materials = None def load(self): super(MRPSession, self).load() # update sequence cache self.sequence_cache = dict( map(lambda v: (str(v.material), v), self.sequence)) def run(self): self._begin() try: def report(message, group=None, level=0): if group is None: LOG.debug("%s%s" % ("\t" * level, message)) else: LOG.debug("%s%s: %s" % ("\t" * level, group, message)) report("\nBuilding Run Sequences") self._build_run_sequences() report("\nInitial sequence" % self.sequence) if len(self.sequence) == 0: report("(No sequence initialized)") else: for s in self.sequence: report(s, "i", 1) report("") # Run sequence; Execute mrp for seq in self.sequence: # Read Material # => update reorder_point, lead_time, lot_size -> making 'create_supply()' seq.populate('material_master') # Extract attributes reorder_point = seq.reorder_point procurement_type = seq.material_master.procurement_type lot_size = seq.material_master.lot_size mrp_type = seq.material_master.mrp_type report("Started %s" % seq) report("reorder_point=%s" % reorder_point, "i", 1) report("lot_size=%s" % lot_size, "i", 1) report( "supply_type=%s" % ('PurchaseRequisition' if procurement_type == MaterialMaster.EXTERNAL else 'ProductionOrder'), 'i', 1) # Type = 'MRP' or 'Reorder', if 'No MRP' = Skip if mrp_type not in [ MaterialMaster.MRP, MaterialMaster.REORDER ]: raise err.ProhibitedError( 'Unable to process MRP Sequence %s - incorrect MRP type' % seq.material) # Delete weak supply seq.delete_weak_supplies(verbose=report) # Gathering Demand/Supply seq.gather(verbose=report) report("-", "i", 1) # Go through Demand/Supply in chronological order # Try to resolve negative balance situation. current = 0 for group_id, a in seq.create_demand_groups(): first_marker = a[0].marker # print demand report("marker=%s group_id=%s" % (first_marker, group_id), "O", 1) # e = MRPSessionExecutionRecordEntry for e in a: current += e.quantity report("%s\t= %s" % (e, current), level=2) # For Every demand group, we need to produce a supply for it. # TODO: Each demand group might have a supply within that will in turn misled the calculation. if current <= seq.reorder_point: reorder_amount = current - seq.reorder_point # MAX_STOCK_LEVEL if lot_size == MaterialMaster.LZ_MAX_STOCK_LEVEL: max_stock_level = seq.material_master.lot_size_arg if max_stock_level <= 0 or max_stock_level is None: raise err.BadParameterError( 'Invalid lot_size_arg for material %s' % seq.material) reorder_amount = current - max_stock_level # IF supply is needed if reorder_amount < 0: replenished = seq.create_supply(-reorder_amount, first_marker, self, ref_doc=e.ref_docs, remark=e.remark, verbose=report) current += replenished # create sequence handle this replenishing # just for printing sake rec = MRPSessionExecutionRecordEntry.create( marker=first_marker, quantity=replenished, ref_docs=e.ref_docs, remark=e.remark, original=False) seq.created_supplies.append(rec) report("%s\t= %s %s" % (rec, current, rec.remark), level=2) else: report("no shortage found", "i", 1) report("DONE\n", "i", level=1) self._enclose() except Exception as e: LOG.error(traceback.format_exc()) self._enclose(False, [str(e)]) def _build_run_sequences(self): """ Populate sequence variables :return: """ initial_affinity = 200000 # Add manually inserted material first if self.target_materials is not None and len( self.target_materials) > 0: map( lambda t: self._add_sequence(t[0], t[1], t[2], initial_affinity ), self.target_materials) # Query production order # Walk down each production order schematic, extract required materials # - any materials that is referenced pull these from material master instead. cond = {'status': {'$lt': ProductionOrder.STATUS_CONFIRMED}} if self.target_materials is not None and len( self.target_materials) > 0: cond['$or'] = map( lambda t: { 'material': str(t[0]), 'revision': t[1], 'size': t[2] }, self.target_materials) orders = ProductionOrder.manager.find(cond=cond) # Extract schematic # populate/update affinity values for order in orders: seq = self._add_sequence(order.material, order.revision, order.size, initial_affinity) if seq is None: continue new_affinity = seq.affinity - 1 order.populate('operation') for op in order.operation: for po_comp in filter(lambda m: m.quantity > 0, op.materials): self._add_sequence(po_comp.material, po_comp.revision, po_comp.size, new_affinity) self.sequence = sorted(self.sequence) def _add_sequence(self, stock_code, revision, size, affinity): """ Nested building running sequences. :param stock_code: :param affinity: :return: """ key = "%sr%s-%s" % (str(stock_code), str(revision), str(size)) mm = MaterialMaster.get(stock_code) # Verify if order.material has type != 'No MRP' if key not in self.skip_indices: # Cache self.skip_indices[key] = (mm.mrp_type == MaterialMaster.NO_MRP) if self.skip_indices[key]: # Skip due to NO_MRP return None if key not in self.sequence_cache: LOG.debug("[+]\tSequence key=%s" % key) o = MRPSessionExecutionRecord() o.affinity = affinity o.revision = revision o.size = size o.material = stock_code o.mrp_session_id = self.object_id self.sequence_cache[key] = o self.sequence.append(o) # discover its own schematic from its default revision if mm.procurement_type == MaterialMaster.INTERNAL: sch = Schematic.of(mm.object_id, revision) if sch is None: raise ValueError( 'No schematic found for %s rev%s (material=%s)' % (stock_code, revision, mm.object_id)) for sch_entry in sch.schematic: for mat in sch_entry.materials: if any(q > 0 for q in mat.quantity): stock_code = mat.code bom_mm = MaterialMaster.factory(mat.code) if bom_mm.procurement_type is MaterialMaster.EXTERNAL or bom_mm.schematic is None: self._add_sequence(stock_code, None, None, affinity - 1) else: bom_mm.populate('schematic') self._add_sequence(stock_code, bom_mm.schematic.rev_id, size, affinity - 1) else: self.sequence_cache[key].affinity = min( self.sequence_cache[key].affinity, affinity) r = self.sequence_cache[key] return r def _begin(self): self.start = datetime.now() self.end = None self.errors = [] self.save() self._log(doc.Event.CREATED) def _enclose(self, ok=True, errors=None): self.start = self.start self.end = datetime.now() if errors is not None: self.errors.extend(errors) self.save() self._log(doc.Event.FINISHED if ok else doc.Event.CANCELLED) def _log(self, what, **kwargs): kwargs.update({'against': self}) doc.Event.create(what, self.issued_by, **kwargs) @classmethod def kickoff(cls, user, materials=None): """ Create new MRP session, extracting existing data :param materials - list of tuple [(stock_code, revision, size)] :return: MRPSession Object """ # Check is_running if cls.running_session() is not None: raise err.ProhibitedError('There is MRP Session running.') if not isinstance(materials, (type(None), list)): raise err.ProhibitedError( 'Bad value - materials must be list of tuple size of 3') session = MRPSession() session.target_materials = materials session.issued_by = user key = str(session.object_id) LOG.debug("Running MRP Session=%s" % key) t = multiprocessing.Process(target=MRPSession.run, args=(session, )) cls.processes[key] = t t.start() return session @classmethod def running_session(cls): found = cls.manager.find(pagesize=1, cond={'end': None}) return found[0] if len(found) > 0 else None @classmethod def wait(cls): for t in cls.processes.values(): t.join() class Meta: collection_name = 'mrp-session' require_permission = True
class InventoryMovementEntry(doc.FieldSpecAware): material = doc.FieldTypedCode(codes.StockCode, none=False) # can intercept string """:type: codes.StockCode""" quantity = doc.FieldNumeric(default=1, none=False) batch = doc.FieldString() # Assigned from InventoryMovementInstance """:type: basestring""" value = doc.FieldNumeric() weight = doc.FieldNumeric(none=True) location = doc.FieldString(default=Location.factory('STORE').code, none=False) """:type : Location|basestring""" ref_doc = doc.FieldAnyDoc(none=True) # For production order only """:type : doc.Doc""" reason = doc.FieldString(lov=MOVEMENT_LOV_KEY, none=True) """:type: basestring""" @classmethod def factory(cls, material, quantity, location=None, ref_doc=None, reason=None, batch=None, value=None, weight=None): i = cls() i.material = material i.quantity = quantity i.ref_doc = ref_doc i.reason = reason i.batch = batch i.value = value i.weight = weight if location: i.location = location return i @classmethod def transfer_pair_factory(cls, material, quantity, from_location, to_location, from_ref_doc=None, to_ref_doc=None): """ :param material: :param quantity: :param from_location: :param to_location: :param from_ref_doc: :param to_ref_doc: :return: cursor of InventoryMovementEntry """ # Query inventory content, FIFO batch_candidate = InventoryContent.manager.project( cond={ 'material': str(material), 'location': from_location, 'quantity': { '$gt': 0 } }, project=['_id', 'batch', 'quantity', 'value', 'weight'], sort=[("batch", 1)]) leftover = quantity usage_tuples = [] # batch, quantity, value, weight for a in batch_candidate: batch_quantity = float(a['quantity']) used = min(batch_quantity, leftover) consumption_ratio = used / batch_quantity delta_value = float(a['value']) * consumption_ratio delta_weight = float(a['weight']) * consumption_ratio leftover -= used usage_tuples.append((a['batch'], used, delta_value, delta_weight)) if leftover <= 0: break if leftover > 0: raise ValidationError( _('ERR_FAILED_TO_ALLOCATE_MATERIAL_TRANSFER_PAIR: %(material)s %(from_location)s %(to_location)s' ) % { 'material': material, 'from_location': from_location, 'to_location': to_location }) def create(): for p in usage_tuples: o = cls() o.material = material o.quantity = -p[1] o.batch = p[0] o.value = -p[2] o.weight = -p[3] o.ref_doc = from_ref_doc o.location = from_location yield o i = cls() i.material = material i.quantity = p[1] i.batch = p[0] i.value = p[2] i.weight = p[3] i.ref_doc = to_ref_doc i.location = to_location yield i return create()
class InventoryContent(doc.Doc): # Primary Key: material = doc.FieldTypedCode(codes.StockCode, none=False) """:type : codes.StockCode""" location = doc.FieldString(default=Location.locations['STORE'].code, none=False) """:type : Location""" batch = doc.FieldString() ref_doc = doc.FieldAnyDoc(none=True) # Content Values quantity = doc.FieldNumeric(default=0) value = doc.FieldNumeric(default=0) weight = doc.FieldNumeric(default=None, none=True) def add(self, quantity, value, weight): new_quantity = self.quantity + quantity new_value = self.value + value if new_quantity < 0: raise ValidationError( _("ERROR_INSUFFICIENT_QUANTITY: %(content_signature)s %(content_quantity)s + %(additional_quantity)s < 0" ) % { 'content_quantity': self.quantity, 'additional_quantity': quantity, 'content_signature': str(self) }) new_weight = None if weight is not None or self.weight is not None: new_weight = (self.weight or 0) + (weight or 0) if new_weight < 0: raise ValidationError( "%s: %s + %s < 0" % (_("ERROR_UNBALANCE_WEIGHT"), self.weight, weight)) self.quantity = new_quantity self.value = new_value self.weight = new_weight def should_delete(self): return self.quantity == 0 and self.weight < 0.01 and self.value < 0.01 @classmethod def get_value(cls, material, **kwargs): location = kwargs.pop('location', None) batch = kwargs.pop('batch', None) cond = { 'material': material if isinstance(material, basestring) else str(material) } if batch is not None: cond['batch'] = batch if location is not None: cond['location'] = location r = cls.manager.o.aggregate([{ '$match': cond }, { '$group': { '_id': None, 'total': { '$sum': '$quantity' } } }]) return 0 if len(r['result']) == 0 else r['result'][0]['total'] def __str__(self): return "INV_CNT %s %s %s (ref: %s)" % (self.material, self.location, self.batch, str(self.ref_doc)) @staticmethod def apply(movement, dry_run=False): """ Apply Inventory Movement to the Content :param InventoryMovement movement: :param bool dry_run: """ for item in movement.items: content = InventoryContent.factory(item.material, item.location, item.batch, item.ref_doc) content.add(item.quantity, item.value, item.weight) if not dry_run: if content.should_delete(): content.delete() else: content.save() @staticmethod def factory(material_code, location, batch, ref_doc): """ :param codes.StockCode material_code: :param Location|basestring location: :param basestring batch: :param doc.Doc ref_doc: :return: """ cond = { 'material': unicode(material_code), 'location': location, 'batch': batch } if ref_doc: key1, key2 = doc.FieldAnyDoc.as_value_for_query(ref_doc) cond['ref_doc.0'] = key1 cond['ref_doc.1'] = key2 else: cond['ref_doc'] = None r = InventoryContent.manager.find(1, 0, cond) # if no record found. if len(r) == 0: # Create unsaved new item o = InventoryContent() o.material = material_code o.location = location o.batch = None if batch == "" else batch o.ref_doc = ref_doc return o return r[0] @classmethod def query_content(cls, material=None, location=None, batch=None, ref_doc=None, **kwargs): """ A thin wrapper to make a inventory content query easier :param material: :param location: :param batch: :param ref_doc: :param kwargs: :return: """ cond = { 'material': material, 'location': location, 'batch': batch, 'ref_doc': ref_doc } # sanitize # (1) remove all none attributes del_keys = [] for k in cond: if cond[k] is None: del_keys.append(k) for k in del_keys: del cond[k] # (2) String attributes for k in ['material', 'location', 'batch']: if k in cond: cond[k] = str(cond[k]) # (3) take care of ref_doc field if 'ref_doc' in cond: if isinstance(cond['ref_doc'], doc.Doc): cond['ref_doc.0'] = cond['ref_doc'].object_id cond['ref_doc.1'] = cond['ref_doc'].manager.collection_name del cond['ref_doc'] elif isinstance(cond['ref_doc'], ObjectId): cond['ref_doc.0'] = cond['ref_doc'] del cond['ref_doc'] else: raise ValidationError( _("ERR_QUERY_CONTENT_CANNOT_INTERPRET_PARAMETER: %(parameter)s %(type)s" ) % { 'parameter': 'ref_doc', 'type': type(cond['ref_doc']) }) # query return cls.manager.find(cond=cond) class Meta: collection_name = 'inv_content' require_permission = True
class MaterialMaster(doc.Authored): MRP = 'M' NO_MRP = 'N' REORDER = 'R' MRP_TYPES = ( (MRP, _("MRP_TYPE_MRP")), (NO_MRP, _("MRP_TYPE_NO_MRP")), (REORDER, _("MRP_TYPE_REORDER")), ) EXTERNAL = 'E' INTERNAL = 'I' PROCUREMENT_TYPES = ((EXTERNAL, _("PROCUREMENT_TYPE_EXTERNAL")), (INTERNAL, _("PROCUREMENT_TYPE_INTERNAL"))) # Lot Size Policy LZ_LOT_FOR_LOT = 'EX' # One procurement for One demand LZ_DAILY = 'TB' # Accumulate daily demand to one procurement amount LZ_WEEKLY = 'WS' # Accumulate weekly demand to one procurement amount for the whole week based on offset (:lot_size_arg value 0 <= 6) LZ_MONTHLY = 'MS' # Accumulate monthly demand to one procurement amount for the whole month based on offset (:lot_size_arg value 0 <= 28) LZ_MAX_STOCK_LEVEL = 'HB' # maximize amount to the :lot_size_arg, at the multiplication of :lot_size_max, :lot_size_min LOT_SIZES = ( (LZ_LOT_FOR_LOT, _("LOT_SIZE_LOT_FOR_LOT")), (LZ_DAILY, _("LOT_SIZE_DAILY")), (LZ_WEEKLY, _("LOT_SIZE_WEEKLY")), (LZ_MONTHLY, _("LOT_SIZE_MONTHLY")), (LZ_MAX_STOCK_LEVEL, _("LOT_SIZE_MAX_STOCK_LEVEL")), ) AI_A = 'A' AI_B = 'B' AI_C = 'C' AI_D = 'D' AI_E = 'E' AI_INDICATORS = ( (AI_A, "A"), (AI_B, "B"), (AI_C, "C"), (AI_D, "D"), (AI_E, "E"), ) PLANT_AVAILABLE = 'AV' PLANT_BLOCKED = 'BL' PLANT_STATUSES = ((PLANT_AVAILABLE, _("PLANT_STATUS_AVAILABLE")), (PLANT_BLOCKED, _("PLANT_STATUS_BLOCKED"))) code = doc.FieldTypedCode(codes.StockCode, none=False) # General Info uom = doc.FieldUom(none=False) description = doc.FieldString(max_length=500, none=True) gross_weight = doc.FieldNumeric(none=True) net_weight = doc.FieldNumeric(none=True) plant_status = doc.FieldString(choices=PLANT_STATUSES, default=PLANT_AVAILABLE) # MRP scrap_percentage = doc.FieldNumeric(none=False, default=0) # type: float scale = doc.FieldNumeric(none=False, default=1) # type: float mrp_type = doc.FieldString(choices=MRP_TYPES, default=MRP, max_length=1) location = doc.FieldString(default=Location.locations['STORE'].code) reorder_point = doc.FieldNumeric(none=False, default=0) planning_time_fence = doc.FieldNumeric(default=0) procurement_type = doc.FieldString( choices=PROCUREMENT_TYPES, default=EXTERNAL, none=False) # Default logic is based on stock code gr_processing_time = doc.FieldNumeric(default=0, validators=[(lambda v: v < 0, 'Positive value only')]) planned_delivery_time = doc.FieldNumeric(default=2) lot_size = doc.FieldString(choices=LOT_SIZES, default=LZ_LOT_FOR_LOT) lot_size_arg = doc.FieldNumeric(none=True) # depends on lot_size lot_size_min = doc.FieldNumeric(default=0) lot_size_max = doc.FieldNumeric(default=0) safety_stock = doc.FieldNumeric( default=0, none=True) # if value is None then calculate it instead. deprecated_date = doc.FieldDateTime(none=True, default=None) deprecated_replacement = doc.FieldDoc('material_master', none=True, default=None) # Purchasing ... # Inventory enable_cycle_counting = doc.FieldBoolean(none=False, default=True) abc_indicator = doc.FieldString(choices=AI_INDICATORS, default=AI_E, max_length=1) # MRP calculation, running priorities hierarchy_affinity = doc.FieldNumeric(none=True) # deactivated schematic = doc.FieldDoc('material_schematic', none=True) # Active schematic def get_safety_stock(self): if self.safety_stock is None: # FIXME: Calculate safety_stock based on consumption rate pass return self.safety_stock def update_schematic(self, user, schematic_object, message="update schematic"): if not isinstance(schematic_object, Schematic): raise ProhibitedError( "update_schematic only accept schematic_object") schematic_object.material_id = self self.schematic = schematic_object self.hierarchy_affinity = None # required new setup self.touched(user, message=message) def revisions(self): """ :return: all possible revisions of this material object """ return Schematic.revisions(self.object_id) def revision(self, revision_id): """ Return specific revision of given this material. :param revision_id: :return: """ if revision_id is None: return self.schematic return Schematic.of(self.object_id, revision_id, throw=False) def validate_pair(self, revision_id, size): return Schematic.pair_exists(self.object_id, revision_id, size) def has_schematic(self): if self.schematic is not None: self.populate('schematic') return len(self.schematic.schematic) > 0 return False @classmethod def get(cls, code): """ Lookup material master by code. And raise error if such code is not found. :param basestring|codes.StockCode code: :return: MaterialMaster """ mm = MaterialMaster.of('code', str(code)) if mm is None: raise BadParameterError( _('ERROR_UNKNOWN_MATERIAL %(material_code)s') % {'material_code': code}) return mm @classmethod def factory(cls, code, uom=None, procurement_type=EXTERNAL, author=None, scrap_percentage=0): """ Lookup by Code first, - if not exists, - if UoM is not supplied - raise Exception - create a MaterialMaster - if exists, - return a fetched MaterialMaster :param codes.StockCode code: :param basestring|UOM uom: :param basestring procurement_type: :param IntraUser author: :param int scrap_percentage: :return MaterialMaster: """ ProhibitedError.raise_if(not isinstance(code, codes.StockCode), "provided code must be StockCode instance") materials = cls.manager.find(1, 0, cond={'code': str(code)}) # if exists if len(materials) > 0: return materials[0] if uom is None or author is None: raise ProhibitedError( "UOM and author must be supplied in case of creation") if not UOM.has(uom): raise BadParameterError("UOM \"%s\" is invalid" % uom) # Initialize Scale according to code if re.compile('^stock-[A-Z]{3}02[123]').match(str(code)) is not None: scale = 10 else: scale = 1 o = cls() o.code = code o.uom = uom o.procurement_type = procurement_type o.scrap_percentage = scrap_percentage o.scale = scale o.touched(author) return o class Meta: collection_name = 'material_master' require_permission = True
class Schematic(doc.Authored): material_id = doc.FieldDoc( 'material_master') # link to Material (for redundancy check), rev_id = doc.FieldNumeric( default=1 ) # Support Revision (based on Design rev_id)in case schematic is detached conf_size = doc.FieldList(doc.FieldString()) # Support Configuration source = doc.FieldAnyDoc( none=True) # source of schematic, link directly to design object schematic = doc.FieldList(doc.FieldNested( SchematicEntry)) # schematic - saved from Design's Process Entry. def expand(self, is_production=False): """ :param is_production: :return: void """ if self.schematic and len(self.schematic) > 0: context = map(lambda s: s.to_dummy(), self.schematic) new_context = task.Task.expand(context, is_production=is_production, verbose=False) self.schematic = map(lambda a: SchematicEntry.from_dummy(a), new_context) @classmethod def pair_exists(cls, material_id, rev_id, conf_size): """ validate if material_id + rev_id + conf_size exist. :param material_id: :param rev_id: :param conf_size: :return: """ return 0 < Schematic.manager.count( cond={ 'material_id': doc._objectid(material_id), 'rev_id': rev_id, 'conf_size': conf_size }) @staticmethod def factory(material_id, rev_id, author, **kwargs): verbose = kwargs.pop('verbose', False) sch = Schematic.manager.find(1, 0, cond={ 'material_id': material_id, 'rev_id': rev_id }) if len(sch) > 0: if verbose: print( "Schematic.factory() -> Schematic already exists %s, %s" % (material_id, rev_id)) return sch[0] # Create new one sch = Schematic() sch.material_id = material_id sch.rev_id = rev_id sch.conf_size = kwargs.pop('conf_size', []) sch.touched(author) if verbose: print("Schematic.factory() -> Creating new schematic %s, %s" % (material_id, rev_id)) return sch @classmethod def of(cls, material_id, revision_id, throw=False): """ :param basestring|ObjectId material_id: :param int revision_id: :param bool throw: :return Schematic: """ o = Schematic.manager.find(cond={ 'material_id': doc._objectid(material_id), 'rev_id': revision_id }) if len(o) == 0: if throw: raise ValueError('Unknown material+revision=%s+%s' % (material_id, revision_id)) else: return None return o[0] @classmethod def revisions(cls, material_id): """ :param basestring|ObjectId material_id: :return [Schematic]: """ return Schematic.manager.find( cond={'material_id': doc._objectid(material_id)}) def __repr__(self): txt = "Material: %s rev%s (conf=%s) + schematic ...\n" % ( self.material_id, self.rev_id, self.conf_size) if self.schematic and len(self.schematic) > 0: for sch in self.schematic: txt += "\t%s\n" % sch return txt class Meta: collection_name = 'material_schematic' references = [('material_master', 'material_id')]
class SchematicEntry(doc.FieldSpecAware): """ A Schematic's Line """ id = doc.FieldString() process = doc.FieldTask() materials = doc.FieldList(doc.FieldNested(SchematicMaterial)) source = doc.FieldList(doc.FieldString()) is_configurable = doc.FieldBoolean(default=False) duration = doc.FieldList(doc.FieldNumeric(none=False, default=0.0)) staging_duration = doc.FieldList(doc.FieldNumeric(none=False, default=0.0)) labor_cost = doc.FieldNumeric(none=False, default=0.0) markup = doc.FieldNumeric(none=False, default=0.0) remark = doc.FieldString(default="") def __repr__(self): return "%s: %s w/%s materials conf=%s from=%s" % \ (self.id, self.process, len(self.materials), self.is_configurable, self.source) def add_material(self, code, quantities, is_configurable, counter, cost, add=True): m = SchematicMaterial() m.code = code m.quantity = quantities m.is_configurable = is_configurable m.counter = counter m.cost = cost if add: self.materials.append(m) return m def to_dummy(self): return task.SchematicDummy(self.id, self.process.code, self.source, ref=self) def try_propagate(self, target_material, replacement_array): """ Replace target_material (take/borrow only) with replacement_array (array of sized materials) :param target_material: :param replacement_array: :return: """ # Sanity Check if not isinstance(target_material, codes.StockCode): raise BadParameterError.std(require_type=codes.StockCode) if not self.materials: return # all these materials can be ... # - Borrow case # - Take case <-- TODO: Should filter this case out. # - Make case append_list = [ ] # to keep the loop safe, we will extend this self.materials once we complete the removal process_list = list(reversed(list(enumerate(self.materials)))) print 'Process_list', process_list print '\tlooking for', target_material print '\tReplacement_array', replacement_array for sch_mat_index, sch_mat in process_list: if sch_mat.code == target_material: # Sanity check if sch_mat.is_configurable and len( sch_mat.quantity) != len(replacement_array): raise BadParameterError( _("ERR_CANNOT_PROPAGATE_MATERIAL_SIZES: %(material)s") % {'material': str(sch_mat.code)}) # update append_list for size_index, replace_mat in enumerate(replacement_array): quantities = [0] * len(replacement_array) quantities[size_index] = sch_mat.quantity[ size_index] if sch_mat.is_configurable else sch_mat.quantity[ 0] append_list.append( self.add_material(code=replace_mat, quantities=quantities, is_configurable=True, counter=sch_mat.counter, cost=sch_mat.cost, add=False)) # delete original one del self.materials[sch_mat_index] print '\toutput append_list', append_list print '\tbefore', self.materials self.materials.extend(append_list) print '\tafter\n', self.materials # If appended, this task must be converted to configurable if len(append_list) > 0 and not self.is_configurable: self.is_configurable = True self.duration = map(lambda _: self.duration[0], range(0, len(replacement_array))) self.staging_duration = map(lambda _: self.staging_duration[0], range(0, len(replacement_array))) def normalize(self, for_size_index): o = self.__class__() # simple stuff o.id = self.id o.process = self.process o.source = self.source[:] o.is_configurable = False o.labor_cost = self.labor_cost o.markup = self.markup o.remark = self.remark # nested stuff o.materials = map(lambda a: a.normalized(for_size_index), self.materials) # size sensitive stuff if not self.is_configurable: o.duration = self.duration[:] o.staging_duration = self.staging_duration[:] else: if not (0 <= for_size_index < len(self.duration)): raise ValidationError( _("ERR_NORMALIZE_SCHEMATIC_ENTRY_FAILED_DURATION_SIZE_INDEX_OUT_OF_RANGE" )) if not (0 <= for_size_index < len(self.staging_duration)): raise ValidationError( _("ERR_NORMALIZE_SCHEMATIC_ENTRY_FAILED_STAGING_DURATION_SIZE_INDEX_OUT_OF_RANGE" )) o.duration = [self.duration[for_size_index]] o.staging_duration = [self.staging_duration[for_size_index]] return o @classmethod def from_dummy(cls, dummy): if isinstance(dummy.ref, SchematicEntry): entry = dummy.ref else: entry = SchematicEntry() entry.labor_cost = 0 entry.markup = 0 entry.remark = "Expanded" entry.duration = [task.Task.factory(dummy.task_code).standard_time] entry.staging_duration = [ task.Task.factory(dummy.task_code).staging_duration ] entry.is_configurable = False entry.id = dummy.id entry.process = dummy.task_code entry.source = dummy.source return entry