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 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 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 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 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 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 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