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, '')
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()
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)
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
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
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)
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)