Esempio n. 1
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
        ]
Esempio 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()
Esempio n. 3
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'
Esempio n. 4
0
class Confirmation(doc.FieldSpecAware):
    # assign from frontend
    actual_start = doc.FieldDateTime(none=True)  # datetime
    actual_duration = doc.FieldNumeric(none=True)  # seconds
    actual_end = doc.FieldDateTime(none=True)  # datetime
    assignee = doc.FieldAssignee()  # user_code, object_id, group_code

    # automatically injected
    created_by = doc.FieldIntraUser()
    created_on = doc.FieldDateTime()
    cancelled = doc.FieldDoc('event', none=True)

    def cancel(self, cancelled_by, **kwargs):
        self.cancelled = doc.Event.create(doc.Event.CANCELLED, cancelled_by,
                                          **kwargs)

    def is_cancelled(self):
        return self.cancelled is not None

    @classmethod
    def create(cls, actual_start, actual_end, actual_duration, assignee,
               **kwargs):
        """
        A convenient method to convert **details from AJAX to python confirm object.

        :param actual_start:
        :param actual_end:
        :param actual_duration:
        :param assignee:
        :return:
        """
        o = cls()
        o.actual_start = actual_start
        o.actual_end = actual_end
        o.actual_duration = actual_duration
        o.assignee = assignee
        o.created_by = kwargs.pop('created_by', assignee)
        o.created_on = kwargs.pop('created_on', utils.NOW())
        return o
Esempio n. 5
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)
Esempio n. 6
0
class TaskDoc(doc.Authored):
    task = doc.FieldTask(none=False)
    """:type : Task"""
    planned_duration = doc.FieldNumeric(none=True)  # save in minutes
    actual_start = doc.FieldDateTime(none=True)
    actual_duration = doc.FieldNumeric(none=True, default=0)
    actual_end = doc.FieldDateTime(none=True)
    assignee = doc.FieldAssignee()
    confirmations = doc.FieldList(doc.FieldNested(Confirmation))
    ref_doc = doc.FieldAnyDoc()
    cancelled = doc.FieldDoc('event')
    details = doc.FieldSpec(dict, none=True)
    doc_no = doc.FieldString(none=True)

    def validate(self):
        if not self.task:
            raise ValidationError(_("ERROR_TASK_IS_REQUIRED"))

        if self.assignee is None:
            self.assignee = self.task.default_assignee()

    def assert_assignee(self, assignee):
        """
        Validate if assignee as IntraUser is applicable for the task.

        :param assignee:
        :return:
        """
        if isinstance(assignee, IntraUser):
            if not assignee.can("write", self.task.code):
                raise ValidationError(
                    _("ERR_INVALID_ASSIGNEE: %(user)s %(task_code)s") % {
                        'user': self.assignee,
                        'task_code': self.task.code
                    })
        elif isinstance(assignee, TaskGroup):
            if self.task.default_assignee() is not self.assignee:
                raise ValidationError(
                    _("ERR_INVALID_ASSIGNEE: %(group)s %(task_code)s") % {
                        'task_code': self.task.code,
                        'group': self.assignee
                    })

    def cancel_confirmation(self, user, index, **kwargs):
        self.confirmations[index].cancelled = doc.Event.create(
            doc.Event.CANCELLED, user)

        self.touched(user, **kwargs)

    def cancel(self, user, **kwargs):
        if self.cancelled:
            raise ValidationError(_("ERROR_TASK_IS_ALREADY_CANCELLED"))

        self.cancelled = doc.Event.create(doc.Event.CANCELLED,
                                          user,
                                          against=self)

        self.touched(user, **kwargs)

    @classmethod
    def confirmation_class(cls):
        return Confirmation

    def confirm(self, user, confirmation, **kwargs):
        if not isinstance(confirmation, Confirmation):
            raise BadParameterError(
                _("ERROR_CONFIRMATION_MUST_BE_INSTANCE_OF_CONFIRMATION"))

        # for first confirmation, init array. otherwise, append it
        if len(self.confirmations) > 0:
            self.confirmations.append(confirmation)
        else:
            self.confirmations = [confirmation]

        # dispatch event
        signals.task_confirmed.send(self.__class__,
                                    instance=self,
                                    code=str(task.code),
                                    confirmation=confirmation)
        self.touched(user, **kwargs)

    def touched(self, user, **kwargs):
        bypass = kwargs.pop("bypass", None)
        if not kwargs.pop("automated", False) and not bypass:
            self.assert_permission(user, self.PERM_W, self.task.code)
        super(TaskDoc, self).touched(user, **kwargs)

    def save(self):
        if not self.doc_no:
            self.doc_no = doc.RunningNumberCenter.new_number(TASK_NUMBER_KEY)

        super(TaskDoc, self).save()

    class Meta:
        collection_name = 'task'
        require_permission = True
        permission_write = task.Task.get_task_list() + [
            TASK_PERM_OVERRIDE_ASSIGNEE
        ]
        doc_no_prefix = TASK_NUMBER_PREFIX
Esempio n. 7
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
Esempio n. 8
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
Esempio n. 9
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
Esempio n. 10
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
Esempio n. 11
0
class OffHoursRange(docs.Doc):
    start = docs.FieldDateTime()
    end = docs.FieldDateTime()

    @classmethod
    def sync(cls, are_touching=None):
        """

        :param are_touching:
        :return:
        """
        if are_touching is None:
            are_touching = lambda x, y: y - x == 1

        offhours = OffHours.manager.find(cond={
            '$sort': 'start_time'
        })
        output = []

        new_start = None
        new_end = None

        length = len(offhours)
        for i, offhour in enumerate(offhours):
            if new_start is None:
                new_start = offhour.start_time
                new_end = offhour.end_time
            elif new_end >= offhour.start_time or are_touching(new_end, offhour.start_time):
                new_end = max(offhour.end_time, new_end)
            else:
                output.append([new_start, new_end])
                new_start = offhour.start_time
                new_end = offhour.end_time

            # Last item
            if i + 1 == length:
                output.append([new_start, new_end])

        bulk = cls.manager.o.initialize_ordered_bulk_op()
        bulk.find({}).remove()      # delete everything
        for entry in output:
            bulk.insert({'start': entry[0], 'end': entry[1]})
        print bulk.execute()

    @classmethod
    def between(cls, start_time=None, end_time=None):
        """
        Query any items that overlaps between start_time, and end_time
        if both start_time: and end_time: is omitted, start_time=now() will be used.

        :param start_time: optional
        :param end_time: optional
        :return:
        """
        # Sanitize input
        if start_time is None and end_time is None:
            start_time = datetime.now()

        # Build condition
        cond = {
            "end": {"$gt": start_time},
            "$sort": "start"
        }
        if end_time is not None:
            cond["start"] = {"$lt": end_time}

        return cls.manager.find(cond=cond)

    class Meta:
        collection_name = "offhours-range"
        indices = [
            ([("start_time", 1)], {"background": True, "sparse": False}),   # improve search performance
            ([("end_time", -1)], {"background": True, "sparse": False})     # improve search performance
        ]