Example #1
0
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
Example #2
0
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
Example #3
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)
Example #4
0
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
Example #5
0
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
Example #6
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
Example #7
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)
Example #8
0
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
Example #9
0
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)
Example #10
0
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
Example #11
0
class QualityItem(doc.FieldSpecAware):
    # inspection quantity
    quantity = doc.FieldNumeric(default=0)
    weight = doc.FieldNumeric(default=0)
    defects = doc.FieldList(doc.FieldNested(DefectItem))
Example #12
0
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')]
Example #13
0
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
Example #14
0
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