Beispiel #1
0
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
Beispiel #3
0
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
Beispiel #4
0
class PurchaseOrderItem(PurchaseBaseItem):
    ref_doc = doc.FieldDoc(PurchaseRequisition)
    ref_doc_item = doc.FieldNumeric()
Beispiel #5
0
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
Beispiel #6
0
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'