Esempio n. 1
0
class QualityDocument(doc.Authored):
    ref_doc = doc.FieldAnyDoc()
    cancelled = doc.FieldDoc('event')
    doc_no = doc.FieldString(none=True)
    items = doc.FieldList(doc.FieldNested(QualityItem))

    class Meta:
        collection_name = 'quality_document'
        require_permission = True
        doc_no_prefix = QUALITY_DOC_PREFIX
Esempio n. 2
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. 3
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. 4
0
class Schematic(doc.Authored):
    material_id = doc.FieldDoc(
        'material_master')  # link to Material (for redundancy check),
    rev_id = doc.FieldNumeric(
        default=1
    )  # Support Revision (based on Design rev_id)in case schematic is detached
    conf_size = doc.FieldList(doc.FieldString())  # Support Configuration
    source = doc.FieldAnyDoc(
        none=True)  # source of schematic, link directly to design object
    schematic = doc.FieldList(doc.FieldNested(
        SchematicEntry))  # schematic - saved from Design's Process Entry.

    def expand(self, is_production=False):
        """

        :param is_production:
        :return: void
        """
        if self.schematic and len(self.schematic) > 0:
            context = map(lambda s: s.to_dummy(), self.schematic)
            new_context = task.Task.expand(context,
                                           is_production=is_production,
                                           verbose=False)
            self.schematic = map(lambda a: SchematicEntry.from_dummy(a),
                                 new_context)

    @classmethod
    def pair_exists(cls, material_id, rev_id, conf_size):
        """
        validate if material_id + rev_id + conf_size exist.

        :param material_id:
        :param rev_id:
        :param conf_size:
        :return:
        """
        return 0 < Schematic.manager.count(
            cond={
                'material_id': doc._objectid(material_id),
                'rev_id': rev_id,
                'conf_size': conf_size
            })

    @staticmethod
    def factory(material_id, rev_id, author, **kwargs):
        verbose = kwargs.pop('verbose', False)
        sch = Schematic.manager.find(1,
                                     0,
                                     cond={
                                         'material_id': material_id,
                                         'rev_id': rev_id
                                     })
        if len(sch) > 0:
            if verbose:
                print(
                    "Schematic.factory() -> Schematic already exists %s, %s" %
                    (material_id, rev_id))
            return sch[0]

        # Create new one
        sch = Schematic()
        sch.material_id = material_id
        sch.rev_id = rev_id
        sch.conf_size = kwargs.pop('conf_size', [])
        sch.touched(author)
        if verbose:
            print("Schematic.factory() -> Creating new schematic %s, %s" %
                  (material_id, rev_id))
        return sch

    @classmethod
    def of(cls, material_id, revision_id, throw=False):
        """

        :param basestring|ObjectId material_id:
        :param int revision_id:
        :param bool throw:
        :return Schematic:
        """
        o = Schematic.manager.find(cond={
            'material_id': doc._objectid(material_id),
            'rev_id': revision_id
        })
        if len(o) == 0:
            if throw:
                raise ValueError('Unknown material+revision=%s+%s' %
                                 (material_id, revision_id))
            else:
                return None
        return o[0]

    @classmethod
    def revisions(cls, material_id):
        """

        :param basestring|ObjectId material_id:
        :return [Schematic]:
        """
        return Schematic.manager.find(
            cond={'material_id': doc._objectid(material_id)})

    def __repr__(self):
        txt = "Material: %s rev%s (conf=%s) + schematic ...\n" % (
            self.material_id, self.rev_id, self.conf_size)
        if self.schematic and len(self.schematic) > 0:
            for sch in self.schematic:
                txt += "\t%s\n" % sch
        return txt

    class Meta:
        collection_name = 'material_schematic'
        references = [('material_master', 'material_id')]
Esempio n. 5
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
Esempio n. 6
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()
Esempio n. 7
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