Ejemplo n.º 1
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()
Ejemplo n.º 2
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()
Ejemplo n.º 3
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)
Ejemplo n.º 4
0
class SchematicMaterial(doc.FieldSpecAware):
    """
    SchematicMaterial = Schematic's component

    An entry that represent as a material to be used within SchematicEntry
    """
    code = doc.FieldTypedCode(codes.StockCode,
                              allow_incomplete=True)  # TypedCode
    quantity = doc.FieldList(doc.FieldNumeric(default=1, none=False))
    is_configurable = doc.FieldBoolean(default=False)
    counter = doc.FieldUom(
        none=True)  # default from material object resolved from code
    cost = doc.FieldNumeric(
        default=0,
        none=False)  # default from material object resolved from code

    def normalized(self, for_size_index):
        o = self.__class__()
        # easy stuff
        o.code = self.code
        o.is_configurable = False
        o.counter = self.counter
        o.cost = self.cost
        # size sensitive stuff
        if not self.is_configurable:
            o.quantity = self.quantity[:]
        else:
            if len(self.quantity) <= for_size_index or for_size_index < 0:
                raise ValidationError(
                    _("ERR_NORMALIZE_SCHEMATIC_ENTRY_FAILED_MATERIAL_QUANTITY_SIZE_INDEX_OUT_OF_RANGE"
                      ))
            o.quantity = [self.quantity[for_size_index]]
        return o

    def __repr__(self):
        return "%s x %s %s %s@%s" % (self.code, self.quantity, str(
            self.counter), "(conf) " if self.is_configurable else "",
                                     self.cost)
Ejemplo n.º 5
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
Ejemplo n.º 6
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)
Ejemplo n.º 7
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
Ejemplo n.º 8
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
Ejemplo n.º 9
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
Ejemplo n.º 10
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()