class AuxiliaryTaskConfirmation(Confirmation): completed = doc.FieldBoolean(default=True, transient=True) materials = doc.FieldList(doc.FieldNested(AuxiliaryTaskComponent)) @classmethod def create(cls, materials, **kwargs): o = super(AuxiliaryTaskConfirmation, cls).create(**kwargs) o.completed = kwargs.pop('completed', True) # FIXME: Translate given materials to TaskComponent def define_material(m): if 'weight' not in m: m['weight'] = None if 'uom' not in m: m['uom'] = None component = AuxiliaryTaskComponent.factory(m['material'], m['revision'], m['size'], m['quantity'], uom=m['uom'], weight=m['weight']) if len(o.materials) > 0: o.materials.append(component) else: o.materials = [component] map(define_material, materials) return o
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 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 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 PurchaseRequisition(PurchaseBase): PR_TYPE_MRP = 0 PR_TYPE_MANUAL = 1 PR_TYPES = ( (PR_TYPE_MRP, _('PR_TYPE_MRP')), (PR_TYPE_MANUAL, _('PR_TYPE_MANUAL')), ) STATUS_OPEN = 0 STATUS_APPROVED = 1 STATUS_PARTIAL_CONVERTED = 2 STATUS_CONVERTED = 3 STATUS_CANCELLED = 4 DOC_STATUSES = ( (STATUS_OPEN, _('PR_STATUS_OPEN')), (STATUS_APPROVED, _('PR_STATUS_APPROVED')), (STATUS_PARTIAL_CONVERTED, _('PR_STATUS_PARTIAL_CONVERTED')), (STATUS_CONVERTED, _('PR_STATUS_CONVERTED')), (STATUS_CANCELLED, _('PR_STATUS_CANCELLED')), ) status = doc.FieldNumeric(choices=DOC_STATUSES, default=STATUS_OPEN) type = doc.FieldNumeric(choices=PR_TYPES, default=PR_TYPE_MANUAL) items = doc.FieldList(doc.FieldNested(PurchaseRequisitionItem)) def validate(self): for item in self.items: if not item.open_quantity: item.open_quantity = item.quantity elif item.open_quantity > item.quantity: raise ValidationError(_("ERROR_OPEN_QTY_MORE_THAN_QTY")) @classmethod def factory(cls, material, revision, size, due_date, quantity, user, pr_type=0, mrp_session_id=None, **kwargs): req = cls() req.mrp_session = mrp_session_id req.type = pr_type item = PurchaseRequisitionItem() item.material = material item.revision = revision item.size = size item.delivery_date = due_date item.quantity = quantity req.items = [item] req.touched(user, **kwargs) return req def save(self): if not self.doc_no: self.doc_no = doc.RunningNumberCenter.new_number(PR_NUMBER_KEY) super(PurchaseRequisition, self).save() class Meta: collection_name = 'purchase_requisition' require_permission = True doc_no_prefix = PR_NUMBER_PREFIX
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 AuxiliaryTask(TaskDoc): STATUS_OPEN = 0 STATUS_CONFIRMED = 1 STATUSES = ((STATUS_OPEN, _("STATUS_OPEN")), (STATUS_CONFIRMED, _("STATUS_CONFIRMED"))) status = doc.FieldNumeric(default=STATUS_OPEN, choices=STATUSES) parent_task = doc.FieldDoc('task', none=False) """:type TaskDoc""" materials = doc.FieldList(doc.FieldNested(AuxiliaryTaskComponent)) confirmations = doc.FieldList(doc.FieldNested(AuxiliaryTaskConfirmation)) def invoke_set_status(self, user, _status, **kwargs): # Update status per form validation status = int(_status) if status in [self.STATUS_CONFIRMED]: self.confirm(user, **kwargs) return self def get_parent_task(self): """ :return TaskDoc: parent task """ self.populate('parent_task') return self.parent_task def list_components(self): """ :return [AuxiliaryTaskComponent]: """ valid_confirmations = filter(lambda c: not c.cancelled, self.confirmations) if len(valid_confirmations) > 0: return list( chain.from_iterable(v.materials for v in valid_confirmations)) return self.materials @classmethod def factory(cls, parent_task, components=None): target_task, duration, assignee = cls.default_parameters(parent_task) if target_task is None: return None if components: if not isinstance(components, list) or not reduce( lambda x, y: isinstance(x, dict) and isinstance(y, dict), components): raise BadParameterError("Component should be list of dict") components = map( lambda a: AuxiliaryTaskComponent.factory( material=a['material'], revision=a['revision'], size=a['size'], quantity=a['quantity'], uom=a['uom'], weight=a['weight']), components) else: components = filter(lambda a: a.quantity > 0, parent_task.materials[:]) components = map(AuxiliaryTaskComponent.transmute, components) t = cls() t.status = cls.STATUS_OPEN t.task = target_task t.parent_task = parent_task t.planned_duration = duration t.assignee = assignee t.materials = components if len(t.materials) <= 0: return None return t @classmethod def default_parameters(cls, parent_task): """ Retrieve auxiliary task_code to be created for specific parent_task document :param parent_task: :return task.Task: None if such parent_task does not required any aux_task """ return None, parent_task.staging_duration, IntraUser.robot() def find_material(self, material): return next( (sm for sm in self.materials if sm.material == material.material and sm.size == material.size and sm.revision == material.revision), None) @classmethod def confirmation_class(cls): return AuxiliaryTaskConfirmation def confirm(self, user, confirmation, **kwargs): """ :param IntraUser user: :param AuxiliaryTaskConfirmation confirmation: :param dict kwargs: materials :return: """ if self.status >= self.STATUS_CONFIRMED: raise ValidationError( _("ERROR_TASK_STATUS_CANT_CONFIRM: %(status)s") % {'status': self.status}) if not confirmation: raise ValidationError(_("ERROR_CONFIRMATION_IS_REQUIRED")) if not isinstance(confirmation, AuxiliaryTaskConfirmation): raise ValidationError( _("ERROR_CONFIRMATION_MUST_BE_AUX_TASK_CONFIRMATION")) if not confirmation.materials: raise ValidationError(_("ERROR_MATERIAL_IS_REQUIRED")) if not self.confirmations: self.actual_start = confirmation.actual_start self.actual_duration += confirmation.actual_duration # if confirmation is marked completed, set status to confirmed if confirmation.completed: self.actual_end = confirmation.actual_end self.status = self.STATUS_CONFIRMED super(AuxiliaryTask, self).confirm(user, confirmation, **kwargs)
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 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 QualityItem(doc.FieldSpecAware): # inspection quantity quantity = doc.FieldNumeric(default=0) weight = doc.FieldNumeric(default=0) defects = doc.FieldList(doc.FieldNested(DefectItem))
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
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