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 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 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 Confirmation(doc.FieldSpecAware): # assign from frontend actual_start = doc.FieldDateTime(none=True) # datetime actual_duration = doc.FieldNumeric(none=True) # seconds actual_end = doc.FieldDateTime(none=True) # datetime assignee = doc.FieldAssignee() # user_code, object_id, group_code # automatically injected created_by = doc.FieldIntraUser() created_on = doc.FieldDateTime() cancelled = doc.FieldDoc('event', none=True) def cancel(self, cancelled_by, **kwargs): self.cancelled = doc.Event.create(doc.Event.CANCELLED, cancelled_by, **kwargs) def is_cancelled(self): return self.cancelled is not None @classmethod def create(cls, actual_start, actual_end, actual_duration, assignee, **kwargs): """ A convenient method to convert **details from AJAX to python confirm object. :param actual_start: :param actual_end: :param actual_duration: :param assignee: :return: """ o = cls() o.actual_start = actual_start o.actual_end = actual_end o.actual_duration = actual_duration o.assignee = assignee o.created_by = kwargs.pop('created_by', assignee) o.created_on = kwargs.pop('created_on', utils.NOW()) return o
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 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 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 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 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 OffHoursRange(docs.Doc): start = docs.FieldDateTime() end = docs.FieldDateTime() @classmethod def sync(cls, are_touching=None): """ :param are_touching: :return: """ if are_touching is None: are_touching = lambda x, y: y - x == 1 offhours = OffHours.manager.find(cond={ '$sort': 'start_time' }) output = [] new_start = None new_end = None length = len(offhours) for i, offhour in enumerate(offhours): if new_start is None: new_start = offhour.start_time new_end = offhour.end_time elif new_end >= offhour.start_time or are_touching(new_end, offhour.start_time): new_end = max(offhour.end_time, new_end) else: output.append([new_start, new_end]) new_start = offhour.start_time new_end = offhour.end_time # Last item if i + 1 == length: output.append([new_start, new_end]) bulk = cls.manager.o.initialize_ordered_bulk_op() bulk.find({}).remove() # delete everything for entry in output: bulk.insert({'start': entry[0], 'end': entry[1]}) print bulk.execute() @classmethod def between(cls, start_time=None, end_time=None): """ Query any items that overlaps between start_time, and end_time if both start_time: and end_time: is omitted, start_time=now() will be used. :param start_time: optional :param end_time: optional :return: """ # Sanitize input if start_time is None and end_time is None: start_time = datetime.now() # Build condition cond = { "end": {"$gt": start_time}, "$sort": "start" } if end_time is not None: cond["start"] = {"$lt": end_time} return cls.manager.find(cond=cond) class Meta: collection_name = "offhours-range" indices = [ ([("start_time", 1)], {"background": True, "sparse": False}), # improve search performance ([("end_time", -1)], {"background": True, "sparse": False}) # improve search performance ]