Пример #1
0
class PurchaseBaseItem(doc.FieldSpecAware):
    material = doc.FieldTypedCode(codes.StockCode, none=False)
    uom = doc.FieldUom(none=False, default='pc')
    quantity = doc.FieldNumeric(default=1)
    revision = doc.FieldNumeric(none=True)
    size = doc.FieldString(none=True)
    location = doc.FieldString(default=Location.locations['STORE'].code,
                               none=False)
    delivery_date = doc.FieldDateTime()
    net_price = doc.FieldNumeric()
Пример #2
0
class SalesOrderEntry(doc.Doc):
    material = doc.FieldTypedCode(codes.StockCode, none=False)
    revision = doc.FieldNumeric(none=True)
    quantity = doc.FieldNumeric(default=1, none=False)
    uom = doc.FieldUom(none=False)
    location = doc.FieldString(default=Location.locations['STORE'].code,
                               none=False)
    size = doc.FieldString(none=True)
    # TODO: weight?
    net_price = doc.FieldNumeric(none=False, default=0)
    remark = doc.FieldString(none=True)
Пример #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)
Пример #4
0
class TaskComponent(doc.FieldSpecAware, decorators.JsonSerializable):
    # assign from front end
    material = doc.FieldTypedCode(codes.StockCode)  # TypedCode
    quantity = doc.FieldNumeric(default=1, none=False)
    uom = doc.FieldUom(none=False)
    revision = doc.FieldNumeric(none=True)
    size = doc.FieldString(none=True)

    @classmethod
    def factory(cls, material, revision, size, quantity, uom=None, **kwargs):
        c = cls()
        c.material = material
        c.revision = revision
        c.size = size
        c.quantity = quantity
        c.uom = uom if uom is not None else MaterialMaster.get(material).uom
        return c

    def as_json(self):
        return {
            'material': str(self.material),
            'quantity': self.quantity,
            'uom': self.uom,
            'revision': self.revision,
            'size': self.size
        }

    def material_key(self):
        return "%sr%s-%s" % (self.material, self.revision, self.size)

    def __repr__(self):
        return "TaskComponent material_key=[%s]" % self.material_key()
Пример #5
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
Пример #6
0
class TestExtendDoc(docs.Validatable, TestBaseDoc):
    """
    Also use multiple inheritance
    """
    field_a = docs.FieldNumeric(default=2)
    field_b = docs.FieldString(default="default_value2")

    class Meta:
        collection_name = ':extend'
Пример #7
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
Пример #8
0
class Authentication(doc.Doc):
    """
    Authentication token

    Parameter meanings

        User (requester) requested action (action) override authentication by (authorizer) on date (doc)

        Token = object_id

    """
    requester = doc.FieldIntraUser(none=False)
    authorizer = doc.FieldIntraUser(none=False)
    target_permissions = doc.FieldList(doc.FieldString(), none=False)
    created = doc.FieldDateTime(none=False)

    @classmethod
    def create(cls, requester, authenticated_by, authentication_challenge,
               target_permissions):
        """

        :param IntraUser requester:
        :param IntraUser authenticated_by:
        :param basestring authentication_challenge:
        :param basestring|list target_permissions:
        :raise ValidationError:
        :return Authentication:
        """
        # sanitize input
        if isinstance(target_permissions, basestring):
            target_permissions = [target_permissions]

        # Validate if target user has enough permission allow such task to happen?
        if any(not authenticated_by.can(p) for p in target_permissions):
            raise ValidationError(
                _("ERR_AUTHENTICATE_USER_DOES_NOT_HAVE_TARGET_PERMISSION: %(target_permissions)s"
                  ) % {'target_permissions': ",".join(target_permissions)})

        # Validate password challenge
        if not authenticated_by.check_password(authentication_challenge):
            raise ValidationError(_("ERR_AUTHENTICATION_FAILED"))

        # Validated, success create the token
        o = cls()
        o.target_permissions = target_permissions
        o.requester = requester
        o.authorizer = authenticated_by
        o.created = NOW()
        o.save()
        return o

    class Meta:
        collection_name = 'authentication'
Пример #9
0
class OffHours(docs.Doc):
    start_time = docs.FieldDateTime()
    end_time = docs.FieldDateTime()
    uid = docs.FieldString()

    def __init__(self, object_id=None):
        super(OffHours, self).__init__(object_id)

    def __lt__(self, other):
        return self.start_time < other.start_time

    def __gt__(self, other):
        return self.start_time > other.start_time

    def __repr__(self):
        return "%s [%s - %s]" % (self.uid, self.start_time, self.end_time)

    @staticmethod
    def sync(ics_url=None, verbose=False):
        ics_url = ics_url if ics_url is not None else OFFHOURS_ICS_URL
        if ics_url is None:
            raise ProhibitedError("unable to sync() ics_url must not be null")
        if verbose:
            print("Performing sync with URL=%s" % ics_url)
        # download from ics_url
        p = parser.CalendarParser(ics_url=ics_url)
        bulk = OffHours.manager.o.initialize_ordered_bulk_op()
        for event in p.parse_calendar():
            uid = "%s/%s" % (event.recurrence_id, event.uid)
            bulk.find({'uid': uid}).upsert().update({'$set': {
                'uid': uid,
                'start_time': event.start_time,
                'end_time': event.end_time
            }})
        r = bulk.execute()
        if verbose:
            print("Sync result:")
            for o in r.iteritems():
                print("\t%s = %s" % o)
        OffHoursRange.sync()
        return r

    class Meta:
        collection_name = "offhours"
        indices = [
            ([("uid", 1)], {"unique": True, "sparse": False}),
            ([("start_time", 1)], {"background": True, "sparse": False})    # improve search performance
        ]
Пример #10
0
class MRPSessionExecutionRecordEntry(doc.FieldSpecAware):
    marker = doc.FieldDateTime(none=False)
    quantity = doc.FieldNumeric()
    ref_docs = doc.FieldAnyDoc()
    original = doc.FieldBoolean(default=False)
    remark = doc.FieldString(none=True)

    @classmethod
    def create(cls, marker, quantity, ref_docs, original, remark=None):
        o = MRPSessionExecutionRecordEntry()
        o.marker = marker
        o.quantity = quantity
        o.ref_docs = ref_docs
        o.original = original
        o.remark = remark
        return o

    @staticmethod
    def initial_balance_marker():
        return datetime.fromtimestamp(0)

    @staticmethod
    def safety_stock_marker():
        return datetime.fromtimestamp(1)

    def __repr__(self):
        name = self.marker.strftime('%Y-%m-%d %H:%M')
        if self.marker == MRPSessionExecutionRecordEntry.initial_balance_marker(
        ):
            name = "Initial Balance"
        elif self.marker == MRPSessionExecutionRecordEntry.safety_stock_marker(
        ):
            name = "Safety Stock"
        return "[%16s] %s%4s" % (name, '-' if self.quantity < 0 else '+',
                                 abs(self.quantity))

    def __cmp__(self, other):
        if self.marker == other.marker:
            return cmp(self.quantity, other.quantity)
        return cmp(self.marker, other.marker)
Пример #11
0
class DefectItem(doc.FieldSpecAware):
    defect = doc.FieldString()
    quantity = doc.FieldNumeric()
Пример #12
0
class TestExtenderDoc(TestExtendDoc):
    field_b = docs.FieldString(default="default_value3")
    field_c = docs.FieldString(default="exclusive_value")

    class Meta:
        collection_name = ':extender'
Пример #13
0
class TestTupleFieldDoc(docs.Doc):
    tuple_items = docs.FieldTuple(docs.FieldNumeric(), docs.FieldNumeric(), docs.FieldString())

    class Meta:
        collection_name = 'test-tuple'
Пример #14
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
Пример #15
0
class TestBaseDoc(docs.Doc):
    field_a = docs.FieldString(default="default_value")

    class Meta:
        collection_name = 'test'
Пример #16
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
Пример #17
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
Пример #18
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)
Пример #19
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
Пример #20
0
class InventoryMovementEntry(doc.FieldSpecAware):
    material = doc.FieldTypedCode(codes.StockCode,
                                  none=False)  # can intercept string
    """:type: codes.StockCode"""
    quantity = doc.FieldNumeric(default=1, none=False)
    batch = doc.FieldString()  # Assigned from InventoryMovementInstance
    """:type: basestring"""
    value = doc.FieldNumeric()
    weight = doc.FieldNumeric(none=True)
    location = doc.FieldString(default=Location.factory('STORE').code,
                               none=False)
    """:type : Location|basestring"""
    ref_doc = doc.FieldAnyDoc(none=True)  # For production order only
    """:type : doc.Doc"""
    reason = doc.FieldString(lov=MOVEMENT_LOV_KEY, none=True)
    """:type: basestring"""
    @classmethod
    def factory(cls,
                material,
                quantity,
                location=None,
                ref_doc=None,
                reason=None,
                batch=None,
                value=None,
                weight=None):
        i = cls()
        i.material = material
        i.quantity = quantity
        i.ref_doc = ref_doc
        i.reason = reason
        i.batch = batch
        i.value = value
        i.weight = weight

        if location:
            i.location = location

        return i

    @classmethod
    def transfer_pair_factory(cls,
                              material,
                              quantity,
                              from_location,
                              to_location,
                              from_ref_doc=None,
                              to_ref_doc=None):
        """

        :param material:
        :param quantity:
        :param from_location:
        :param to_location:
        :param from_ref_doc:
        :param to_ref_doc:
        :return: cursor of InventoryMovementEntry
        """
        # Query inventory content, FIFO
        batch_candidate = InventoryContent.manager.project(
            cond={
                'material': str(material),
                'location': from_location,
                'quantity': {
                    '$gt': 0
                }
            },
            project=['_id', 'batch', 'quantity', 'value', 'weight'],
            sort=[("batch", 1)])

        leftover = quantity
        usage_tuples = []  # batch, quantity, value, weight
        for a in batch_candidate:
            batch_quantity = float(a['quantity'])
            used = min(batch_quantity, leftover)
            consumption_ratio = used / batch_quantity
            delta_value = float(a['value']) * consumption_ratio
            delta_weight = float(a['weight']) * consumption_ratio
            leftover -= used
            usage_tuples.append((a['batch'], used, delta_value, delta_weight))
            if leftover <= 0:
                break
        if leftover > 0:
            raise ValidationError(
                _('ERR_FAILED_TO_ALLOCATE_MATERIAL_TRANSFER_PAIR: %(material)s %(from_location)s %(to_location)s'
                  ) % {
                      'material': material,
                      'from_location': from_location,
                      'to_location': to_location
                  })

        def create():
            for p in usage_tuples:
                o = cls()
                o.material = material
                o.quantity = -p[1]
                o.batch = p[0]
                o.value = -p[2]
                o.weight = -p[3]
                o.ref_doc = from_ref_doc
                o.location = from_location
                yield o
                i = cls()
                i.material = material
                i.quantity = p[1]
                i.batch = p[0]
                i.value = p[2]
                i.weight = p[3]
                i.ref_doc = to_ref_doc
                i.location = to_location
                yield i

        return create()
Пример #21
0
class InventoryContent(doc.Doc):
    # Primary Key:
    material = doc.FieldTypedCode(codes.StockCode, none=False)
    """:type : codes.StockCode"""
    location = doc.FieldString(default=Location.locations['STORE'].code,
                               none=False)
    """:type : Location"""
    batch = doc.FieldString()
    ref_doc = doc.FieldAnyDoc(none=True)
    # Content Values
    quantity = doc.FieldNumeric(default=0)
    value = doc.FieldNumeric(default=0)
    weight = doc.FieldNumeric(default=None, none=True)

    def add(self, quantity, value, weight):
        new_quantity = self.quantity + quantity
        new_value = self.value + value
        if new_quantity < 0:
            raise ValidationError(
                _("ERROR_INSUFFICIENT_QUANTITY: %(content_signature)s %(content_quantity)s + %(additional_quantity)s < 0"
                  ) % {
                      'content_quantity': self.quantity,
                      'additional_quantity': quantity,
                      'content_signature': str(self)
                  })

        new_weight = None
        if weight is not None or self.weight is not None:
            new_weight = (self.weight or 0) + (weight or 0)
            if new_weight < 0:
                raise ValidationError(
                    "%s: %s + %s < 0" %
                    (_("ERROR_UNBALANCE_WEIGHT"), self.weight, weight))

        self.quantity = new_quantity
        self.value = new_value
        self.weight = new_weight

    def should_delete(self):
        return self.quantity == 0 and self.weight < 0.01 and self.value < 0.01

    @classmethod
    def get_value(cls, material, **kwargs):
        location = kwargs.pop('location', None)
        batch = kwargs.pop('batch', None)
        cond = {
            'material':
            material if isinstance(material, basestring) else str(material)
        }
        if batch is not None:
            cond['batch'] = batch
        if location is not None:
            cond['location'] = location
        r = cls.manager.o.aggregate([{
            '$match': cond
        }, {
            '$group': {
                '_id': None,
                'total': {
                    '$sum': '$quantity'
                }
            }
        }])
        return 0 if len(r['result']) == 0 else r['result'][0]['total']

    def __str__(self):
        return "INV_CNT %s %s %s (ref: %s)" % (self.material, self.location,
                                               self.batch, str(self.ref_doc))

    @staticmethod
    def apply(movement, dry_run=False):
        """
        Apply Inventory Movement to the Content

        :param InventoryMovement movement:
        :param bool dry_run:
        """
        for item in movement.items:
            content = InventoryContent.factory(item.material, item.location,
                                               item.batch, item.ref_doc)
            content.add(item.quantity, item.value, item.weight)
            if not dry_run:
                if content.should_delete():
                    content.delete()
                else:
                    content.save()

    @staticmethod
    def factory(material_code, location, batch, ref_doc):
        """

        :param codes.StockCode material_code:
        :param Location|basestring location:
        :param basestring batch:
        :param doc.Doc ref_doc:
        :return:
        """
        cond = {
            'material': unicode(material_code),
            'location': location,
            'batch': batch
        }
        if ref_doc:
            key1, key2 = doc.FieldAnyDoc.as_value_for_query(ref_doc)
            cond['ref_doc.0'] = key1
            cond['ref_doc.1'] = key2
        else:
            cond['ref_doc'] = None
        r = InventoryContent.manager.find(1, 0, cond)

        # if no record found.
        if len(r) == 0:
            # Create unsaved new item
            o = InventoryContent()
            o.material = material_code
            o.location = location
            o.batch = None if batch == "" else batch
            o.ref_doc = ref_doc
            return o
        return r[0]

    @classmethod
    def query_content(cls,
                      material=None,
                      location=None,
                      batch=None,
                      ref_doc=None,
                      **kwargs):
        """
        A thin wrapper to make a inventory content query easier

        :param material:
        :param location:
        :param batch:
        :param ref_doc:
        :param kwargs:
        :return:
        """
        cond = {
            'material': material,
            'location': location,
            'batch': batch,
            'ref_doc': ref_doc
        }
        # sanitize
        # (1) remove all none attributes
        del_keys = []
        for k in cond:
            if cond[k] is None:
                del_keys.append(k)
        for k in del_keys:
            del cond[k]

        # (2) String attributes
        for k in ['material', 'location', 'batch']:
            if k in cond:
                cond[k] = str(cond[k])
        # (3) take care of ref_doc field
        if 'ref_doc' in cond:
            if isinstance(cond['ref_doc'], doc.Doc):
                cond['ref_doc.0'] = cond['ref_doc'].object_id
                cond['ref_doc.1'] = cond['ref_doc'].manager.collection_name
                del cond['ref_doc']
            elif isinstance(cond['ref_doc'], ObjectId):
                cond['ref_doc.0'] = cond['ref_doc']
                del cond['ref_doc']
            else:
                raise ValidationError(
                    _("ERR_QUERY_CONTENT_CANNOT_INTERPRET_PARAMETER: %(parameter)s %(type)s"
                      ) % {
                          'parameter': 'ref_doc',
                          'type': type(cond['ref_doc'])
                      })
        # query
        return cls.manager.find(cond=cond)

    class Meta:
        collection_name = 'inv_content'
        require_permission = True
Пример #22
0
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
Пример #23
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')]
Пример #24
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