예제 #1
0
class OpticalWorkOrder(Domain):
    """This holds the necessary information to execute an work order for optical
    stores.

    This includes all the details present in the prescription.

    For reference:
    http://en.wikipedia.org/wiki/Eyeglass_prescription

    See http://en.wikipedia.org/wiki/Eyeglass_prescription#Abbreviations_and_terms
    for reference no the names used here.

    In some places, RE is used as a short for right eye, and LE for left eye
    """
    __storm_table__ = 'optical_work_order'

    #: Lens used in glasses
    LENS_TYPE_OPHTALMIC = u'ophtalmic'

    #: Contact lenses
    LENS_TYPE_CONTACT = u'contact'

    #: The frame for the lens is a closed ring
    FRAME_TYPE_CLOSED_RING = u'closed-ring'

    #: The frame uses a nylon string to hold the lenses.
    FRAME_TYPE_NYLON = u'nylon'

    #: The frame is made 3 pieces
    FRAME_TYPE_3_PIECES = u'3-pieces'

    lens_types = {
        LENS_TYPE_OPHTALMIC: _('Ophtalmic'),
        LENS_TYPE_CONTACT: _('Contact'),
    }

    frame_types = {
        # Translators: Aro fechado
        FRAME_TYPE_3_PIECES: _('Closed ring'),

        # Translators: Fio de nylon
        FRAME_TYPE_NYLON: _('Nylon String'),

        # Translators: 3 preças
        FRAME_TYPE_CLOSED_RING: _('3 pieces'),
    }

    work_order_id = IdCol(allow_none=False)
    work_order = Reference(work_order_id, 'WorkOrder.id')

    medic_id = IdCol()
    medic = Reference(medic_id, 'OpticalMedic.id')

    prescription_date = DateTimeCol()

    #: The name of the patient. Note that we already have the client of the work
    #: order, but the patient may be someone else (like the son, father,
    #: etc...). Just the name is enough
    patient = UnicodeCol()

    #: The type of the lens, Contact or Ophtalmic
    lens_type = EnumCol(default=LENS_TYPE_OPHTALMIC)

    #
    #   Frame
    #

    #: The type of the frame. One of OpticalWorkOrder.FRAME_TYPE_*
    frame_type = EnumCol(default=FRAME_TYPE_CLOSED_RING)

    #: The vertical frame measure
    frame_mva = DecimalCol(default=decimal.Decimal(0))

    #: The horizontal frame measure
    frame_mha = DecimalCol(default=decimal.Decimal(0))

    #: The diagonal frame measure
    frame_mda = DecimalCol(default=decimal.Decimal(0))

    #: The brige is the part of the frame between the two lenses, above the nose.
    frame_bridge = DecimalCol()

    #
    # Left eye distance vision
    #

    le_distance_spherical = DecimalCol(default=0)
    le_distance_cylindrical = DecimalCol(default=0)
    le_distance_axis = DecimalCol(default=0)
    le_distance_prism = DecimalCol(default=0)
    le_distance_base = DecimalCol(default=0)
    le_distance_height = DecimalCol(default=0)

    #: Pupil distance (DNP in pt_BR)
    le_distance_pd = DecimalCol(default=0)
    le_addition = DecimalCol(default=0)

    #
    # Left eye distance vision
    #
    le_near_spherical = DecimalCol(default=0)
    le_near_cylindrical = DecimalCol(default=0)
    le_near_axis = DecimalCol(default=0)

    #: Pupil distance (DNP in pt_BR)
    le_near_pd = DecimalCol(default=0)

    #
    # Right eye distance vision
    #

    re_distance_spherical = DecimalCol(default=0)
    re_distance_cylindrical = DecimalCol(default=0)
    re_distance_axis = DecimalCol(default=0)
    re_distance_prism = DecimalCol(default=0)
    re_distance_base = DecimalCol(default=0)
    re_distance_height = DecimalCol(default=0)

    #: Pupil distance (DNP in pt_BR)
    re_distance_pd = DecimalCol(default=0)
    re_addition = DecimalCol(default=0)

    #
    # Right eye near vision
    #
    re_near_spherical = DecimalCol(default=0)
    re_near_cylindrical = DecimalCol(default=0)
    re_near_axis = DecimalCol(default=0)

    #: Pupil distance (DNP in pt_BR)
    re_near_pd = DecimalCol(default=0)

    @property
    def frame_type_str(self):
        return self.frame_types.get(self.frame_type, '')

    @property
    def lens_type_str(self):
        return self.lens_types.get(self.lens_type, '')
예제 #2
0
class OpticalProduct(Domain):
    """Stores information about products sold by optical stores.

    There are 3 main types of products sold by optical stores:

    - Glass frames (without lenses)
    - Glass lenses
    - Contact lenses
    """
    __storm_table__ = 'optical_product'

    #: The frame of the glases (without lenses)
    TYPE_GLASS_FRAME = u'glass-frame'

    #: The glasses to be used with a frame
    TYPE_GLASS_LENSES = u'glass-lenses'

    #: Contact lenses
    TYPE_CONTACT_LENSES = u'contact-lenses'

    product_id = IdCol(allow_none=False)
    product = Reference(product_id, 'Product.id')

    # The type indicates what of the following fields should be edited.
    optical_type = EnumCol()

    #: If this product should be reserved automatically when added to the sale
    #: with work order
    auto_reserve = BoolCol(default=True)

    #
    # Glass frame details
    #

    #: The type of the frame (prescription or sunglasses)
    gf_glass_type = UnicodeCol()

    #: Size of the frame, accordingly to the manufacturer (may also be a string,
    #: for instance Large, one size fits all, etc..)
    gf_size = UnicodeCol()

    # The type of the lenses used in this frame. (for isntance: demo lens,
    # solar, polarized, mirrored)
    gf_lens_type = UnicodeCol()

    # Color of the frame, accordingly to the manufacturer specification
    gf_color = UnicodeCol()

    #
    # Glass lenses details
    #

    # Fotossensivel
    #: Type of the lenses photosensitivity (for instance: tints, sunsensors,
    #: transitions, etc...)
    gl_photosensitive = UnicodeCol()

    # Anti reflexo
    #: A description of the anti glare treatment the lenses have.
    gl_anti_glare = UnicodeCol()

    # Índice refração
    #: Decimal value describing the refraction index
    gl_refraction_index = DecimalCol()

    # Classificação
    #: lenses may be monofocal, bifocal or multifocal
    gl_classification = UnicodeCol()

    # Adição
    #: Free text describing the range of the possible additions.
    gl_addition = UnicodeCol()

    # Diametro
    # Free text describing the range of the possible diameters for the lens
    gl_diameter = UnicodeCol()

    # Altura
    #: Free text describing the height of the lens
    gl_height = UnicodeCol()

    # Disponibilidade
    #: Free text describint the avaiability of the lens (in what possible
    #: parameters they are avaiable. For instance: "-10,00 a -2,25 Cil -2,00"
    gl_availability = UnicodeCol()

    #
    # Contact lenses details
    #

    # Grau
    #: Degree of the lenses, a decimal from -30 to +30, in steps of +- 0.25
    cl_degree = DecimalCol()

    # Classificação
    #: Free text describing the classification of the lenses (solid, gel, etc..)
    cl_classification = UnicodeCol()

    # tipo lente
    #: The type of the lenses (monofocal, toric, etc..)
    cl_lens_type = UnicodeCol()

    # Descarte
    #: How often the lens should be discarded (anually, daily, etc..)
    cl_discard = UnicodeCol()

    # Adição
    #: Free text describing the addition of the lenses.
    cl_addition = UnicodeCol()

    # Cilindrico
    # XXX: I still need to verify if a decimal column is ok, or if there are
    # possible text values.
    #: Cylindrical value of the lenses.
    cl_cylindrical = DecimalCol()

    # Eixo
    # XXX: I still need to verify if a decimal column is ok, or if there are
    # possible text values.
    #: Axix  of the lenses.
    cl_axis = DecimalCol()

    #: Free text color description of the lens (for cosmetic use)
    cl_color = UnicodeCol()

    # Curvatura
    #: Free text description of the curvature. normaly a decimal, but may have
    #: textual descriptions
    cl_curvature = UnicodeCol()

    @classmethod
    def get_from_product(cls, product):
        return product.store.find(cls, product=product).one()
예제 #3
0
class OpticalWorkOrder(Domain):
    """This holds the necessary information to execute an work order for optical
    stores.

    This includes all the details present in the prescription.

    For reference:
    http://en.wikipedia.org/wiki/Eyeglass_prescription

    See http://en.wikipedia.org/wiki/Eyeglass_prescription#Abbreviations_and_terms
    for reference no the names used here.

    In some places, RE is used as a short for right eye, and LE for left eye
    """
    __storm_table__ = 'optical_work_order'

    #: Lens used in glasses
    LENS_TYPE_OPHTALMIC = u'ophtalmic'

    #: Contact lenses
    LENS_TYPE_CONTACT = u'contact'

    #: The frame for the lens is a closed ring
    FRAME_TYPE_CLOSED_RING = u'closed-ring'

    #: The frame uses a nylon string to hold the lenses.
    FRAME_TYPE_NYLON = u'nylon'

    #: The frame is made 3 pieces
    FRAME_TYPE_3_PIECES = u'3-pieces'

    lens_types = {
        LENS_TYPE_OPHTALMIC: _('Ophtalmic'),
        LENS_TYPE_CONTACT: _('Contact'),
    }

    frame_types = {
        # Translators: Aro fechado
        FRAME_TYPE_3_PIECES: _('Closed ring'),

        # Translators: Fio de nylon
        FRAME_TYPE_NYLON: _('Nylon String'),

        # Translators: 3 preças
        FRAME_TYPE_CLOSED_RING: _('3 pieces'),
    }

    work_order_id = IdCol(allow_none=False)
    work_order = Reference(work_order_id, 'WorkOrder.id')

    medic_id = IdCol()
    medic = Reference(medic_id, 'OpticalMedic.id')

    prescription_date = DateTimeCol()

    #: The name of the patient. Note that we already have the client of the work
    #: order, but the patient may be someone else (like the son, father,
    #: etc...). Just the name is enough
    patient = UnicodeCol()

    #: The type of the lens, Contact or Ophtalmic
    lens_type = EnumCol(default=LENS_TYPE_OPHTALMIC)

    #
    #   Frame
    #

    #: The type of the frame. One of OpticalWorkOrder.FRAME_TYPE_*
    frame_type = EnumCol(default=FRAME_TYPE_CLOSED_RING)

    #: The vertical frame measure
    frame_mva = DecimalCol(default=decimal.Decimal(0))

    #: The horizontal frame measure
    frame_mha = DecimalCol(default=decimal.Decimal(0))

    #: The diagonal frame measure
    frame_mda = DecimalCol(default=decimal.Decimal(0))

    #: The brige is the part of the frame between the two lenses, above the nose.
    frame_bridge = DecimalCol()

    #
    # Left eye distance vision
    #

    le_distance_spherical = DecimalCol(default=0)
    le_distance_cylindrical = DecimalCol(default=0)
    le_distance_axis = DecimalCol(default=0)
    le_distance_prism = DecimalCol(default=0)
    le_distance_base = DecimalCol(default=0)
    le_distance_height = DecimalCol(default=0)

    #: Pupil distance (DNP in pt_BR)
    le_distance_pd = DecimalCol(default=0)
    le_addition = DecimalCol(default=0)

    #
    # Left eye distance vision
    #
    le_near_spherical = DecimalCol(default=0)
    le_near_cylindrical = DecimalCol(default=0)
    le_near_axis = DecimalCol(default=0)

    #: Pupil distance (DNP in pt_BR)
    le_near_pd = DecimalCol(default=0)

    #
    # Right eye distance vision
    #

    re_distance_spherical = DecimalCol(default=0)
    re_distance_cylindrical = DecimalCol(default=0)
    re_distance_axis = DecimalCol(default=0)
    re_distance_prism = DecimalCol(default=0)
    re_distance_base = DecimalCol(default=0)
    re_distance_height = DecimalCol(default=0)

    #: Pupil distance (DNP in pt_BR)
    re_distance_pd = DecimalCol(default=0)
    re_addition = DecimalCol(default=0)

    #
    # Right eye near vision
    #
    re_near_spherical = DecimalCol(default=0)
    re_near_cylindrical = DecimalCol(default=0)
    re_near_axis = DecimalCol(default=0)

    #: Pupil distance (DNP in pt_BR)
    re_near_pd = DecimalCol(default=0)

    #
    # Class methods
    #

    @classmethod
    def find_by_work_order(cls, store, work_order):
        return store.find(cls, work_order_id=work_order.id).one()

    #
    # Properties
    #

    @property
    def frame_type_str(self):
        return self.frame_types.get(self.frame_type, '')

    @property
    def lens_type_str(self):
        return self.lens_types.get(self.lens_type, '')

    #
    # Public API
    #

    def can_create_purchase(self):
        work_order = self.work_order
        if work_order.status != WorkOrder.STATUS_WORK_IN_PROGRESS:
            return False

        if not work_order.sale:
            return False

        purchases = [i.purchase_item for i in work_order.get_items()]
        # If there are any item in this work order that was not purchased yet, then we
        # can still create a purchase
        return None in purchases

    def create_purchase(self, supplier, work_order_item, is_freebie):
        """Create a purchase

        :param supplier: the |supplier| of that purchase
        :param work_order_item: The work order item that a purchase is being created
        for.
        :param is_freebie: indicates if the item is a freebie
        """
        sellable = work_order_item.sellable
        store = self.work_order.store
        purchase = PurchaseOrder(store=store,
                                 status=PurchaseOrder.ORDER_PENDING,
                                 supplier=supplier,
                                 responsible=api.get_current_user(store),
                                 branch=api.get_current_branch(store),
                                 work_order=self.work_order)
        if is_freebie:
            purchase.notes = _('The product %s is a freebie') % sellable.description
            # FIXME We may want the cost 0, but as it is we wont be able to
            # receive this purchase without changing the receiving. We must
            # evaluate the consequences of changing the receiving a little bit
            # further in order to change that behavior.
            cost = decimal.Decimal('0.01')
        else:
            cost = sellable.cost

        # Add the sellable to the purchase
        purchase_item = purchase.add_item(sellable,
                                          quantity=work_order_item.quantity,
                                          cost=cost)
        work_order_item.purchase_item = purchase_item

        purchase.confirm()
        return purchase

    def can_receive_purchase(self, purchase):
        work_order = self.work_order
        if not work_order.status == WorkOrder.STATUS_WORK_FINISHED:
            return False

        # XXX Lets assume that there is only on purchase
        return purchase and purchase.status == PurchaseOrder.ORDER_CONFIRMED

    def receive_purchase(self, purchase_order, reserve=False):
        receiving = purchase_order.create_receiving_order()
        receiving.confirm()
        if reserve:
            self.reserve_products(purchase_order)

    def reserve_products(self, purchase_order):
        for item in self.work_order.get_items():
            if not item.purchase_item:
                continue
            sale_item = item.sale_item
            to_reserve = sale_item.quantity - sale_item.quantity_decreased
            if to_reserve > 0:
                sale_item.reserve(quantize(to_reserve))

    def copy(self, target):
        """Make a copy of self into a target |work_order|

        :param target: a |work_order|
        """
        props = ['lens_type', 'le_distance_spherical', 'le_distance_cylindrical',
                 'le_distance_axis', 'le_distance_prism', 'le_distance_base',
                 'le_distance_height', 'le_distance_pd', 'le_addition',
                 'le_near_spherical', 'le_near_cylindrical', 'le_near_axis',
                 'le_near_pd', 're_distance_spherical', 're_distance_cylindrical',
                 're_distance_axis', 're_distance_prism', 're_distance_base',
                 're_distance_height', 're_distance_pd', 're_addition',
                 're_near_spherical', 're_near_cylindrical', 're_near_axis', 're_near_pd']

        for prop in props:
            value = getattr(self, prop)
            setattr(target, prop, value)
예제 #4
0
파일: ecfdomain.py 프로젝트: sarkis89/stoq
class DeviceConstant(Domain):
    """
    Describes a device constant

    The constant_value field is only used by custom tax codes,
    eg when constant_type is TYPE_TAX and constant_enum is TaxType.CUSTOM

    @cvar constant_type: the type of constant
    @cvar constant_name: name of the constant
    @cvar constant_enum: enum value of the constant
    @cvar constant_value: value of the constant, only for TAX constants for
      which it represents the tax percentage
    @cvar device_value: the device value
    @cvar printer: printer
    """

    __storm_table__ = 'device_constant'

    constant_type = EnumCol()
    constant_name = UnicodeCol()
    constant_value = DecimalCol(default=None)
    constant_enum = IntCol(default=None)
    device_value = BLOBCol()
    printer_id = IdCol()
    printer = Reference(printer_id, 'ECFPrinter.id')

    TYPE_UNIT = u'unit'
    TYPE_TAX = u'tax'
    TYPE_PAYMENT = u'payment'

    constant_types = {TYPE_UNIT: _(u'Unit'),
                      TYPE_TAX: _(u'Tax'),
                      TYPE_PAYMENT: _(u'Payment')}

    def get_constant_type_description(self):
        """
        Describe the type in a human readable form
        @returns: description of the constant type
        @rtype: str
        """
        return DeviceConstant.constant_types[self.constant_type]

    @classmethod
    def get_custom_tax_constant(cls, printer, constant_value, store):
        """
        Fetches a custom tax constant.

        @param printer: printer to fetch constants from
        @type printer: :class:`ECFPrinter`
        @param constant_enum: tax enum code
        @type constant_enum: int
        @param store: a store
        @returns: the constant
        @rtype: :class:`DeviceConstant`
        """
        return store.find(DeviceConstant,
                          printer=printer,
                          constant_type=DeviceConstant.TYPE_TAX,
                          constant_enum=int(TaxType.CUSTOM),
                          constant_value=constant_value).one()

    @classmethod
    def get_tax_constant(cls, printer, constant_enum, store):
        """
        Fetches a tax constant.
        Note that you need to use :class:`ECFPrinter.get_custom_tax_constant`
        for custom tax constants.

        @param printer: printer to fetch constants from
        @type printer: :class:`ECFPrinter`
        @param constant_enum: tax enum code
        @type constant_enum: int
        @param store: a store
        @returns: the constant
        @rtype: :class:`DeviceConstant`
        """
        if constant_enum == TaxType.CUSTOM:
            raise ValueError("Use get_custom_tax_constant for custom "
                             "tax codes")
        return store.find(DeviceConstant,
                          printer=printer,
                          constant_type=DeviceConstant.TYPE_TAX,
                          constant_enum=int(constant_enum)).one()

    def get_description(self):
        return self.constant_name
예제 #5
0
class Product(Domain):
    """A Product is a thing that can be:

      * ordered (via |purchase|)
      * stored (via |storable|)
      * sold (via |sellable|)
      * manufactured (via |production|)

    A manufactured product can have several |components|, which are parts
    that when combined create the product.

    A consigned product is borrowed from a |supplier|. You can also loan out
    your own products via |loan|.

    If the product does not use stock managment, it will be possible to sell
    items, even if it was never purchased.

    See also:
    `schema <http://doc.stoq.com.br/schema/tables/product.html>`__
    """

    __storm_table__ = 'product'

    sellable_id = IdCol()

    #: |sellable| for this product
    sellable = Reference(sellable_id, 'Sellable.id')

    suppliers = ReferenceSet('id', 'ProductSupplierInfo.product_id')

    #: if this product is loaned from the |supplier|
    consignment = BoolCol(default=False)

    #: ``True`` if this product has |components|.
    #: This is stored on Product to avoid a join to find out if there is any
    #: components or not.
    is_composed = BoolCol(default=False)

    #: If this product will use stock management.
    #: When this is set to ``True``, a corresponding |storable| should be created.
    #: For ``False`` a storable will not be created and the quantity currently
    #: in stock will not be known, e.g. |purchases| will not increase the stock
    # quantity, and the operations that decrease stock (like a |sale| or a
    # |loan|, will be allowed at any time.
    manage_stock = BoolCol(default=True)

    #: physical location of this product, like a drawer or shelf number
    location = UnicodeCol(default=u'')

    manufacturer_id = IdCol(default=None)

    #: name of the manufacturer for this product, eg "General Motors"
    manufacturer = Reference(manufacturer_id, 'ProductManufacturer.id')

    #: name of the brand, eg "Chevrolet" or "Opel"
    brand = UnicodeCol(default=u'')

    #: name of the family, eg "Cobalt" or "Astra"
    family = UnicodeCol(default=u'')

    #: name of the model, eg "2.2 L Ecotec L61 I4" or "2.0 8V/ CD 2.0 Hatchback 5p Aut"
    model = UnicodeCol(default=u'')

    #: a number representing this part
    part_number = UnicodeCol(default=u'')

    #: physical width of this product, unit not enforced
    width = DecimalCol(default=0)

    #: physical height of this product, unit not enforced
    height = DecimalCol(default=0)

    #: depth in this product, unit not enforced
    depth = DecimalCol(default=0)

    #: physical weight of this product, unit not enforced
    weight = DecimalCol(default=0)

    #: The time in days it takes to manufacter this product
    production_time = IntCol(default=1)

    #: Brazil specific: NFE: nomenclature comon do mercuosol
    ncm = UnicodeCol(default=None)

    #: NFE: see ncm
    ex_tipi = UnicodeCol(default=None)

    #: NFE: see ncm
    genero = UnicodeCol(default=None)

    icms_template_id = IdCol(default=None)

    icms_template = Reference(icms_template_id, 'ProductIcmsTemplate.id')

    ipi_template_id = IdCol(default=None)

    ipi_template = Reference(ipi_template_id, 'ProductIpiTemplate.id')

    #: Used for composed products only
    quality_tests = ReferenceSet('id', 'ProductQualityTest.product_id')

    #: list of |suppliers| that sells this product
    suppliers = ReferenceSet('id', 'ProductSupplierInfo.product_id')

    @property
    def description(self):
        return self.sellable.description

    @property
    def storable(self):
        return self.store.find(Storable, product=self).one()

    #
    #  Public API
    #

    def has_quality_tests(self):
        return not self.quality_tests.find().is_empty()

    def remove(self):
        """Deletes this product from the database.
        """
        storable = self.storable
        if storable:
            self.store.remove(storable)
        for i in self.get_suppliers_info():
            self.store.remove(i)
        for i in self.get_components():
            self.store.remove(i)

        self.store.remove(self)

    def can_remove(self):
        """Whether we can delete this product and it's |sellable| from the
        database.

        ``False`` if the product was sold, received or used in a
        production. ``True`` otherwise.
        """
        from stoqlib.domain.production import ProductionItem
        if self.get_history().count():
            return False
        storable = self.storable
        if storable and storable.get_stock_items().count():
            return False
        # Return False if the product is component of other.
        elif self.store.find(ProductComponent,
                             component=self).count():
            return False
        # Return False if the component(product) is used in a production.
        elif self.store.find(ProductionItem,
                             product=self).count():
            return False
        return True

    def can_close(self):
        """Checks if this product can be closed

        Called by |sellable| to check if it can be closed or not.
        A product can be closed if it doesn't have any stock left
        """
        if self.manage_stock:
            return self.storable.get_total_balance() == 0
        return True

    def get_manufacture_time(self, quantity, branch):
        """Returns the estimated time in days to manufacture a product

        If the |components| don't have enough stock, the estimated time to
        obtain missing |components| will also be considered (using the max
        lead time from the |suppliers|)

        :param quantity:
        :param branch: the |branch|
        """
        assert self.is_composed

        # Components maximum lead time
        comp_max_time = 0
        for i in self.get_components():
            storable = i.component.storable
            needed = quantity * i.quantity
            stock = storable.get_balance_for_branch(branch)
            # We have enought of this component items to produce.
            if stock >= needed:
                continue
            comp_max_time = max(comp_max_time,
                                i.component.get_max_lead_time(needed, branch))
        return self.production_time + comp_max_time

    def get_max_lead_time(self, quantity, branch):
        """Returns the longest lead time for this product.

        If this is a composed product, the lead time will be the time to
        manufacture the product plus the time to obtain all the missing
        components

        If its a regular product this will be the longest lead time for a
        supplier to deliver the product (considering the worst case).

        quantity and |branch| are used only when the product is composed
        """
        if self.is_composed:
            return self.get_manufacture_time(quantity, branch)
        else:
            return self.suppliers.find().max(ProductSupplierInfo.lead_time) or 0

    def get_history(self):
        """Returns the list of :class:`ProductHistory` for this product.
        """
        return self.store.find(ProductHistory, sellable=self.sellable)

    def get_main_supplier_name(self):
        supplier_info = self.get_main_supplier_info()
        return supplier_info.get_name()

    def get_main_supplier_info(self):
        """Gets a list of main suppliers for a Product, the main supplier
        is the most recently selected supplier.

        :returns: main supplier info
        :rtype: ProductSupplierInfo or None if a product lacks
           a main suppliers
        """
        store = self.store
        return store.find(ProductSupplierInfo,
                          product=self, is_main_supplier=True).one()

    def get_suppliers_info(self):
        """Returns a list of suppliers for this product

        :returns: a list of suppliers
        :rtype: list of ProductSupplierInfo
        """
        return self.store.find(ProductSupplierInfo,
                               product=self)

    def get_components(self):
        """Returns the products which are our |components|.

        :returns: a sequence of |components|
        """
        return self.store.find(ProductComponent, product=self)

    def has_components(self):
        """Returns if this product has a |component| or not.

        :returns: ``True`` if this product has |components|, ``False`` otherwise.
        """
        return self.get_components().count() > 0

    def get_production_cost(self):
        """ Return the production cost of one unit of the product.

        :returns: the production cost
        """
        return self.sellable.cost

    def is_supplied_by(self, supplier):
        """If this product is supplied by the given |supplier|, returns the
        object with the supplier information. Returns ``None`` otherwise
        """
        store = self.store
        return store.find(ProductSupplierInfo, product=self,
                          supplier=supplier).one() is not None

    def is_composed_by(self, product):
        """Returns if we are composed by a given product or not.

        :param product: a possible component of this product
        :returns: ``True`` if the given product is one of our component or a
          component of our components, otherwise ``False``.
        """
        for component in self.get_components():
            if product is component.component:
                return True
            if component.component.is_composed_by(product):
                return True
        return False

    def is_being_produced(self):
        from stoqlib.domain.production import ProductionOrderProducingView
        return ProductionOrderProducingView.is_product_being_produced(self)

    #
    # Domain
    #

    def on_create(self):
        ProductCreateEvent.emit(self)

    def on_delete(self):
        ProductRemoveEvent.emit(self)

    def on_update(self):
        store = self.store
        emitted_store_list = getattr(self, '_emitted_store_list', set())

        # Since other classes can propagate this event (like Sellable),
        # emit the event only once for each store.
        if not store in emitted_store_list:
            ProductEditEvent.emit(self)
            emitted_store_list.add(store)

        self._emitted_store_list = emitted_store_list
예제 #6
0
class WorkOrder(Domain):
    """Represents a work order

    Normally, this is a maintenance task, like:
        * The |client| reports a defect on an equipment.
        * The responsible for doing the quote analyzes the equipment
          and detects the real defect.
        * The |client| then approves the quote and the work begins.
        * After it's finished, a |sale| is created for it, the
          |client| pays and gets it's equipment back.

    .. graphviz::

       digraph work_order_status {
         STATUS_OPENED -> STATUS_APPROVED;
         STATUS_OPENED -> STATUS_CANCELLED;
         STATUS_APPROVED -> STATUS_OPENED;
         STATUS_APPROVED -> STATUS_CANCELLED;
         STATUS_APPROVED -> STATUS_WORK_IN_PROGRESS;
         STATUS_WORK_IN_PROGRESS -> STATUS_WORK_FINISHED;
         STATUS_WORK_FINISHED -> STATUS_CLOSED;
       }

    See also:
    `schema <http://doc.stoq.com.br/schema/tables/work_order.html>`__
    """

    __storm_table__ = 'work_order'

    implements(IContainer)

    #: a request for an order has been created, the order has not yet
    #: been approved the |client|
    STATUS_OPENED = 0

    #: for some reason it was cancelled
    STATUS_CANCELLED = 1

    #: the |client| has approved the order, work has not begun yet
    STATUS_APPROVED = 2

    #: work is currently in progress
    STATUS_WORK_IN_PROGRESS = 3

    #: work has been finished, but no |sale| has been created yet.
    #: Work orders with this status will be displayed in the till/pos
    #: applications and it's possible to create a |sale| from them.
    STATUS_WORK_FINISHED = 4

    #: a |sale| has been created, delivery and payment handled there
    STATUS_CLOSED = 5

    statuses = {
        STATUS_OPENED: _(u'Waiting'),
        STATUS_CANCELLED: _(u'Cancelled'),
        STATUS_APPROVED: _(u'Approved'),
        STATUS_WORK_IN_PROGRESS: _(u'In progress'),
        STATUS_WORK_FINISHED: _(u'Finished'),
        STATUS_CLOSED: _(u'Closed')
    }

    status = IntCol(default=STATUS_OPENED)

    #: A numeric identifier for this object. This value should be used instead of
    #: :obj:`Domain.id` when displaying a numerical representation of this object to
    #: the user, in dialogs, lists, reports and such.
    identifier = IdentifierCol()

    #: defected equipment
    equipment = UnicodeCol()

    #: defect reported by the |client|
    defect_reported = UnicodeCol()

    #: defect detected by the :obj:`.quote_responsible`
    defect_detected = UnicodeCol()

    #: estimated hours needed to complete the work
    estimated_hours = DecimalCol(default=None)

    #: estimated cost of the work
    estimated_cost = PriceCol(default=None)

    #: estimated date the work will start
    estimated_start = DateTimeCol(default=None)

    #: estimated date the work will finish
    estimated_finish = DateTimeCol(default=None)

    #: date this work was opened
    open_date = DateTimeCol(default_factory=localnow)

    #: date this work was approved (set by :obj:`.approve`)
    approve_date = DateTimeCol(default=None)

    #: date this work was finished (set by :obj:`.finish`)
    finish_date = DateTimeCol(default=None)

    branch_id = IntCol()
    #: the |branch| where this order was created and responsible for it
    branch = Reference(branch_id, 'Branch.id')

    current_branch_id = IntCol()
    #: the actual branch where the order is. Can differ from
    # :attr:`.branch` if the order was sent in a |workorderpackage|
    #: to another |branch| for execution
    current_branch = Reference(current_branch_id, 'Branch.id')

    quote_responsible_id = IntCol(default=None)
    #: the |loginuser| responsible for the :obj:`.defect_detected`
    quote_responsible = Reference(quote_responsible_id, 'LoginUser.id')

    execution_responsible_id = IntCol(default=None)
    #: the |loginuser| responsible for the execution of the work
    execution_responsible = Reference(execution_responsible_id, 'LoginUser.id')

    client_id = IntCol(default=None)
    #: the |client|, owner of the equipment
    client = Reference(client_id, 'Client.id')

    category_id = IntCol(default=None)
    #: the |workordercategory| this work belongs
    category = Reference(category_id, 'WorkOrderCategory.id')

    sale_id = IntCol(default=None)
    #: the |sale| created after this work is finished
    sale = Reference(sale_id, 'Sale.id')

    order_items = ReferenceSet('id', 'WorkOrderItem.order_id')

    @property
    def status_str(self):
        if self.is_in_transport():
            return _("In transport")
        return self.statuses[self.status]

    def __init__(self, *args, **kwargs):
        super(WorkOrder, self).__init__(*args, **kwargs)

        if self.current_branch is None:
            self.current_branch = self.branch

    #
    #  IContainer implementation
    #

    def add_item(self, item):
        assert item.order is None
        item.order = self

    def get_items(self):
        return self.order_items

    def remove_item(self, item):
        assert item.order is self
        # Setting the quantity to 0 and calling sync_stock
        # will return all the actual quantity to the stock
        item.quantity = 0
        item.sync_stock()
        self.store.remove(item)

    #
    #  Public API
    #

    def get_total_amount(self):
        """Returns the total amount of this work order

        This is the same as::

            sum(item.total for item in :obj:`.order_items`)

        """
        items = self.order_items.find()
        return (items.sum(WorkOrderItem.price * WorkOrderItem.quantity)
                or currency(0))

    def add_sellable(self, sellable, price=None, quantity=1, batch=None):
        """Adds a sellable to this work order

        :param sellable: the |sellable| being added
        :param price: the price the sellable will be sold when
            finishing this work order
        :param quantity: the sellable's quantity
        :param batch: the |batch| this sellable comes from, if the sellable is a
          storable. Should be ``None`` if it is not a storable or if the storable
          does not have batches.
        :returns: the created |workorderitem|
        """
        self.validate_batch(batch, sellable=sellable)
        if price is None:
            price = sellable.base_price

        item = WorkOrderItem(store=self.store,
                             sellable=sellable,
                             batch=batch,
                             price=price,
                             quantity=quantity,
                             order=self)
        return item

    def sync_stock(self):
        """Synchronizes the stock for this work order's items

        Just a shortcut to call :meth:`WorkOrderItem.sync_stock` in all
        items in this work order.
        """
        for item in self.get_items():
            item.sync_stock()

    def is_in_transport(self):
        """Checks if this work order is in transport

        A work order is in transport if it's :attr:`.current_branch`
        is ``None``. The transportation of the work order is done in
        a |workorderpackage|

        :returns: ``True`` if in transport, ``False`` otherwise
        """
        return self.current_branch is None

    def is_finished(self):
        """Checks if this work order is finished

        A work order is finished when the work that needs to be done
        on it finished, so this will be ``True`` when :obj:`WorkOrder.status` is
        :obj:`.STATUS_WORK_FINISHED` and :obj:`.STATUS_CLOSED`
        """
        return self.status in [self.STATUS_WORK_FINISHED, self.STATUS_CLOSED]

    def is_late(self):
        """Checks if this work order is late

        Being late means we set an
        :obj:`estimated finish date <.estimated_finish>` and that
        date has already passed.
        """
        if self.status in [self.STATUS_WORK_FINISHED, self.STATUS_CLOSED]:
            return False
        if not self.estimated_finish:
            # No estimated_finish means we are not late
            return False

        today = localtoday().date()
        return self.estimated_finish.date() < today

    def can_cancel(self):
        """Checks if this work order can be cancelled

        Only opened and approved orders can be cancelled. Once the
        work has started, it should not be possible to do that anymore.

        :returns: ``True`` if can be cancelled, ``False`` otherwise
        """
        return self.status in [self.STATUS_OPENED, self.STATUS_APPROVED]

    def can_approve(self):
        """Checks if this work order can be approved

        :returns: ``True`` if can be approved, ``False`` otherwise
        """
        return self.status == self.STATUS_OPENED

    def can_undo_approval(self):
        """Checks if this work order order can be unapproved

        Only approved orders can be unapproved. Once the work
        has started, it should not be possible to do that anymore

        :returns: ``True`` if can be unapproved, ``False`` otherwise
        """
        return self.status == self.STATUS_APPROVED

    def can_start(self):
        """Checks if this work order can start

        Note that the work needs to be approved before it can be started.

        :returns: ``True`` if can start, ``False`` otherwise
        """
        return self.status == self.STATUS_APPROVED

    def can_finish(self):
        """Checks if this work order can finish

        Note that the work needs to be started before you can finish.

        :returns: ``True`` if can finish, ``False`` otherwise
        """
        if not self.order_items.count():
            return False
        return self.status == self.STATUS_WORK_IN_PROGRESS

    def can_close(self):
        """Checks if this work order can close

        Note that the work needs to be finished before you can close.

        :returns: ``True`` if can close, ``False`` otherwise
        """
        return self.status == self.STATUS_WORK_FINISHED

    def cancel(self):
        """Cancels this work order

        Cancel the work order, probably because the |client|
        didn't approve it or simply gave up of doing it.
        """
        assert self.can_cancel()
        self.status = self.STATUS_CANCELLED

    def approve(self):
        """Approves this work order

        Approving means that the |client| has accepted the
        work's quote and it's cost and it can now start.
        """
        assert self.can_approve()
        self.approve_date = localnow()
        self.status = self.STATUS_APPROVED

    def undo_approval(self):
        """Unapproves this work order

        Unapproving means that the |client| once has approved the
        order's task and it's cost, but now he doesn't anymore.
        Different from :meth:`.cancel`, the |client| still can
        approve this again.
        """
        assert self.can_undo_approval()
        self.approve_date = None
        self.status = self.STATUS_OPENED

    def start(self):
        """Starts this work order's task

        The :obj:`.execution_responsible` started working on
        this order's task and will finish sometime in the future.
        """
        assert self.can_start()
        self.status = self.STATUS_WORK_IN_PROGRESS

    def finish(self):
        """Finishes this work order's task

        The :obj:`.execution_responsible` has finished working on
        this order's task. It's possible now to give the equipment
        back to the |client| and create a |sale| so we are able
        to :meth:`close <.close>` this order.
        """
        assert self.can_finish()
        self.finish_date = localnow()
        self.status = self.STATUS_WORK_FINISHED

    def close(self):
        """Closes this work order

        This order's task is done, the |client| got the equipment
        back and a |sale| was created for the |workorderitems|
        Nothing more needs to be done.
        """
        assert self.can_close()
        self.status = self.STATUS_CLOSED

    def change_status(self, new_status):
        """
        Change the status of this work order

        Using this function you can change the status is several steps.

        :returns: if the status was changed
        :raises: :exc:`stoqlib.exceptions.InvalidStatus` if the status cannot be changed
        """
        if self.status == WorkOrder.STATUS_WORK_FINISHED:
            raise InvalidStatus(
                _("This work order has already been finished, it cannot be modified."
                  ))

        # This is the logic order of status changes, this is the flow/ordering
        # of the status that should be used
        status_order = [
            WorkOrder.STATUS_OPENED, WorkOrder.STATUS_APPROVED,
            WorkOrder.STATUS_WORK_IN_PROGRESS, WorkOrder.STATUS_WORK_FINISHED
        ]

        old_index = status_order.index(self.status)
        new_index = status_order.index(new_status)
        direction = cmp(new_index, old_index)

        next_status = self.status
        while True:
            # Calculate what's the next status we should set in order to reach
            # our goal (new_status). Note that this can go either forward or backward
            # depending on the direction
            next_status = status_order[status_order.index(next_status) +
                                       direction]
            if next_status == WorkOrder.STATUS_WORK_IN_PROGRESS:
                if not self.can_start():
                    raise InvalidStatus(_("This work order cannot be started"))
                self.start()

            if next_status == WorkOrder.STATUS_WORK_FINISHED:
                if not self.can_finish():
                    raise InvalidStatus(
                        _('This work order cannot be finished'))
                self.finish()

            if next_status == WorkOrder.STATUS_APPROVED:
                if not self.can_approve():
                    raise InvalidStatus(
                        _("This work order cannot be approved, it's already in progress"
                          ))
                self.approve()

            if next_status == WorkOrder.STATUS_OPENED:
                if not self.can_undo_approval():
                    raise InvalidStatus(
                        _('This work order cannot be re-opened'))
                self.undo_approval()

            # We've reached our goal, bail out
            if next_status == new_status:
                break

    @classmethod
    def find_by_sale(cls, store, sale):
        """Returns all |workorders| associated with the given |sale|.

        :param sale: The |sale| used to filter the existing |workorders|
        :resturn: An iterable with all work orders:
        :rtype: resultset
        """
        return store.find(cls, sale=sale)
예제 #7
0
class OpticalWorkOrder(Domain):
    """This holds the necessary information to execute an work order for optical
    stores.

    This includes all the details present in the prescription.

    For reference:
    http://en.wikipedia.org/wiki/Eyeglass_prescription

    See http://en.wikipedia.org/wiki/Eyeglass_prescription#Abbreviations_and_terms
    for reference no the names used here.

    In some places, RE is used as a short for right eye, and LE for left eye
    """
    __storm_table__ = 'optical_work_order'

    #: Lens used in glasses
    LENS_TYPE_OPHTALMIC = 0

    #: Contact lenses
    LENS_TYPE_CONTACT = 1

    #: The frame for the lens is a closed ring
    FRAME_TYPE_CLOSED_RING = 0

    #: The frame uses a nylon string to hold the lenses.
    FRAME_TYPE_NYLON = 1

    #: The frame is made 3 pieces
    FRAME_TYPE_3_PIECES = 2

    work_order_id = IdCol(allow_none=False)
    work_order = Reference(work_order_id, 'WorkOrder.id')

    medic_id = IdCol()
    medic = Reference(medic_id, 'OpticalMedic.id')

    prescription_date = DateTimeCol()

    #: The name of the patient. Note that we already have the client of the work
    #: order, but the patient may be someone else (like the son, father,
    #: etc...). Just the name is enough
    patient = UnicodeCol()

    #: The type of the lens, Contact or Ophtalmic
    lens_type = IntCol(default=LENS_TYPE_OPHTALMIC)

    #
    #   Frame
    #

    #: The type of the frame. One of OpticalWorkOrder.FRAME_TYPE_*
    frame_type = IntCol()

    # TODO: I still need to find out the real meaning of this property (waiting
    # for the clients anweser
    frame_mva = DecimalCol()

    # TODO: I still need to find out the real meaning of this property (waiting
    # for the clients anweser
    frame_mha = DecimalCol()

    #: The brige is the part of the frame between the two lenses, above the nose.
    frame_bridge = DecimalCol()

    #
    # Left eye distance vision
    #

    le_distance_spherical = DecimalCol(default=0)
    le_distance_cylindrical = DecimalCol(default=0)
    le_distance_axis = DecimalCol(default=0)
    le_distance_prism = DecimalCol(default=0)
    le_distance_base = DecimalCol(default=0)
    le_distance_height = DecimalCol(default=0)

    #: Pupil distance (DNP in pt_BR)
    le_distance_pd = DecimalCol(default=0)
    le_addition = DecimalCol(default=0)

    #
    # Left eye distance vision
    #
    le_near_spherical = DecimalCol(default=0)
    le_near_cylindrical = DecimalCol(default=0)
    le_near_axis = DecimalCol(default=0)

    #: Pupil distance (DNP in pt_BR)
    le_near_pd = DecimalCol(default=0)

    #
    # Right eye distance vision
    #

    re_distance_spherical = DecimalCol(default=0)
    re_distance_cylindrical = DecimalCol(default=0)
    re_distance_axis = DecimalCol(default=0)
    re_distance_prism = DecimalCol(default=0)
    re_distance_base = DecimalCol(default=0)
    re_distance_height = DecimalCol(default=0)

    #: Pupil distance (DNP in pt_BR)
    re_distance_pd = DecimalCol(default=0)
    re_addition = DecimalCol(default=0)

    #
    # Right eye near vision
    #
    re_near_spherical = DecimalCol(default=0)
    re_near_cylindrical = DecimalCol(default=0)
    re_near_axis = DecimalCol(default=0)

    #: Pupil distance (DNP in pt_BR)
    re_near_pd = DecimalCol(default=0)