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 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 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 PurchaseOrderItem(PurchaseBaseItem): ref_doc = doc.FieldDoc(PurchaseRequisition) ref_doc_item = doc.FieldNumeric()
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 MaterialMaster(doc.Authored): MRP = 'M' NO_MRP = 'N' REORDER = 'R' MRP_TYPES = ( (MRP, _("MRP_TYPE_MRP")), (NO_MRP, _("MRP_TYPE_NO_MRP")), (REORDER, _("MRP_TYPE_REORDER")), ) EXTERNAL = 'E' INTERNAL = 'I' PROCUREMENT_TYPES = ((EXTERNAL, _("PROCUREMENT_TYPE_EXTERNAL")), (INTERNAL, _("PROCUREMENT_TYPE_INTERNAL"))) # Lot Size Policy LZ_LOT_FOR_LOT = 'EX' # One procurement for One demand LZ_DAILY = 'TB' # Accumulate daily demand to one procurement amount LZ_WEEKLY = 'WS' # Accumulate weekly demand to one procurement amount for the whole week based on offset (:lot_size_arg value 0 <= 6) LZ_MONTHLY = 'MS' # Accumulate monthly demand to one procurement amount for the whole month based on offset (:lot_size_arg value 0 <= 28) LZ_MAX_STOCK_LEVEL = 'HB' # maximize amount to the :lot_size_arg, at the multiplication of :lot_size_max, :lot_size_min LOT_SIZES = ( (LZ_LOT_FOR_LOT, _("LOT_SIZE_LOT_FOR_LOT")), (LZ_DAILY, _("LOT_SIZE_DAILY")), (LZ_WEEKLY, _("LOT_SIZE_WEEKLY")), (LZ_MONTHLY, _("LOT_SIZE_MONTHLY")), (LZ_MAX_STOCK_LEVEL, _("LOT_SIZE_MAX_STOCK_LEVEL")), ) AI_A = 'A' AI_B = 'B' AI_C = 'C' AI_D = 'D' AI_E = 'E' AI_INDICATORS = ( (AI_A, "A"), (AI_B, "B"), (AI_C, "C"), (AI_D, "D"), (AI_E, "E"), ) PLANT_AVAILABLE = 'AV' PLANT_BLOCKED = 'BL' PLANT_STATUSES = ((PLANT_AVAILABLE, _("PLANT_STATUS_AVAILABLE")), (PLANT_BLOCKED, _("PLANT_STATUS_BLOCKED"))) code = doc.FieldTypedCode(codes.StockCode, none=False) # General Info uom = doc.FieldUom(none=False) description = doc.FieldString(max_length=500, none=True) gross_weight = doc.FieldNumeric(none=True) net_weight = doc.FieldNumeric(none=True) plant_status = doc.FieldString(choices=PLANT_STATUSES, default=PLANT_AVAILABLE) # MRP scrap_percentage = doc.FieldNumeric(none=False, default=0) # type: float scale = doc.FieldNumeric(none=False, default=1) # type: float mrp_type = doc.FieldString(choices=MRP_TYPES, default=MRP, max_length=1) location = doc.FieldString(default=Location.locations['STORE'].code) reorder_point = doc.FieldNumeric(none=False, default=0) planning_time_fence = doc.FieldNumeric(default=0) procurement_type = doc.FieldString( choices=PROCUREMENT_TYPES, default=EXTERNAL, none=False) # Default logic is based on stock code gr_processing_time = doc.FieldNumeric(default=0, validators=[(lambda v: v < 0, 'Positive value only')]) planned_delivery_time = doc.FieldNumeric(default=2) lot_size = doc.FieldString(choices=LOT_SIZES, default=LZ_LOT_FOR_LOT) lot_size_arg = doc.FieldNumeric(none=True) # depends on lot_size lot_size_min = doc.FieldNumeric(default=0) lot_size_max = doc.FieldNumeric(default=0) safety_stock = doc.FieldNumeric( default=0, none=True) # if value is None then calculate it instead. deprecated_date = doc.FieldDateTime(none=True, default=None) deprecated_replacement = doc.FieldDoc('material_master', none=True, default=None) # Purchasing ... # Inventory enable_cycle_counting = doc.FieldBoolean(none=False, default=True) abc_indicator = doc.FieldString(choices=AI_INDICATORS, default=AI_E, max_length=1) # MRP calculation, running priorities hierarchy_affinity = doc.FieldNumeric(none=True) # deactivated schematic = doc.FieldDoc('material_schematic', none=True) # Active schematic def get_safety_stock(self): if self.safety_stock is None: # FIXME: Calculate safety_stock based on consumption rate pass return self.safety_stock def update_schematic(self, user, schematic_object, message="update schematic"): if not isinstance(schematic_object, Schematic): raise ProhibitedError( "update_schematic only accept schematic_object") schematic_object.material_id = self self.schematic = schematic_object self.hierarchy_affinity = None # required new setup self.touched(user, message=message) def revisions(self): """ :return: all possible revisions of this material object """ return Schematic.revisions(self.object_id) def revision(self, revision_id): """ Return specific revision of given this material. :param revision_id: :return: """ if revision_id is None: return self.schematic return Schematic.of(self.object_id, revision_id, throw=False) def validate_pair(self, revision_id, size): return Schematic.pair_exists(self.object_id, revision_id, size) def has_schematic(self): if self.schematic is not None: self.populate('schematic') return len(self.schematic.schematic) > 0 return False @classmethod def get(cls, code): """ Lookup material master by code. And raise error if such code is not found. :param basestring|codes.StockCode code: :return: MaterialMaster """ mm = MaterialMaster.of('code', str(code)) if mm is None: raise BadParameterError( _('ERROR_UNKNOWN_MATERIAL %(material_code)s') % {'material_code': code}) return mm @classmethod def factory(cls, code, uom=None, procurement_type=EXTERNAL, author=None, scrap_percentage=0): """ Lookup by Code first, - if not exists, - if UoM is not supplied - raise Exception - create a MaterialMaster - if exists, - return a fetched MaterialMaster :param codes.StockCode code: :param basestring|UOM uom: :param basestring procurement_type: :param IntraUser author: :param int scrap_percentage: :return MaterialMaster: """ ProhibitedError.raise_if(not isinstance(code, codes.StockCode), "provided code must be StockCode instance") materials = cls.manager.find(1, 0, cond={'code': str(code)}) # if exists if len(materials) > 0: return materials[0] if uom is None or author is None: raise ProhibitedError( "UOM and author must be supplied in case of creation") if not UOM.has(uom): raise BadParameterError("UOM \"%s\" is invalid" % uom) # Initialize Scale according to code if re.compile('^stock-[A-Z]{3}02[123]').match(str(code)) is not None: scale = 10 else: scale = 1 o = cls() o.code = code o.uom = uom o.procurement_type = procurement_type o.scrap_percentage = scrap_percentage o.scale = scale o.touched(author) return o class Meta: collection_name = 'material_master' require_permission = True
class Schematic(doc.Authored): material_id = doc.FieldDoc( 'material_master') # link to Material (for redundancy check), rev_id = doc.FieldNumeric( default=1 ) # Support Revision (based on Design rev_id)in case schematic is detached conf_size = doc.FieldList(doc.FieldString()) # Support Configuration source = doc.FieldAnyDoc( none=True) # source of schematic, link directly to design object schematic = doc.FieldList(doc.FieldNested( SchematicEntry)) # schematic - saved from Design's Process Entry. def expand(self, is_production=False): """ :param is_production: :return: void """ if self.schematic and len(self.schematic) > 0: context = map(lambda s: s.to_dummy(), self.schematic) new_context = task.Task.expand(context, is_production=is_production, verbose=False) self.schematic = map(lambda a: SchematicEntry.from_dummy(a), new_context) @classmethod def pair_exists(cls, material_id, rev_id, conf_size): """ validate if material_id + rev_id + conf_size exist. :param material_id: :param rev_id: :param conf_size: :return: """ return 0 < Schematic.manager.count( cond={ 'material_id': doc._objectid(material_id), 'rev_id': rev_id, 'conf_size': conf_size }) @staticmethod def factory(material_id, rev_id, author, **kwargs): verbose = kwargs.pop('verbose', False) sch = Schematic.manager.find(1, 0, cond={ 'material_id': material_id, 'rev_id': rev_id }) if len(sch) > 0: if verbose: print( "Schematic.factory() -> Schematic already exists %s, %s" % (material_id, rev_id)) return sch[0] # Create new one sch = Schematic() sch.material_id = material_id sch.rev_id = rev_id sch.conf_size = kwargs.pop('conf_size', []) sch.touched(author) if verbose: print("Schematic.factory() -> Creating new schematic %s, %s" % (material_id, rev_id)) return sch @classmethod def of(cls, material_id, revision_id, throw=False): """ :param basestring|ObjectId material_id: :param int revision_id: :param bool throw: :return Schematic: """ o = Schematic.manager.find(cond={ 'material_id': doc._objectid(material_id), 'rev_id': revision_id }) if len(o) == 0: if throw: raise ValueError('Unknown material+revision=%s+%s' % (material_id, revision_id)) else: return None return o[0] @classmethod def revisions(cls, material_id): """ :param basestring|ObjectId material_id: :return [Schematic]: """ return Schematic.manager.find( cond={'material_id': doc._objectid(material_id)}) def __repr__(self): txt = "Material: %s rev%s (conf=%s) + schematic ...\n" % ( self.material_id, self.rev_id, self.conf_size) if self.schematic and len(self.schematic) > 0: for sch in self.schematic: txt += "\t%s\n" % sch return txt class Meta: collection_name = 'material_schematic' references = [('material_master', 'material_id')]
class 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 TestListFieldDoc(docs.Doc): listing = docs.FieldList(docs.FieldDoc(TestBaseDoc)) class Meta: collection_name = 'test-listing'