class WorkOrderPackage(Domain): """A package of |workorders| This is a package (called 'malote' on Brazil) that will be used to send workorder(s) to another branch for the task execution. .. graphviz:: digraph work_order_package_status { STATUS_OPENED -> STATUS_SENT; STATUS_SENT -> STATUS_RECEIVED; } See also: `schema <http://doc.stoq.com.br/schema/tables/work_order_package.html>`__ """ __storm_table__ = 'work_order_package' #: package is opened, waiting to be sent STATUS_OPENED = 0 #: package was sent to the :attr:`.destination_branch` STATUS_SENT = 1 #: package was received by the :attr:`.destination_branch` STATUS_RECEIVED = 2 statuses = { STATUS_OPENED: _(u'Opened'), STATUS_SENT: _(u'Sent'), STATUS_RECEIVED: _(u'Received') } status = IntCol(allow_none=False, default=STATUS_OPENED) # FIXME: Change identifier to another name, to avoid # confusions with IdentifierCol used elsewhere #: the packages's identifier identifier = UnicodeCol() #: when the package was sent from the :attr:`.source_branch` send_date = DateTimeCol() #: when the package was received by the :attr:`.destination_branch` receive_date = DateTimeCol() send_responsible_id = IntCol(default=None) #: the |loginuser| responsible for sending the package send_responsible = Reference(send_responsible_id, 'LoginUser.id') receive_responsible_id = IntCol(default=None) #: the |loginuser| responsible for receiving the package receive_responsible = Reference(receive_responsible_id, 'LoginUser.id') destination_branch_id = IntCol(validator=_validate_package_branch) #: the destination branch, that is, the branch where #: the package is going to be sent to destination_branch = Reference(destination_branch_id, 'Branch.id') source_branch_id = IntCol(allow_none=False, validator=_validate_package_branch) #: the source branch, that is, the branch where #: the package is leaving source_branch = Reference(source_branch_id, 'Branch.id') #: the |workorderpackageitems| inside this package package_items = ReferenceSet('id', 'WorkOrderPackageItem.package_id') @property def quantity(self): """The quantity of |workorderpackageitems| inside this package""" return self.package_items.count() # # Public API # def add_order(self, workorder): """Add a |workorder| on this package :returns: the created |workorderpackageitem| """ if workorder.current_branch != self.source_branch: raise ValueError( _("The order %s is not in the source branch") % (workorder, )) if not self.package_items.find(order=workorder).is_empty(): raise ValueError( _("The order %s is already on the package %s") % (workorder, self)) return WorkOrderPackageItem(store=self.store, order=workorder, package=self) def can_send(self): """If we can send this package to the :attr:`.destination_branch`""" return self.status == self.STATUS_OPENED def can_receive(self): """If we can receive this package in the :attr:`.destination_branch`""" return self.status == self.STATUS_SENT def send(self): """Send the package to the :attr:`.destination_branch` This will mark the package as sent. Note that it's only possible to call this on the same branch as :attr:`.source_branch`. When calling this, the work orders' :attr:`WorkOrder.current_branch` will be ``None``, since they are on a package and not on any branch. """ assert self.can_send() if self.source_branch != get_current_branch(self.store): raise ValueError( _("This package's source branch is %s and you are in %s. " "It's not possible to send a package outside the " "source branch") % (self.source_branch, get_current_branch(self.store))) workorders = [item.order for item in self.package_items] if not len(workorders): raise ValueError(_("There're no orders to send")) for order in workorders: assert order.current_branch == self.source_branch # The order is going to leave the current_branch order.current_branch = None self.send_date = localnow() self.status = self.STATUS_SENT def receive(self): """Receive the package on the :attr:`.destination_branch` This will mark the package as received in the branch to receive it there. Note that it's only possible to call this on the same branch as :attr:`.destination_branch`. When calling this, the work orders' :attr:`WorkOrder.current_branch` will be set to :attr:`.destination_branch`, since receiving means they got to their destination. """ assert self.can_receive() if self.destination_branch != get_current_branch(self.store): raise ValueError( _("This package's destination branch is %s and you are in %s. " "It's not possible to receive a package outside the " "destination branch") % (self.destination_branch, get_current_branch(self.store))) for order in [item.order for item in self.package_items]: assert order.current_branch is None # The order is in destination branch now order.current_branch = self.destination_branch self.receive_date = localnow() self.status = self.STATUS_RECEIVED
class ProductionOrder(Domain): """Production Order object implementation. """ __storm_table__ = 'production_order' #: The production order is opened, production items might have been added. ORDER_OPENED = u'opened' #: The production order is waiting some conditions to start the #: manufacturing process. ORDER_WAITING = u'waiting' #: The production order have already started. ORDER_PRODUCING = u'producing' #: The production is in quality assurance phase. ORDER_QA = u'quality-assurance' #: The production have finished. ORDER_CLOSED = u'closed' #: Production cancelled ORDER_CANCELLED = u'cancelled' statuses = collections.OrderedDict([ (ORDER_OPENED, _(u'Opened')), (ORDER_WAITING, _(u'Waiting')), (ORDER_PRODUCING, _(u'Producing')), (ORDER_QA, _(u'Quality Assurance')), (ORDER_CLOSED, _(u'Closed')), (ORDER_CANCELLED, _(u'Cancelled')), ]) #: 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() #: the production order status status = EnumCol(allow_none=False, default=ORDER_OPENED) #: the date when the production order was created open_date = DateTimeCol(default_factory=localnow) #: the date when the production order have been closed close_date = DateTimeCol(default=None) #: the date when the production order have been cancelled cancel_date = DateTimeCol(default=None) #: the production order description description = UnicodeCol(default=u'') expected_start_date = DateTimeCol(default=None) start_date = DateTimeCol(default=None) responsible_id = IdCol(default=None) #: the person responsible for the production order responsible = Reference(responsible_id, 'Employee.id') branch_id = IdCol() #: branch this production belongs to branch = Reference(branch_id, 'Branch.id') produced_items = ReferenceSet('id', 'ProductionProducedItem.order_id') # # IContainer implmentation # def get_items(self): return self.store.find(ProductionItem, order=self) def add_item(self, sellable, quantity=Decimal(1)): return ProductionItem(order=self, product=sellable.product, quantity=quantity, store=self.store) def remove_item(self, item): assert isinstance(item, ProductionItem) if item.order is not self: raise ValueError( _(u'Argument item must have an order attribute ' u'associated with the current production ' u'order instance.')) item.order = None self.store.maybe_remove(item) # # Public API # def can_cancel(self): """Checks if this order can be cancelled Only orders that didn't start yet can be canceled, this means only opened and waiting productions. """ return self.status in [self.ORDER_OPENED, self.ORDER_WAITING] def can_finalize(self): """Checks if this order can be finalized Only orders that didn't start yet can be canceled, this means only producing and waiting qa productions. """ return self.status in [self.ORDER_PRODUCING, self.ORDER_QA] def get_service_items(self): """Returns all the services needed by this production. :returns: a sequence of :class:`ProductionService` instances. """ return self.store.find(ProductionService, order=self) def remove_service_item(self, item): assert isinstance(item, ProductionService) if item.order is not self: raise ValueError( _(u'Argument item must have an order attribute ' u'associated with the current production ' u'order instance.')) item.order = None self.store.maybe_remove(item) def get_material_items(self): """Returns all the material needed by this production. :returns: a sequence of :class:`ProductionMaterial` instances. """ return self.store.find( ProductionMaterial, order=self, ) def start_production(self): """Start the production by allocating all the material needed. """ assert self.status in [ ProductionOrder.ORDER_OPENED, ProductionOrder.ORDER_WAITING ] for material in self.get_material_items(): material.allocate() self.start_date = localtoday() self.status = ProductionOrder.ORDER_PRODUCING def cancel(self): """Cancel the production when this is Open or Waiting. """ assert self.can_cancel() self.status = self.ORDER_CANCELLED self.cancel_date = localtoday() def is_completely_produced(self): return all(i.is_completely_produced() for i in self.get_items()) def is_completely_tested(self): # Produced items are only stored if there are quality tests for this # product produced_items = list(self.produced_items) if not produced_items: return True return all([item.test_passed for item in produced_items]) def try_finalize_production(self, ignore_completion=False): """When all items are completely produced, change the status of the production to CLOSED. """ assert self.can_finalize(), self.status if ignore_completion: is_produced = True else: is_produced = self.is_completely_produced() is_tested = self.is_completely_tested() if is_produced and not is_tested: # Fully produced but not fully tested. Keep status as QA self.status = ProductionOrder.ORDER_QA elif is_produced and is_tested: # All items must be completely produced and tested self.close_date = localtoday() self.status = ProductionOrder.ORDER_CLOSED # If the order is closed, return the the remaining allocated material to # the stock if self.status == ProductionOrder.ORDER_CLOSED: # Return remaining allocated material to the stock for m in self.get_material_items(): m.return_remaining() # Increase the stock for the produced items for p in self.produced_items: p.send_to_stock() def set_production_waiting(self): assert self.status == ProductionOrder.ORDER_OPENED self.status = ProductionOrder.ORDER_WAITING def get_status_string(self): return ProductionOrder.statuses[self.status] def get_branch_name(self): return self.branch.get_description() def get_responsible_name(self): if self.responsible is not None: return self.responsible.person.name return u'' # # IDescribable implementation # def get_description(self): return self.description
class ECFPrinter(Domain): """ @param model: @param brand: @param device_name: @param device_serial: @param station: @param is_active: @param constants: @param baudrate: @cvar last_sale: reference for the last Sale @cvar last_till_entry: reference for the last TillEntry @cvar user_number: the current registrer user in the printer @cvar register_date: when the current user was registred @cvar register_cro: cro when the user was registred """ __storm_table__ = 'ecf_printer' model = UnicodeCol() brand = UnicodeCol() device_name = UnicodeCol() device_serial = UnicodeCol() station_id = IdCol() station = Reference(station_id, 'BranchStation.id') is_active = BoolCol(default=True) baudrate = IntCol() last_sale_id = IdCol(default=None) last_sale = Reference(last_sale_id, 'Sale.id') last_till_entry_id = IdCol(default=None) last_till_entry = Reference(last_till_entry_id, 'TillEntry.id') user_number = IntCol(default=None) register_date = DateTimeCol(default=None) register_cro = IntCol(default=None) constants = ReferenceSet('id', 'DeviceConstant.printer_id') # # Public API # def create_fiscal_printer_constants(self): """ Creates constants for a fiscal printer This can be called multiple times """ # We only want to populate 'empty' objects. if not self.constants.find().is_empty(): return store = self.store driver = self.get_fiscal_driver() constants = driver.get_constants() for constant in constants.get_items(): constant_value = None if isinstance(constant, PaymentMethodType): constant_type = DeviceConstant.TYPE_PAYMENT elif isinstance(constant, UnitType): constant_type = DeviceConstant.TYPE_UNIT else: continue DeviceConstant(constant_type=constant_type, constant_name=unicode(describe_constant(constant)), constant_value=constant_value, constant_enum=int(constant), device_value=constants.get_value(constant, None), printer=self, store=store) for constant, device_value, value in driver.get_tax_constants(): # FIXME: Looks like this is not used and/or is duplicating code from # ecfpriterdialog.py (_populate_constants) if constant == TaxType.CUSTOM: constant_name = '%0.2f %%' % value else: constant_name = describe_constant(constant) DeviceConstant(constant_type=DeviceConstant.TYPE_TAX, constant_name=unicode(constant_name), constant_value=value, constant_enum=int(constant), device_value=device_value, printer=self, store=store) def get_constants_by_type(self, constant_type): """ Fetchs a list of constants for the current ECFPrinter object. @param constant_type: type of constant @type constant_type: :class:`DeviceConstant` @returns: list of constants """ return self.store.find(DeviceConstant, printer=self, constant_type=constant_type) def get_payment_constant(self, payment): """ @param payment: the payment whose method we will lookup the constant @returns: the payment constant @rtype: :class:`DeviceConstant` """ constant_enum = payment.method.operation.get_constant(payment) if constant_enum is None: raise AssertionError return self.store.find(DeviceConstant, printer=self, constant_type=DeviceConstant.TYPE_PAYMENT, constant_enum=int(constant_enum)).one() def get_tax_constant_for_device(self, sellable): """ Returns a tax_constant for a device Raises DeviceError if a constant is not found @param sellable: sellable which has the tax codes @type sellable: :class:`stoqlib.domain.sellable.Sellable` @returns: the tax constant @rtype: :class:`DeviceConstant` """ sellable_constant = sellable.get_tax_constant() if sellable_constant is None: raise DeviceError("No tax constant set for sellable %r" % sellable) store = self.store if sellable_constant.tax_type == TaxType.CUSTOM: constant = DeviceConstant.get_custom_tax_constant( self, sellable_constant.tax_value, store) if constant is None: raise DeviceError(_( "fiscal printer is missing a constant for the custom " "tax constant '%s'") % (sellable_constant.description, )) else: constant = DeviceConstant.get_tax_constant( self, sellable_constant.tax_type, store) if constant is None: raise DeviceError(_( "fiscal printer is missing a constant for tax " "constant '%s'") % (sellable_constant.description, )) return constant def get_fiscal_driver(self): if self.brand == 'virtual': port = VirtualPort() else: port = SerialPort(device=self.device_name, baudrate=self.baudrate) return FiscalPrinter(brand=self.brand, model=self.model, port=port) def set_user_info(self, user_info): self.user_number = user_info.user_number self.register_cro = user_info.cro self.register_date = user_info.register_date # # IActive implementation # def inactivate(self): self.is_active = False def activate(self): self.is_active = True def get_status_string(self): if self.is_active: return _(u'Active') return _(u'Inactive') # # IDescribable implementation # def get_description(self): # Quick workaround to avoid calling FiscalPrinter.setup(), since that # may send commands to the ECF, and we just need the description. # TODO: Improve stoqdrivers so we can get this easyer port = VirtualPort() driver = BasePrinter(brand=self.brand, model=self.model, port=port) return driver.get_model_name() @classmethod def get_last_document(cls, station, store): return store.find(cls, station=station, is_active=True).one()
class Sellable(Domain): """ Sellable information of a certain item such a |product| or a |service|. See also: `schema <http://doc.stoq.com.br/schema/tables/sellable.html>`__ """ __storm_table__ = 'sellable' #: the sellable is available and can be used on a |purchase|/|sale| STATUS_AVAILABLE = u'available' #: the sellable is closed, that is, it still exists for references, #: but it should not be possible to create a |purchase|/|sale| with it STATUS_CLOSED = u'closed' statuses = collections.OrderedDict([ (STATUS_AVAILABLE, _(u'Available')), (STATUS_CLOSED, _(u'Closed')), ]) #: a code used internally by the shop to reference this sellable. #: It is usually not printed and displayed to |clients|, barcode is for that. #: It may be used as an shorter alternative to the barcode. code = UnicodeCol(default=u'', validator=_validate_code) #: barcode, mostly for products, usually printed and attached to the #: package. barcode = UnicodeCol(default=u'', validator=_validate_barcode) #: status the sellable is in status = EnumCol(allow_none=False, default=STATUS_AVAILABLE) #: cost of the sellable, this is not tied to a specific |supplier|, #: which may have a different cost. This can also be the production cost of #: manufactured item by the company. cost = PriceCol(default=0) #: price of sellable, how much the |client| paid. base_price = PriceCol(default=0) #: the last time the cost was updated cost_last_updated = DateTimeCol(default_factory=localnow) #: the last time the price was updated price_last_updated = DateTimeCol(default_factory=localnow) #: full description of sellable description = UnicodeCol(default=u'') #: maximum discount allowed max_discount = PercentCol(default=0) #: commission to pay after selling this sellable commission = PercentCol(default=0) #: A sort order to override default alphabetic order in lists. sort_order = IntCol() #: If this is a favorite sellable favorite = BoolCol() #: Some keywords for this sellable. keywords = UnicodeCol() #: notes for the sellable notes = UnicodeCol(default=u'') unit_id = IdCol(default=None) #: the |sellableunit|, quantities of this sellable are in this unit. unit = Reference(unit_id, 'SellableUnit.id') category_id = IdCol(default=None) #: a reference to category table category = Reference(category_id, 'SellableCategory.id') tax_constant_id = IdCol(default=None) #: the |sellabletaxconstant|, this controls how this sellable is taxed tax_constant = Reference(tax_constant_id, 'SellableTaxConstant.id') #: the |product| for this sellable or ``None`` product = Reference('id', 'Product.id', on_remote=True) #: the |service| for this sellable or ``None`` service = Reference('id', 'Service.id', on_remote=True) #: the |storable| for this |product|'s sellable product_storable = Reference('id', 'Storable.id', on_remote=True) default_sale_cfop_id = IdCol(default=None) #: the default |cfop| that will be used when selling this sellable default_sale_cfop = Reference(default_sale_cfop_id, 'CfopData.id') #: A special price used when we have a "on sale" state, this #: can be used for promotions on_sale_price = PriceCol(default=0) #: When the promotional/special price starts to apply on_sale_start_date = DateTimeCol(default=None) #: When the promotional/special price ends on_sale_end_date = DateTimeCol(default=None) #: This sellable's images images = ReferenceSet('id', 'Image.sellable_id') #: specifies whether the product requires kitchen production requires_kitchen_production = BoolCol(default=False) def __init__(self, store=None, category=None, cost=None, commission=None, description=None, price=None): """Creates a new sellable :param store: a store :param category: category of this sellable :param cost: the cost, defaults to 0 :param commission: commission for this sellable :param description: readable description of the sellable :param price: the price, defaults to 0 """ Domain.__init__(self, store=store) if category: if commission is None: commission = category.get_commission() if price is None and cost is not None: markup = category.get_markup() price = self._get_price_by_markup(markup, cost=cost) self.category = category self.commission = commission or currency(0) self.cost = cost or currency(0) self.description = description self.price = price or currency(0) # # Helper methods # def _get_price_by_markup(self, markup, cost=None): if cost is None: cost = self.cost return currency(quantize(cost + (cost * (markup / currency(100))))) # # Properties # @property def status_str(self): """The sellable status as a string""" return self.statuses[self.status] @property def unit_description(self): """Returns the description of the |sellableunit| of this sellable :returns: the unit description or an empty string if no |sellableunit| was set. :rtype: unicode """ return self.unit and self.unit.description or u"" @property def image(self): """This sellable's main image.""" # FIXME: Should we use .first() here? What will happen if there are # more than one image with "is_main" flag set to True? There's no way # to prevent that in the database return self.images.find(is_main=True).one() @property def markup(self): """Markup, the opposite of discount, a value added on top of the sale. It's calculated as:: ((cost/price)-1)*100 """ if self.cost == 0: return Decimal(0) return ((self.price / self.cost) - 1) * 100 @markup.setter def markup(self, markup): self.price = self._get_price_by_markup(markup) @property def price(self): if self.is_on_sale(): return self.on_sale_price else: category = sysparam.get_object(self.store, 'DEFAULT_TABLE_PRICE') if category: info = self.get_category_price_info(category) if info: return info.price return self.base_price @price.setter def price(self, price): if price < 0: # Just a precaution for gui validation fails. price = 0 if self.is_on_sale(): self.on_sale_price = price else: self.base_price = price # # Accessors # def is_available(self): """Whether the sellable is available and can be sold. :returns: ``True`` if the item can be sold, ``False`` otherwise. """ # FIXME: Perhaps this should be done elsewhere. Johan 2008-09-26 if sysparam.compare_object('DELIVERY_SERVICE', self.service): return True return self.status == self.STATUS_AVAILABLE def set_available(self): """Mark the sellable as available Being available means that it can be ordered or sold. :raises: :exc:`ValueError`: if the sellable is already available """ if self.is_available(): raise ValueError('This sellable is already available') self.status = self.STATUS_AVAILABLE def is_closed(self): """Whether the sellable is closed or not. :returns: ``True`` if closed, ``False`` otherwise. """ return self.status == Sellable.STATUS_CLOSED def close(self): """Mark the sellable as closed. After the sellable is closed, this will call the close method of the service or product related to this sellable. :raises: :exc:`ValueError`: if the sellable is already closed """ if self.is_closed(): raise ValueError('This sellable is already closed') assert self.can_close() self.status = Sellable.STATUS_CLOSED obj = self.service or self.product obj.close() def can_remove(self): """Whether we can delete this sellable from the database. ``False`` if the product/service was used in some cases below:: - Sold or received - The |product| is in a |purchase| """ if self.product and not self.product.can_remove(): return False if self.service and not self.service.can_remove(): return False return super(Sellable, self).can_remove( skip=[('product', 'id'), ('service', 'id'), ( 'image', 'sellable_id'), ('client_category_price', 'sellable_id')]) def can_close(self): """Whether we can close this sellable. :returns: ``True`` if the product has no stock left or the service is not required by the system (i.e. Delivery service is required). ``False`` otherwise. """ obj = self.service or self.product return obj.can_close() def get_commission(self): return self.commission def get_suggested_markup(self): """Returns the suggested markup for the sellable :returns: suggested markup :rtype: decimal """ return self.category and self.category.get_markup() def get_category_description(self): """Returns the description of this sellables category If it's unset, return the constant from the category, if any :returns: sellable category description or an empty string if no |sellablecategory| was set. :rtype: unicode """ category = self.category return category and category.description or u"" def get_tax_constant(self): """Returns the |sellabletaxconstant| for this sellable. If it's unset, return the constant from the category, if any :returns: the |sellabletaxconstant| or ``None`` if unset """ if self.tax_constant: return self.tax_constant if self.category: return self.category.get_tax_constant() def get_category_prices(self): """Returns all client category prices associated with this sellable. :returns: the client category prices """ return self.store.find(ClientCategoryPrice, sellable=self) def get_category_price_info(self, category): """Returns the :class:`ClientCategoryPrice` information for the given :class:`ClientCategory` and this |sellable|. :returns: the :class:`ClientCategoryPrice` or ``None`` """ info = self.store.find(ClientCategoryPrice, sellable=self, category=category).one() return info def get_price_for_category(self, category): """Given the |clientcategory|, returns the price for that category or the default sellable price. :param category: a |clientcategory| :returns: The value that should be used as a price for this sellable. """ info = self.get_category_price_info(category) if info: return info.price return self.price def get_maximum_discount(self, category=None, user=None): user_discount = user.profile.max_discount if user else 0 if category is not None: info = self.get_category_price_info(category) or self else: info = self return Decimal(max(user_discount, info.max_discount)) def get_requires_kitchen_production(self, branch): """Check if a sellable requires kitchen production :param branch: branch for checking if there is a sellable_branch_override :returns: Whether the sellable requires kitchen production for a given branch """ # Check for overrides before checking the actual sellable sellable_override = SellableBranchOverride.find_by_sellable( sellable=self, branch=branch) if sellable_override and sellable_override.requires_kitchen_production is not None: return sellable_override.requires_kitchen_production return self.requires_kitchen_production def check_code_exists(self, code): """Check if there is another sellable with the same code. :returns: ``True`` if we already have a sellable with the given code ``False`` otherwise. """ return self.check_unique_value_exists(Sellable.code, code) def check_barcode_exists(self, barcode): """Check if there is another sellable with the same barcode. :returns: ``True`` if we already have a sellable with the given barcode ``False`` otherwise. """ return self.check_unique_value_exists(Sellable.barcode, barcode) def check_taxes_validity(self, branch): """Check if icms taxes are valid. This check is done because some icms taxes (such as CSOSN 101) have a 'valid until' field on it. If these taxes has expired, we cannot sell the sellable. Check this method using assert inside a try clause. :raises: :exc:`TaxError` if there are any issues with the sellable taxes. """ icms_template = self.product and self.product.get_icms_template(branch) SellableCheckTaxesEvent.emit(self, branch) if not icms_template: return elif not icms_template.p_cred_sn: return elif not icms_template.is_p_cred_sn_valid(): # Translators: ICMS tax rate credit = Alíquota de crédito do ICMS raise TaxError( _("You cannot sell this item before updating " "the 'ICMS tax rate credit' field on '%s' " "Tax Class.\n" "If you don't know what this means, contact " "the system administrator.") % icms_template.product_tax_template.name) def is_on_sale(self): """Check if the price is currently on sale. :return: ``True`` if it is on sale, ``False`` otherwise """ if not self.on_sale_price: return False return is_date_in_interval(localnow(), self.on_sale_start_date, self.on_sale_end_date) def is_valid_quantity(self, new_quantity): """Whether the new quantity is valid for this sellable or not. If the new quantity is fractioned, check on this sellable unit if it allows fractioned quantities. If not, this new quantity cannot be used. Note that, if the sellable lacks a unit, we will not allow fractions either. :returns: ``True`` if new quantity is Ok, ``False`` otherwise. """ if self.unit and not self.unit.allow_fraction: return not bool(new_quantity % 1) return True def is_valid_price(self, newprice, category=None, user=None, extra_discount=None): """Checks if *newprice* is valid for this sellable Returns a dict indicating whether the new price is a valid price as allowed by the discount by the user, by the category or by the sellable maximum discount :param newprice: The new price that we are trying to sell this sellable for :param category: Optionally define a |clientcategory| that we will get the price info from :param user: The user role may allow a different discount percentage. :param extra_discount: some extra discount for the sellable to be considered for the min_price :returns: A dict with the following keys: * is_valid: ``True`` if the price is valid, else ``False`` * min_price: The minimum price for this sellable. * max_discount: The maximum discount for this sellable. """ if category is not None: info = self.get_category_price_info(category) or self else: info = self max_discount = self.get_maximum_discount(category=category, user=user) min_price = info.price * (1 - max_discount / 100) if extra_discount is not None: # The extra discount can be greater than the min_price, and # a negative min_price doesn't make sense min_price = max(currency(0), min_price - extra_discount) return { 'is_valid': newprice >= min_price, 'min_price': min_price, 'max_discount': max_discount, } def copy_sellable(self, target=None): """This method copies self to another sellable If the |sellable| target is None, a new sellable is created. :param target: The |sellable| target for the copy returns: a |sellable| identical to self """ if target is None: target = Sellable(store=self.store) props = [ 'base_price', 'category_id', 'cost', 'max_discount', 'commission', 'notes', 'unit_id', 'tax_constant_id', 'default_sale_cfop_id', 'on_sale_price', 'on_sale_start_date', 'on_sale_end_date' ] for prop in props: value = getattr(self, prop) setattr(target, prop, value) return target # # IDescribable implementation # def get_description(self, full_description=False): desc = self.description if full_description and self.get_category_description(): desc = u"[%s] %s" % (self.get_category_description(), desc) return desc # # Domain hooks # def on_update(self): obj = self.product or self.service obj.on_update() def on_object_changed(self, attr, old_value, value): if attr == 'cost': self.cost_last_updated = localnow() if self.product: self.product.update_product_cost(value) elif attr == 'base_price': self.price_last_updated = localnow() # # Classmethods # def remove(self): """ Remove this sellable. This will also remove the |product| or |sellable| and |categoryprice| """ assert self.can_remove() # Remove category price before delete the sellable. category_prices = self.get_category_prices() for category_price in category_prices: category_price.remove() for image in self.images: self.store.remove(image) if self.product: self.product.remove() elif self.service: self.service.remove() self.store.remove(self) @classmethod def get_available_sellables_query(cls, store): """Get the sellables that are available and can be sold. For instance, this will filter out the internal sellable used by a |delivery|. This is similar to `.get_available_sellables`, but it returns a query instead of the actual results. :param store: a store :returns: a query expression """ delivery = sysparam.get_object(store, 'DELIVERY_SERVICE') return And(cls.id != delivery.sellable.id, cls.status == cls.STATUS_AVAILABLE) @classmethod def get_available_sellables(cls, store): """Get the sellables that are available and can be sold. For instance, this will filter out the internal sellable used by a |delivery|. :param store: a store :returns: a resultset with the available sellables """ query = cls.get_available_sellables_query(store) return store.find(cls, query) @classmethod def get_unblocked_sellables_query(cls, store, storable=False, supplier=None, consigned=False): """Helper method for get_unblocked_sellables When supplier is not ```None``, you should use this query only with Viewables that join with supplier, like ProductFullStockSupplierView. :param store: a store :param storable: if ``True``, we should filter only the sellables that are also a |storable|. :param supplier: |supplier| to filter on or ``None`` :param consigned: if the sellables are consigned :returns: a query expression """ from stoqlib.domain.product import Product, ProductSupplierInfo query = And(cls.get_available_sellables_query(store), cls.id == Product.id, Product.consignment == consigned) if storable: from stoqlib.domain.product import Storable query = And(query, Sellable.id == Product.id, Storable.id == Product.id) if supplier: query = And(query, Sellable.id == Product.id, Product.id == ProductSupplierInfo.product_id, ProductSupplierInfo.supplier_id == supplier.id) return query @classmethod def get_unblocked_sellables(cls, store, storable=False, supplier=None, consigned=False): """ Returns unblocked sellable objects, which means the available sellables plus the sold ones. :param store: a store :param storable: if `True`, only return sellables that also are |storable| :param supplier: a |supplier| or ``None``, if set limit the returned object to this |supplier| :rtype: queryset of sellables """ query = cls.get_unblocked_sellables_query(store, storable, supplier, consigned) return store.find(cls, query) @classmethod def get_unblocked_by_categories_query(cls, store, categories, include_uncategorized=True): """Returns the available sellables by a list of categories. :param store: a store :param categories: a list of SellableCategory instances :param include_uncategorized: whether or not include the sellables without a category :rtype: generator of sellables """ queries = [] if len(categories): queries.append(In(Sellable.category_id, [c.id for c in categories])) if include_uncategorized: queries.append(Eq(Sellable.category_id, None)) query = cls.get_unblocked_sellables_query(store) return And(query, Or(*queries))
class TransferOrder(Domain): """ Transfer Order class """ __storm_table__ = 'transfer_order' STATUS_PENDING = u'pending' STATUS_SENT = u'sent' STATUS_RECEIVED = u'received' statuses = { STATUS_PENDING: _(u'Pending'), STATUS_SENT: _(u'Sent'), STATUS_RECEIVED: _(u'Received') } status = EnumCol(default=STATUS_PENDING) #: 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() #: The date the order was created open_date = DateTimeCol(default_factory=localnow) #: The date the order was received receival_date = DateTimeCol() #: The invoice number of the transfer invoice_number = IntCol() #: Comments of a transfer comments = UnicodeCol() source_branch_id = IdCol() #: The |branch| sending the stock source_branch = Reference(source_branch_id, 'Branch.id') destination_branch_id = IdCol() #: The |branch| receiving the stock destination_branch = Reference(destination_branch_id, 'Branch.id') source_responsible_id = IdCol() #: The |employee| responsible for the |transfer| at source |branch| source_responsible = Reference(source_responsible_id, 'Employee.id') destination_responsible_id = IdCol() #: The |employee| responsible for the |transfer| at destination |branch| destination_responsible = Reference(destination_responsible_id, 'Employee.id') #: |payments| generated by this transfer payments = None #: |transporter| used in transfer transporter = None invoice_id = IdCol() #: The |invoice| generated by the transfer invoice = Reference(invoice_id, 'Invoice.id') def __init__(self, store=None, **kwargs): kwargs['invoice'] = Invoice(store=store, invoice_type=Invoice.TYPE_OUT) super(TransferOrder, self).__init__(store=store, **kwargs) # # IContainer implementation # def get_items(self): return self.store.find(TransferOrderItem, transfer_order=self) def add_item(self, item): assert self.status == self.STATUS_PENDING item.transfer_order = self def remove_item(self, item): if item.transfer_order is not self: raise ValueError( _('The item does not belong to this ' 'transfer order')) item.transfer_order = None self.store.maybe_remove(item) # # IInvoice implementation # @property def discount_value(self): return currency(0) @property def invoice_subtotal(self): subtotal = self.get_items().sum(TransferOrderItem.quantity * TransferOrderItem.stock_cost) return currency(subtotal) @property def invoice_total(self): return self.invoice_subtotal @property def recipient(self): return self.destination_branch.person @property def operation_nature(self): # TODO: Save the operation nature in new transfer_order table field return _(u"Transfer") # # Public API # @property def branch(self): return self.source_branch @property def status_str(self): return (self.statuses[self.status]) def add_sellable(self, sellable, batch, quantity=1, cost=None): """Add the given |sellable| to this |transfer|. :param sellable: The |sellable| we are transfering :param batch: What |batch| of the storable (represented by sellable) we are transfering. :param quantity: The quantity of this product that is being transfered. """ assert self.status == self.STATUS_PENDING self.validate_batch(batch, sellable=sellable) product = sellable.product if product.manage_stock: stock_item = product.storable.get_stock_item( self.source_branch, batch) stock_cost = stock_item.stock_cost else: stock_cost = sellable.cost return TransferOrderItem(store=self.store, transfer_order=self, sellable=sellable, batch=batch, quantity=quantity, stock_cost=cost or stock_cost) def can_send(self): return (self.status == self.STATUS_PENDING and self.get_items().count() > 0) def can_receive(self): return self.status == self.STATUS_SENT def send(self): """Sends a transfer order to the destination branch. """ assert self.can_send() for item in self.get_items(): item.send() # Save invoice number, operation_nature and branch in Invoice table. self.invoice.invoice_number = self.invoice_number self.invoice.operation_nature = self.operation_nature self.invoice.branch = self.branch self.status = self.STATUS_SENT def receive(self, responsible, receival_date=None): """Confirms the receiving of the transfer order. """ assert self.can_receive() for item in self.get_items(): item.receive() self.receival_date = receival_date or localnow() self.destination_responsible = responsible self.status = self.STATUS_RECEIVED @classmethod def get_pending_transfers(cls, store, branch): """Get all the transfers that need to be recieved Get all transfers that have STATUS_SENT and the current branch as the destination This is useful if you want to list all the items that need to be recieved in a certain branch """ return store.find( cls, And(cls.status == cls.STATUS_SENT, cls.destination_branch == branch)) def get_source_branch_name(self): """Returns the source |branch| name""" return self.source_branch.get_description() def get_destination_branch_name(self): """Returns the destination |branch| name""" return self.destination_branch.get_description() def get_source_responsible_name(self): """Returns the name of the |employee| responsible for the transfer at source |branch| """ return self.source_responsible.person.name def get_destination_responsible_name(self): """Returns the name of the |employee| responsible for the transfer at destination |branch| """ if not self.destination_responsible: return u'' return self.destination_responsible.person.name def get_total_items_transfer(self): """Retuns the |transferitems| quantity """ return sum([item.quantity for item in self.get_items()], 0)
class ProductSupplierInfo(Domain): """Supplier information for a |product|. Each product can has more than one |supplier|. See also: `schema <http://doc.stoq.com.br/schema/tables/product_supplier_info.html>`__ """ __storm_table__ = 'product_supplier_info' #: the cost which helps the purchaser to define the main cost of a #: certain product. Each product can have multiple |suppliers| and for #: each |supplier| a base_cost is available. The purchaser in this case #: must decide how to define the main cost based in the base cost avarage #: of all suppliers. base_cost = PriceCol(default=0) notes = UnicodeCol(default=u'') #: if this object stores information for the main |supplier|. is_main_supplier = BoolCol(default=False) #: the number of days needed to deliver the product to purchaser. lead_time = IntCol(default=1) #: the minimum amount that we can buy from this supplier. minimum_purchase = QuantityCol(default=Decimal(1)) #: a Brazil-specific attribute that means 'Imposto sobre circulacao #: de mercadorias e prestacao de servicos' icms = PercentCol(default=0) supplier_id = IdCol() #: the |supplier| supplier = Reference(supplier_id, 'Supplier.id') product_id = IdCol() #: the |product| product = Reference(product_id, 'Product.id') #: the product code in the supplier supplier_code = UnicodeCol(default=u'') # # Auxiliary methods # def get_name(self): if self.supplier: return self.supplier.get_description() def get_lead_time_str(self): if self.lead_time > 1: day_str = _(u"Days") lead_time = self.lead_time else: day_str = _(u"Day") lead_time = self.lead_time or 0 return u"%d %s" % (lead_time, day_str)
class Ding(Domain): __storm_table__ = 'ding' int_field = IntCol(default=0) str_field = UnicodeCol(default=u'')
class ProfileSettings(Domain): __storm_table__ = 'profile_settings' app_dir_name = UnicodeCol() has_permission = BoolCol(default=False) user_profile_id = IdCol()
class DeviceSettings(Domain): __storm_table__ = 'device_settings' #: The type of this device (printer or scale) type = IntCol() #: The brand (maker) of this device brand = UnicodeCol() #: The model of the device model = UnicodeCol() #: The device name on the computer (either /dev/ttySX or COMX - Linux/Windows) device_name = UnicodeCol() #: The baudrate of the device baudrate = IntCol(default=9600) station_id = IdCol() #: The station this device is connected to. station = Reference(station_id, 'BranchStation.id') #: Is this device is active or not is_active = BoolCol(default=True) (SCALE_DEVICE, NON_FISCAL_PRINTER_DEVICE, CHEQUE_PRINTER_DEVICE) = range(1, 4) device_types = { SCALE_DEVICE: _(u'Scale'), NON_FISCAL_PRINTER_DEVICE: _(u'Non Fiscal Printer'), CHEQUE_PRINTER_DEVICE: _(u'Cheque Printer') } # # Domain # @property def station_name(self): return self.station.name @property def device_type_name(self): return self.describe_device_type(self.type) def get_printer_description(self): return u"%s %s" % (self.brand.capitalize(), self.model) def describe_device_type(self, type): return DeviceSettings.device_types[type] # XXX: Maybe stoqdrivers can implement a generic way to do this? def get_interface(self): """ Based on the column values instantiate the stoqdrivers interface for the device itself. """ if self.device_name == '/dev/null': interface = 'serial' port = VirtualPort() product_id = vendor_id = None elif self.device_name.startswith('usb:'): # USB device interface, vendor_id, product_id = self.device_name.split(':') vendor_id = int(vendor_id, 16) product_id = int(product_id, 16) port = None else: # Serial device interface = 'serial' port = SerialPort(device=self.device_name, baudrate=self.baudrate) product_id = vendor_id = None if self.type == DeviceSettings.CHEQUE_PRINTER_DEVICE: return ChequePrinter(brand=self.brand, model=self.model, port=port) elif self.type == DeviceSettings.NON_FISCAL_PRINTER_DEVICE: return NonFiscalPrinter(brand=self.brand, model=self.model, port=port, interface=interface, product_id=product_id, vendor_id=vendor_id) elif self.type == DeviceSettings.SCALE_DEVICE: return Scale(brand=self.brand, model=self.model, device=self.device_name, port=port) raise DatabaseInconsistency("The device type referred by this " "record (%r) is invalid, given %r." % (self, self.type)) def is_valid(self): return (all((self.model, self.device_name, self.brand, self.station)) and self.type in DeviceSettings.device_types) @classmethod def get_by_station_and_type(cls, store, station, type): """Fetch the settings for a specific station and type. Note that one station can have only one device of a given type. XXX: Currently, is_active is not really used. All added devices are active. :param store: a store :param station: a BranchStation instance :param type: device type """ return store.find(cls, station=station, type=type).one() @classmethod def get_scale_settings(cls, store): """ Get the scale device settings for the current station :param store: a store :returns: a :class:`DeviceSettings` object or None if there is none """ station = get_current_station(store) return cls.get_by_station_and_type(store, station, cls.SCALE_DEVICE) # # IActive implementation # def inactivate(self): self.is_active = False def activate(self): self.is_active = True def get_status_string(self): if self.is_active: return _(u'Active') return _(u'Inactive') @property def description(self): return self.get_printer_description()
class PurchaseOrder(Domain): """Purchase and order definition.""" __storm_table__ = 'purchase_order' ORDER_QUOTING = u'quoting' ORDER_PENDING = u'pending' ORDER_CONFIRMED = u'confirmed' ORDER_CONSIGNED = u'consigned' ORDER_CANCELLED = u'cancelled' ORDER_CLOSED = u'closed' statuses = collections.OrderedDict([ (ORDER_QUOTING, _(u'Quoting')), (ORDER_PENDING, _(u'Pending')), (ORDER_CONFIRMED, _(u'Confirmed')), (ORDER_CONSIGNED, _(u'Consigned')), (ORDER_CANCELLED, _(u'Cancelled')), (ORDER_CLOSED, _(u'Closed')), ]) FREIGHT_FOB = u'fob' FREIGHT_CIF = u'cif' freight_types = {FREIGHT_FOB: _(u'FOB'), FREIGHT_CIF: _(u'CIF')} #: 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() status = EnumCol(allow_none=False, default=ORDER_QUOTING) open_date = DateTimeCol(default_factory=localnow, allow_none=False) quote_deadline = DateTimeCol(default=None) expected_receival_date = DateTimeCol(default_factory=localnow) expected_pay_date = DateTimeCol(default_factory=localnow) receival_date = DateTimeCol(default=None) confirm_date = DateTimeCol(default=None) notes = UnicodeCol(default=u'') salesperson_name = UnicodeCol(default=u'') freight_type = EnumCol(allow_none=False, default=FREIGHT_FOB) expected_freight = PriceCol(default=0) surcharge_value = PriceCol(default=0) discount_value = PriceCol(default=0) consigned = BoolCol(default=False) supplier_id = IdCol() supplier = Reference(supplier_id, 'Supplier.id') branch_id = IdCol() branch = Reference(branch_id, 'Branch.id') transporter_id = IdCol(default=None) transporter = Reference(transporter_id, 'Transporter.id') responsible_id = IdCol() responsible = Reference(responsible_id, 'LoginUser.id') group_id = IdCol() group = Reference(group_id, 'PaymentGroup.id') # # IContainer Implementation # def get_items(self, with_children=True): """Get the items of the purchase order :param with_children: indicate if we should fetch children_items or not """ query = PurchaseItem.order == self if not with_children: query = And(query, Eq(PurchaseItem.parent_item_id, None)) return self.store.find(PurchaseItem, query) def remove_item(self, item): if item.order is not self: raise ValueError( _(u'Argument item must have an order attribute ' 'associated with the current purchase instance')) item.order = None self.store.maybe_remove(item) def add_item(self, sellable, quantity=Decimal(1), parent=None, cost=None): """Add a sellable to this purchase. If the sellable is part of a package (parent is not None), then the actual cost and quantity will be calculated based on how many items of this component is on the package. :param sellable: the sellable being added :param quantity: How many units of this sellable we are adding :param cost: The price being paid for this sellable :param parent: The parent of this sellable, incase of a package """ if cost is None: cost = sellable.cost if parent: component = parent.sellable.product.get_component(sellable) cost = cost / component.quantity quantity = quantity * component.quantity else: if sellable.product.is_package: # If this is a package, the cost will be calculated and updated by the # compoents of the package cost = Decimal('0') store = self.store return PurchaseItem(store=store, order=self, sellable=sellable, quantity=quantity, cost=cost, parent_item=parent) # # Properties # @property def discount_percentage(self): """Discount by percentage. Note that percentage must be added as an absolute value not as a factor like 1.05 = 5 % of surcharge The correct form is 'percentage = 3' for a discount of 3 %""" discount_value = self.discount_value if not discount_value: return currency(0) subtotal = self.purchase_subtotal assert subtotal > 0, (u'the subtotal should not be zero ' u'at this point') total = subtotal - discount_value percentage = (1 - total / subtotal) * 100 return quantize(percentage) @discount_percentage.setter def discount_percentage(self, value): self.discount_value = self._get_percentage_value(value) @property def surcharge_percentage(self): """Surcharge by percentage. Note that surcharge must be added as an absolute value not as a factor like 0.97 = 3 % of discount. The correct form is 'percentage = 3' for a surcharge of 3 %""" surcharge_value = self.surcharge_value if not surcharge_value: return currency(0) subtotal = self.purchase_subtotal assert subtotal > 0, (u'the subtotal should not be zero ' u'at this point') total = subtotal + surcharge_value percentage = ((total / subtotal) - 1) * 100 return quantize(percentage) @surcharge_percentage.setter def surcharge_percentage(self, value): self.surcharge_value = self._get_percentage_value(value) @property def payments(self): """Returns all valid payments for this purchase This will return a list of valid payments for this purchase, that is, all payments on the payment group that were not cancelled. If you need to get the cancelled too, use self.group.payments. :returns: a list of |payment| """ return self.group.get_valid_payments() # # Private # def _get_percentage_value(self, percentage): if not percentage: return currency(0) subtotal = self.purchase_subtotal percentage = Decimal(percentage) return subtotal * (percentage / 100) def _payback_paid_payments(self): paid_value = self.group.get_total_paid() # If we didn't pay anything yet, there is no need to create a payback. if not paid_value: return money = PaymentMethod.get_by_name(self.store, u'money') payment = money.create_payment( Payment.TYPE_IN, self.group, self.branch, paid_value, description=_(u'%s Money Returned for Purchase %s') % (u'1/1', self.identifier)) payment.set_pending() payment.pay() # # Public API # def is_paid(self): for payment in self.payments: if not payment.is_paid(): return False return True def can_cancel(self): """Find out if it's possible to cancel the order :returns: True if it's possible to cancel the order, otherwise False """ # FIXME: Canceling partial orders disabled until we fix bug 3282 for item in self.get_items(): if item.has_partial_received(): return False return self.status in [ self.ORDER_QUOTING, self.ORDER_PENDING, self.ORDER_CONFIRMED ] def can_close(self): """Find out if it's possible to close the order :returns: True if it's possible to close the order, otherwise False """ # Consigned orders can be closed only after being confirmed if self.status == self.ORDER_CONSIGNED: return False for item in self.get_items(): if not item.has_been_received(): return False return True def confirm(self, confirm_date=None): """Confirms the purchase order :param confirm_data: optional, datetime """ if confirm_date is None: confirm_date = TransactionTimestamp() if self.status not in [ PurchaseOrder.ORDER_PENDING, PurchaseOrder.ORDER_CONSIGNED ]: fmt = _(u'Invalid order status, it should be ' u'ORDER_PENDING or ORDER_CONSIGNED, got %s') raise ValueError(fmt % (self.status_str, )) # In consigned purchases there is no payments at this point. if self.status != PurchaseOrder.ORDER_CONSIGNED: for payment in self.payments: payment.set_pending() if self.supplier: self.group.recipient = self.supplier.person self.responsible = get_current_user(self.store) self.status = PurchaseOrder.ORDER_CONFIRMED self.confirm_date = confirm_date Event.log( self.store, Event.TYPE_ORDER, _(u"Order %s, total value %2.2f, supplier '%s' " u"is now confirmed") % (self.identifier, self.purchase_total, self.supplier.person.name)) def set_consigned(self): if self.status != PurchaseOrder.ORDER_PENDING: raise ValueError( _(u'Invalid order status, it should be ' u'ORDER_PENDING, got %s') % (self.status_str, )) self.responsible = get_current_user(self.store) self.status = PurchaseOrder.ORDER_CONSIGNED def close(self): """Closes the purchase order """ if self.status != PurchaseOrder.ORDER_CONFIRMED: raise ValueError( _(u'Invalid status, it should be confirmed ' u'got %s instead') % self.status_str) self.status = self.ORDER_CLOSED Event.log( self.store, Event.TYPE_ORDER, _(u"Order %s, total value %2.2f, supplier '%s' " u"is now closed") % (self.identifier, self.purchase_total, self.supplier.person.name)) def cancel(self): """Cancels the purchase order """ assert self.can_cancel() # we have to cancel the payments too self._payback_paid_payments() self.group.cancel() self.status = self.ORDER_CANCELLED def receive_item(self, item, quantity_to_receive): if not item in self.get_pending_items(): raise StoqlibError( _(u'This item is not pending, hence ' u'cannot be received')) quantity = item.quantity - item.quantity_received if quantity < quantity_to_receive: raise StoqlibError( _(u'The quantity that you want to receive ' u'is greater than the total quantity of ' u'this item %r') % item) self.increase_quantity_received(item, quantity_to_receive) def increase_quantity_received(self, purchase_item, quantity_received): sellable = purchase_item.sellable items = [ item for item in self.get_items() if item.sellable.id == sellable.id ] qty = len(items) if not qty: raise ValueError( _(u'There is no purchase item for ' u'sellable %r') % sellable) purchase_item.quantity_received += quantity_received def update_products_cost(self): """Update purchase's items cost Update the costs of all products on this purchase to the costs specified in the order. """ for item in self.get_items(): item.sellable.cost = item.cost product = item.sellable.product product_supplier = product.get_product_supplier_info(self.supplier) product_supplier.base_cost = item.cost @property def status_str(self): return PurchaseOrder.translate_status(self.status) @property def freight_type_name(self): if not self.freight_type in self.freight_types.keys(): raise DatabaseInconsistency( _(u'Invalid freight_type, got %d') % self.freight_type) return self.freight_types[self.freight_type] @property def branch_name(self): return self.branch.get_description() @property def supplier_name(self): return self.supplier.get_description() @property def transporter_name(self): if not self.transporter: return u"" return self.transporter.get_description() @property def responsible_name(self): return self.responsible.get_description() @property def purchase_subtotal(self): """Get the subtotal of the purchase. The sum of all the items cost * items quantity """ return currency( self.get_items().sum(PurchaseItem.cost * PurchaseItem.quantity) or 0) @property def purchase_total(self): subtotal = self.purchase_subtotal total = subtotal - self.discount_value + self.surcharge_value if total < 0: raise ValueError(_(u'Purchase total can not be lesser than zero')) # XXX: Since the purchase_total value must have two digits # (at the moment) we need to format the value to a 2-digit number and # then convert it to currency data type, because the subtotal value # may return a 3-or-more-digit value, depending on COST_PRECISION_DIGITS # parameters. return currency(get_formatted_price(total)) @property def received_total(self): """Like {purchase_subtotal} but only takes into account the received items """ return currency(self.get_items().sum( PurchaseItem.cost * PurchaseItem.quantity_received) or 0) def get_remaining_total(self): """The total value to be paid for the items not received yet """ return self.purchase_total - self.received_total def get_pending_items(self, with_children=True): """ Returns a sequence of all items which we haven't received yet. """ return self.get_items(with_children=with_children).find( PurchaseItem.quantity_received < PurchaseItem.quantity) def get_partially_received_items(self): """ Returns a sequence of all items which are partially received. """ return self.get_items().find(PurchaseItem.quantity_received > 0) def get_open_date_as_string(self): return self.open_date and self.open_date.strftime("%x") or u"" def get_quote_deadline_as_string(self): return self.quote_deadline and self.quote_deadline.strftime( "%x") or u"" def get_receiving_orders(self): """Returns all ReceivingOrder related to this purchase order """ from stoqlib.domain.receiving import PurchaseReceivingMap, ReceivingOrder tables = [PurchaseReceivingMap, ReceivingOrder] query = And(PurchaseReceivingMap.purchase_id == self.id, PurchaseReceivingMap.receiving_id == ReceivingOrder.id) return self.store.using(*tables).find(ReceivingOrder, query) def get_data_for_labels(self): """ This function returns some necessary data to print the purchase's items labels """ for purchase_item in self.get_items(): sellable = purchase_item.sellable label_data = Settable(barcode=sellable.barcode, code=sellable.code, description=sellable.description, price=sellable.price, quantity=purchase_item.quantity) yield label_data def has_batch_item(self): """Fetch the storables from this purchase order and returns ``True`` if any of them is a batch storable. :returns: ``True`` if this purchase order has batch items, ``False`` if it doesn't. """ return not self.store.find( Storable, And(self.id == PurchaseOrder.id, PurchaseOrder.id == PurchaseItem.order_id, PurchaseItem.sellable_id == Sellable.id, Sellable.id == Storable.id, Eq(Storable.is_batch, True))).is_empty() # # Classmethods # @classmethod def translate_status(cls, status): if not status in cls.statuses: raise DatabaseInconsistency( _(u'Got an unexpected status value: ' u'%s') % status) return cls.statuses[status]
class PaymentMethod(Domain): """A PaymentMethod controls how a payments is paid. Example of payment methods are:: * money * bill * check * credit card This class consists of the persistent part of a payment method. The logic itself for the various different methods are in the PaymentMethodOperation classes. Each :class:`PaymentMethod` has a PaymentMethodOperation associated. """ __storm_table__ = 'payment_method' method_name = UnicodeCol() is_active = BoolCol(default=True) daily_interest = PercentCol(default=0) #: a value for the penalty. It must always be in the format:: #: #: 0 <= penalty <= 100 #: penalty = PercentCol(default=0) #: which day in the month is the credit provider going to pay the store? #: Usually they pay in the same day every month. payment_day = IntCol(default=None) #: which day the credit provider stoq counting sales to pay in the #: payment_day? Sales after this day will be paid only in the next month. closing_day = IntCol(default=None) max_installments = IntCol(default=1) destination_account_id = IdCol(default=None) destination_account = Reference(destination_account_id, 'Account.id') # # IActive implementation # def inactivate(self): assert self.is_active, ('This provider is already inactive') self.is_active = False def activate(self): assert not self.is_active, ('This provider is already active') self.is_active = True def get_status_string(self): if self.is_active: return _(u'Active') return _(u'Inactive') # # IDescribable implementation # def get_description(self): return self.description # # Properties # @property def description(self): return self.operation.description @property def operation(self): """Get the operation for this method. The operation contains method specific logic when creating/deleting a payment. :return: the operation associated with the method :rtype: object implementing IPaymentOperation """ from stoqlib.domain.payment.operation import get_payment_operation return get_payment_operation(self.method_name) # # Public API # # FIXME All create_* methods should be moved to a separate class, # they don't really belong to the method itself. # They should either go into the group or to a separate payment # factory singleton. def create_payment(self, branch, station: BranchStation, payment_type, payment_group, value, due_date=None, description=None, base_value=None, payment_number=None, identifier=None, ignore_max_installments=False): """Creates a new payment according to a payment method interface :param payment_type: the kind of payment, in or out :param payment_group: a :class:`PaymentGroup` subclass :param branch: the :class:`branch <stoqlib.domain.person.Branch>` associated with the payment, for incoming payments this is the branch receiving the payment and for outgoing payments this is the branch sending the payment. :param value: value of payment :param due_date: optional, due date of payment :param details: optional :param description: optional, description of the payment :param base_value: optional :param payment_number: optional :param ignore_max_installments: optional, defines whether max_installments should be ignored. :returns: a :class:`payment <stoqlib.domain.payment.Payment>` """ store = self.store if due_date is None: due_date = TransactionTimestamp() if not ignore_max_installments and payment_type == Payment.TYPE_IN: query = And(Payment.group_id == payment_group.id, Payment.method_id == self.id, Payment.payment_type == Payment.TYPE_IN, Payment.status != Payment.STATUS_CANCELLED) payment_count = store.find(Payment, query).count() if payment_count == self.max_installments: raise PaymentMethodError( _('You can not create more inpayments for this payment ' 'group since the maximum allowed for this payment ' 'method is %d') % self.max_installments) elif payment_count > self.max_installments: raise DatabaseInconsistency( _('You have more inpayments in database than the maximum ' 'allowed for this payment method')) if not description: description = self.describe_payment(payment_group) payment = Payment(store=store, branch=branch, station=station, identifier=identifier, payment_type=payment_type, due_date=due_date, value=value, base_value=base_value, group=payment_group, method=self, category=None, description=description, payment_number=payment_number) self.operation.payment_create(payment) return payment def create_payments(self, branch, station: BranchStation, payment_type, group, value, due_dates): """Creates new payments The values of the individual payments are calculated by taking the value and dividing it by the number of payments. The number of payments is determined by the length of the due_dates sequence. :param payment_type: the kind of payment, in or out :param payment_group: a |paymentgroup| :param branch: the |branch| associated with the payments, for incoming payments this is the branch receiving the payment and for outgoing payments this is the branch sending the payment. :param value: total value of all payments :param due_dates: a list of datetime objects :returns: a list of |payments| """ installments = len(due_dates) if not installments: raise ValueError(_('Need at least one installment')) if payment_type == Payment.TYPE_IN: if installments > self.max_installments: raise ValueError( _('The number of installments can not be greater than %d ' 'for payment method %s') % (self.max_installments, self.method_name)) # Create the requested payments with the right: # - due_date # - description for the specific group # - normalized value payments = [] normalized_values = generate_payments_values(value, installments) for (i, due_date), normalized_value in zip(enumerate(due_dates), normalized_values): description = self.describe_payment(group, i + 1, installments) payment = self.create_payment(station=station, payment_type=payment_type, payment_group=group, branch=branch, value=normalized_value, due_date=due_date, description=description) payments.append(payment) return payments def describe_payment(self, payment_group, installment=1, installments=1): """ Returns a string describing payment, in the following format: current_installment/total_of_installments payment_description for payment_group_description :param payment_group: a :class:`PaymentGroup` :param installment: current installment :param installments: total installments :returns: a payment description """ assert installment > 0 assert installments > 0 assert installments >= installment # TRANSLATORS: This will generate something like: 1/1 Money for sale 00001 return _( u'{installment} {method_name} for {order_description}').format( installment=u'%s/%s' % (installment, installments), method_name=self.get_description(), order_description=payment_group.get_description()) @classmethod def get_active_methods(cls, store): """Returns a list of active payment methods """ methods = store.find(PaymentMethod, is_active=True) return locale_sorted(methods, key=operator.attrgetter('description')) @classmethod def get_by_name(cls, store, name) -> 'PaymentMethod': """Returns the Payment method associated by the nmae :param name: name of a payment method :returns: a :class:`payment methods <PaymentMethod>` """ return store.find(PaymentMethod, method_name=name).one() @classmethod def get_by_account(cls, store, account) -> 'PaymentMethod': """Returns the Payment method associated with an account :param account: |account| for which the payment methods are associated with :returns: a sequence :class:`payment methods <PaymentMethod>` """ return store.find(PaymentMethod, destination_account=account) @classmethod def get_creatable_methods(cls, store, payment_type, separate): """Gets a list of methods that are creatable. Eg, you can use them to create new payments. :returns: a list of :class:`payment methods <PaymentMethod>` """ methods = [] for method in cls.get_active_methods(store): if not method.operation.creatable(method, payment_type, separate): continue methods.append(method) return methods @classmethod def get_editable_methods(cls, store): """Gets a list of methods that are editable Eg, you can change the details such as maximum installments etc. :returns: a list of :class:`payment methods <PaymentMethod>` """ # FIXME: Dont let users see online payments for now, to avoid # confusions with active state. online is an exception to that # logic. 'trade' for the same reason clause = And(cls.method_name != u'online', cls.method_name != u'trade') methods = store.find(cls, clause) return locale_sorted(methods, key=operator.attrgetter('description')) def selectable(self): """Finds out if the method is selectable, eg if the user can select it when doing a sale. :returns: ``True`` if selectable """ return self.operation.selectable(self)
class Image(Domain): __storm_table__ = 'image' image = BLOBCol(default=None) thumbnail = BLOBCol(default=None) description = UnicodeCol(default=u'')
class Till(IdentifiableDomain): """The Till describes the financial operations of a specific day. The operations that are recorded in a Till: * Sales * Adding cash * Removing cash * Giving out an early salary Each operation is associated with a |tillentry|. You can only open a Till once per day, and you cannot open a new till before you closed the previously opened one. """ __storm_table__ = 'till' #: this till is created, but not yet opened STATUS_PENDING = u'pending' #: this till is opened and we can make sales for it. STATUS_OPEN = u'open' #: end of the day, the till is closed and no more #: financial operations can be done in this store. STATUS_CLOSED = u'closed' #: after the till is closed, it can optionally be verified by a different user #: (usually a manager or supervisor) STATUS_VERIFIED = u'verified' statuses = collections.OrderedDict([ (STATUS_PENDING, _(u'Pending')), (STATUS_OPEN, _(u'Opened')), (STATUS_CLOSED, _(u'Closed')), (STATUS_VERIFIED, _(u'Verified')), ]) #: A sequencial number that identifies this till. identifier = IdentifierCol() status = EnumCol(default=STATUS_PENDING) #: The total amount we had the moment the till was opened. initial_cash_amount = PriceCol(default=0, allow_none=False) #: The total amount we have the moment the till is closed. final_cash_amount = PriceCol(default=0, allow_none=False) #: When the till was opened or None if it has not yet been opened. opening_date = DateTimeCol(default=None) #: When the till was closed or None if it has not yet been closed closing_date = DateTimeCol(default=None) #: When the till was verifyed or None if it's not yet verified verify_date = DateTimeCol(default=None) station_id = IdCol() #: the |branchstation| associated with the till, eg the computer #: which opened it. station = Reference(station_id, 'BranchStation.id') branch_id = IdCol() #: the branch this till is from branch = Reference(branch_id, 'Branch.id') observations = UnicodeCol(default=u"") responsible_open_id = IdCol() #: The responsible for opening the till responsible_open = Reference(responsible_open_id, "LoginUser.id") responsible_close_id = IdCol() #: The responsible for closing the till responsible_close = Reference(responsible_close_id, "LoginUser.id") responsible_verify_id = IdCol() #: The responsible for verifying the till responsible_verify = Reference(responsible_verify_id, "LoginUser.id") summary = ReferenceSet('id', 'TillSummary.till_id') # # Classmethods # @classmethod def get_current(cls, store): """Fetches the Till for the current station. :param store: a store :returns: a Till instance or None """ station = get_current_station(store) assert station is not None till = store.find(cls, status=Till.STATUS_OPEN, station=station).one() if till and till.needs_closing(): fmt = _("You need to close the till opened at %s before " "doing any fiscal operations") raise TillError(fmt % (till.opening_date.date(), )) return till @classmethod def get_last_opened(cls, store): """Fetches the last Till which was opened. If in doubt, use Till.get_current instead. This method is a special case which is used to be able to close a till without calling get_current() :param store: a store """ result = store.find(Till, status=Till.STATUS_OPEN, station=get_current_station(store)) result = result.order_by(Till.opening_date) if not result.is_empty(): return result[0] @classmethod def get_last(cls, store): station = get_current_station(store) result = store.find(Till, station=station).order_by(Till.opening_date) return result.last() @classmethod def get_last_closed(cls, store): station = get_current_station(store) result = store.find(Till, station=station, status=Till.STATUS_CLOSED).order_by( Till.opening_date) return result.last() # # Till methods # def open_till(self): """Open the till. It can only be done once per day. The final cash amount of the previous till will be used as the initial value in this one after opening it. """ if self.status == Till.STATUS_OPEN: raise TillError(_('Till is already open')) manager = get_plugin_manager() # The restriction to only allow opening the till only once per day comes from # the (soon to be obsolete) ECF devices. if manager.is_active('ecf'): # Make sure that the till has not been opened today today = localtoday().date() if not self.store.find( Till, And( Date(Till.opening_date) >= today, Till.station_id == self.station.id)).is_empty(): raise TillError(_("A till has already been opened today")) last_till = self._get_last_closed_till() if last_till: if not last_till.closing_date: raise TillError(_("Previous till was not closed")) initial_cash_amount = last_till.final_cash_amount else: initial_cash_amount = 0 self.initial_cash_amount = initial_cash_amount self.opening_date = TransactionTimestamp() self.status = Till.STATUS_OPEN self.responsible_open = get_current_user(self.store) assert self.responsible_open is not None TillOpenedEvent.emit(self) def close_till(self, observations=u""): """This method close the current till operation with the confirmed sales associated. If there is a sale with a differente status than SALE_CONFIRMED, a new 'pending' till operation is created and these sales are associated with the current one. """ if self.status == Till.STATUS_CLOSED: raise TillError(_("Till is already closed")) if self.get_balance() < 0: raise ValueError( _("Till balance is negative, but this should not " "happen. Contact Stoq Team if you need " "assistance")) self.final_cash_amount = self.get_balance() self.closing_date = TransactionTimestamp() self.status = Till.STATUS_CLOSED self.observations = observations self.responsible_close = get_current_user(self.store) assert self.responsible_open is not None TillClosedEvent.emit(self) def add_entry(self, payment): """ Adds an entry to the till. :param payment: a |payment| :returns: |tillentry| representing the added debit """ if payment.is_inpayment(): value = payment.value elif payment.is_outpayment(): value = -payment.value else: # pragma nocoverage raise AssertionError(payment) return self._add_till_entry(value, payment.description, payment) def add_debit_entry(self, value, reason=u""): """Add debit to the till :param value: amount to add :param reason: description of payment :returns: |tillentry| representing the added debit """ assert value >= 0 return self._add_till_entry(-value, reason) def add_credit_entry(self, value, reason=u""): """Add credit to the till :param value: amount to add :param reason: description of entry :returns: |tillentry| representing the added credit """ assert value >= 0 return self._add_till_entry(value, reason) def needs_closing(self): """Checks if there's an open till that needs to be closed before we can do any further fiscal operations. :returns: True if it needs to be closed, otherwise false """ if self.status != Till.STATUS_OPEN: return False # Verify that the till wasn't opened today if self.opening_date.date() == localtoday().date(): return False if localnow().hour < sysparam.get_int('TILL_TOLERANCE_FOR_CLOSING'): return False return True def get_balance(self): """Returns the balance of all till operations plus the initial amount cash amount. :returns: the balance :rtype: currency """ total = self.get_entries().sum(TillEntry.value) or 0 return currency(self.initial_cash_amount + total) def get_cash_amount(self): """Returns the total cash amount on the till. That includes "extra" payments (like cash advance, till complement and so on), the money payments and the initial cash amount. :returns: the cash amount on the till :rtype: currency """ store = self.store money = PaymentMethod.get_by_name(store, u'money') clause = And( Or(Eq(TillEntry.payment_id, None), Payment.method_id == money.id), TillEntry.till_id == self.id) join = LeftJoin(Payment, Payment.id == TillEntry.payment_id) results = store.using(TillEntry, join).find(TillEntry, clause) return currency(self.initial_cash_amount + (results.sum(TillEntry.value) or 0)) def get_entries(self): """Fetches all the entries related to this till :returns: all entries :rtype: sequence of |tillentry| """ return self.store.find(TillEntry, till=self) def get_credits_total(self): """Calculates the total credit for all entries in this till :returns: total credit :rtype: currency """ results = self.store.find( TillEntry, And(TillEntry.value > 0, TillEntry.till_id == self.id)) return currency(results.sum(TillEntry.value) or 0) def get_debits_total(self): """Calculates the total debit for all entries in this till :returns: total debit :rtype: currency """ results = self.store.find( TillEntry, And(TillEntry.value < 0, TillEntry.till_id == self.id)) return currency(results.sum(TillEntry.value) or 0) # FIXME: Rename to create_day_summary def get_day_summary(self): """Get the summary of this till for closing. When using a blind closing process, this will create TillSummary entries that will save the values all payment methods used. """ money_method = PaymentMethod.get_by_name(self.store, u'money') day_history = {} # Keys are (method, provider, card_type), provider and card_type may be None if # payment was not with card day_history[(money_method, None, None)] = 0 for entry in self.get_entries(): provider = card_type = None payment = entry.payment method = payment.method if payment else money_method if payment and payment.card_data: provider = payment.card_data.provider card_type = payment.card_data.card_type key = (method, provider, card_type) day_history.setdefault(key, 0) day_history[key] += entry.value summary = [] for (method, provider, card_type), value in day_history.items(): summary.append( TillSummary(till=self, method=method, provider=provider, card_type=card_type, system_value=value)) return summary # # Private # def _get_last_closed_till(self): results = self.store.find(Till, status=Till.STATUS_CLOSED, station=self.station).order_by( Till.opening_date) return results.last() def _add_till_entry(self, value, description, payment=None): assert value != 0 return TillEntry(value=value, description=description, payment=payment, till=self, branch=self.station.branch, store=self.store)
class Loan(Domain): """ A loan is a collection of |sellable| that is being loaned to a |client|, the items are expected to be either be returned to stock or sold via a |sale|. A loan that can hold a set of :class:`loan items <LoanItem>` See also: `schema <http://doc.stoq.com.br/schema/tables/loan.html>`__ `manual <http://doc.stoq.com.br/manual/loan.html>`__ """ __storm_table__ = 'loan' #: The request for a loan has been added to the system, #: we know which of the items the client wishes to loan, #: it's not defined if the client has actually picked up #: the items. STATUS_OPEN = u'open' #: All the products or other sellable items have been #: returned and are available in stock. STATUS_CLOSED = u'closed' #: The loan is cancelled and all the products or other sellable items have #: been returned and are available in stock. STATUS_CANCELLED = u'cancelled' # FIXME: This is missing a few states, # STATUS_LOANED: stock is completely synchronized statuses = { STATUS_OPEN: _(u'Opened'), STATUS_CLOSED: _(u'Closed'), STATUS_CANCELLED: _(u'Cancelled') } #: 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() #: status of the loan status = EnumCol(allow_none=False, default=STATUS_OPEN) #: notes related to this loan. notes = UnicodeCol(default=u'') #: date loan was opened open_date = DateTimeCol(default_factory=localnow) #: date loan was closed close_date = DateTimeCol(default=None) #: loan expires on this date, we expect the items to #: to be returned by this date expire_date = DateTimeCol(default=None) #: the date the loan was cancelled cancel_date = DateTimeCol(default=None) removed_by = UnicodeCol(default=u'') #: the reason the loan was cancelled cancel_reason = UnicodeCol() #: branch where the loan was done branch_id = IdCol() branch = Reference(branch_id, 'Branch.id') #: :class:`user <stoqlib.domain.person.LoginUser>` of the system #: that made the loan # FIXME: Should probably be a SalesPerson, we can find the # LoginUser via te.user_id responsible_id = IdCol() responsible = Reference(responsible_id, 'LoginUser.id') #: client that loaned the items client_id = IdCol(default=None) client = Reference(client_id, 'Client.id') client_category_id = IdCol(default=None) #: the |clientcategory| used for price determination. client_category = Reference(client_category_id, 'ClientCategory.id') #: a list of all items loaned in this loan loaned_items = ReferenceSet('id', 'LoanItem.loan_id') #: |payments| generated by this loan payments = None #: |transporter| used in loan transporter = None invoice_id = IdCol() #: The |invoice| generated by the loan invoice = Reference(invoice_id, 'Invoice.id') #: The responsible for cancelling the loan. At the moment, the #: |loginuser| that cancelled the loan cancel_responsible_id = IdCol() cancel_responsible = Reference(cancel_responsible_id, 'LoginUser.id') def __init__(self, store=None, **kwargs): kwargs['invoice'] = Invoice(store=store, invoice_type=Invoice.TYPE_OUT) super(Loan, self).__init__(store=store, **kwargs) # # Classmethods # @classmethod def get_status_name(cls, status): if not status in cls.statuses: raise DatabaseInconsistency(_("Invalid status %d") % status) return cls.statuses[status] # # IContainer implementation # def add_item(self, loan_item): assert not loan_item.loan loan_item.loan = self def get_items(self): return self.store.find(LoanItem, loan=self) def remove_item(self, loan_item): loan_item.loan = None self.store.maybe_remove(loan_item) # # IInvoice implementation # @property def comments(self): return [Settable(comment=self.notes)] @property def discount_value(self): discount = currency(0) for item in self.get_items(): if item.price > item.sellable.base_price: continue discount += item.sellable.base_price - item.price return discount @property def invoice_subtotal(self): return self.get_sale_base_subtotal() @property def invoice_total(self): return self.get_total_amount() @property def recipient(self): return self.client.person @property def operation_nature(self): # TODO: Save the operation nature in new loan table field. return _(u"Loan") # # Public API # def add_sellable(self, sellable, quantity=1, price=None, batch=None): """Adds a new sellable item to a loan :param sellable: the |sellable| :param quantity: quantity to add, defaults to 1 :param price: optional, the price, it not set the price from the sellable will be used :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. """ self.validate_batch(batch, sellable=sellable) price = price or sellable.price base_price = sellable.price return LoanItem(store=self.store, quantity=quantity, loan=self, sellable=sellable, batch=batch, price=price, base_price=base_price) def get_available_discount_for_items(self, user=None, exclude_item=None): """Get available discount for items in this loan The available items discount is the total discount not used by items in this sale. For instance, if we have 2 products with a price of 100 and they can have 10% of discount, we have 20 of discount available. If one of those products price is set to 98, that is, using 2 of it's discount, the available discount is now 18. :param user: passed to :meth:`stoqlib.domain.sellable.Sellable.get_maximum_discount` together with :obj:`.client_category` to check for the max discount for sellables on this sale :param exclude_item: a |saleitem| to exclude from the calculations. Useful if you are trying to get some extra discount for that item and you don't want it's discount to be considered here :returns: the available discount """ available_discount = currency(0) used_discount = currency(0) for item in self.get_items(): if item == exclude_item: continue # Don't put surcharges on the discount, or it can end up negative if item.price > item.sellable.base_price: continue used_discount += item.sellable.base_price - item.price max_discount = item.sellable.get_maximum_discount( category=self.client_category, user=user) / 100 available_discount += item.base_price * max_discount return available_discount - used_discount def set_items_discount(self, discount): """Apply discount on this sale's items :param decimal.Decimal discount: the discount to be applied as a percentage, e.g. 10.0, 22.5 """ new_total = currency(0) item = None candidate = None for item in self.get_items(): item.set_discount(discount) new_total += item.price * item.quantity if item.quantity == 1: candidate = item # Since we apply the discount percentage above, items can generate a # 3rd decimal place, that will be rounded to the 2nd, making the value # differ. Find that difference and apply it to a sale item, preferable # to one with a quantity of 1 since, for instance, applying +0,1 to an # item with a quantity of 4 would make it's total +0,4 (+0,3 extra than # we are trying to adjust here). discount_value = (self.get_sale_base_subtotal() * discount) / 100 diff = new_total - self.get_sale_base_subtotal() + discount_value if diff: item = candidate or item item.price -= diff # # Accessors # def get_total_amount(self): """ Fetches the total value of the loan, that is to be paid by the client. It can be calculated as:: Sale total = Sum(product and service prices) + surcharge + interest - discount :returns: the total value """ return currency(self.get_items().sum( Round(LoanItem.price * LoanItem.quantity, DECIMAL_PRECISION)) or 0) def get_client_name(self): if self.client: return self.client.person.name return u'' def get_branch_name(self): if self.branch: return self.branch.get_description() return u'' def get_responsible_name(self): return self.responsible.person.name # # Public API # def sync_stock(self): """Synchronizes the stock of *self*'s :class:`loan items <LoanItem>` Just a shortcut to call :meth:`LoanItem.sync_stock` of all of *self*'s :class:`loan items <LoanItem>` instead of having to do that one by one. """ for loan_item in self.get_items(): # No need to sync stock for products that dont need. if not loan_item.sellable.product.manage_stock: continue loan_item.sync_stock() def can_close(self): """Checks if the loan can be closed. A loan can be closed if it is opened and all the items have been returned or sold. :returns: True if the loan can be closed, False otherwise. """ if self.status != Loan.STATUS_OPEN: return False for item in self.get_items(): if item.sale_quantity + item.return_quantity != item.quantity: return False return True def get_sale_base_subtotal(self): """Get the base subtotal of items Just a helper that, unlike :meth:`.get_sale_subtotal`, will return the total based on item's base price. :returns: the base subtotal """ subtotal = self.get_items().sum(LoanItem.quantity * LoanItem.base_price) return currency(subtotal) def close(self): """Closes the loan. At this point, all the loan items have been returned to stock or sold.""" assert self.can_close() self.close_date = localnow() self.status = Loan.STATUS_CLOSED def confirm(self): # Save the operation nature and branch in Invoice table. self.invoice.operation_nature = self.operation_nature self.invoice.branch = self.branch # Since there is no status change here and the event requires # the parameter, we use None old_status = None StockOperationConfirmedEvent.emit(self, old_status)
class ProductQualityTest(Domain): """A quality test that a manufactured product will be submitted to. See also: `schema <http://doc.stoq.com.br/schema/tables/product_quality_test.html>`__ """ __storm_table__ = 'product_quality_test' (TYPE_BOOLEAN, TYPE_DECIMAL) = range(2) types = { TYPE_BOOLEAN: _(u'Boolean'), TYPE_DECIMAL: _(u'Decimal'), } product_id = IdCol() product = Reference(product_id, 'Product.id') test_type = IntCol(default=TYPE_BOOLEAN) description = UnicodeCol(default=u'') notes = UnicodeCol(default=u'') success_value = UnicodeCol(default=u'True') def get_description(self): return self.description @property def type_str(self): return self.types[self.test_type] @property def success_value_str(self): return _(self.success_value) def get_boolean_value(self): assert self.test_type == self.TYPE_BOOLEAN if self.success_value == u'True': return True elif self.success_value == u'False': return False else: raise ValueError(self.success_value) def get_range_value(self): assert self.test_type == self.TYPE_DECIMAL a, b = self.success_value.split(u' - ') return Decimal(a), Decimal(b) def set_boolean_value(self, value): self.success_value = unicode(value) def set_range_value(self, min_value, max_value): self.success_value = u'%s - %s' % (min_value, max_value) def result_value_passes(self, value): if self.test_type == self.TYPE_BOOLEAN: return self.get_boolean_value() == value else: a, b = self.get_range_value() return a <= value <= b def can_remove(self): from stoqlib.domain.production import ProductionItemQualityResult if self.store.find(ProductionItemQualityResult, quality_test=self).count(): return False return True
class Payment(IdentifiableDomain): """Payment, a transfer of money between a |branch| and |client| or a |supplier|. Payments between: * a client and a branch are :obj:`.TYPE_IN`, has a |sale| associated. * branch and a supplier are :obj:`.TYPE_OUT`, has a |purchase| associated. Payments are sometimes referred to as *installments*. Sales and purchase orders can be accessed via the :obj:`payment group <.group>` +-------------------------+-------------------------+ | **Status** | **Can be set to** | +-------------------------+-------------------------+ | :obj:`STATUS_PREVIEW` | :obj:`STATUS_PENDING` | +-------------------------+-------------------------+ | :obj:`STATUS_PENDING` | :obj:`STATUS_PAID`, | | | :obj:`STATUS_CANCELLED` | +-------------------------+-------------------------+ | :obj:`STATUS_PAID` | :obj:`STATUS_PENDING`, | | | :obj:`STATUS_CANCELLED` | +-------------------------+-------------------------+ | :obj:`STATUS_CANCELLED` | None | +-------------------------+-------------------------+ .. graphviz:: digraph status { STATUS_PREVIEW -> STATUS_PENDING; STATUS_PENDING -> STATUS_PAID; STATUS_PENDING -> STATUS_CANCELLED; STATUS_PAID -> STATUS_PENDING; STATUS_PAID -> STATUS_CANCELLED; } Simple sale workflow: * Creating a sale, status is set to :obj:`STATUS_PREVIEW` * Confirming the sale, status is set to :obj:`STATUS_PENDING` * Paying the installment, status is set to :obj:`STATUS_PAID` * Cancelling the payment, status is set to :obj:`STATUS_CANCELLED` See also: `schema <http://doc.stoq.com.br/schema/tables/payment.html>`__ """ __storm_table__ = 'payment' #: incoming to the company, accounts receivable, payment from #: a |client| to a |branch| TYPE_IN = u'in' #: outgoing from the company, accounts payable, a payment from #: |branch| to a |supplier| TYPE_OUT = u'out' #: payment group this payment belongs to hasn't been confirmed, # should normally be filtered when showing a payment list STATUS_PREVIEW = u'preview' #: payment group has been confirmed and the payment has not been received STATUS_PENDING = u'pending' #: the payment has been received STATUS_PAID = u'paid' # FIXME: Remove these two #: Unused. STATUS_REVIEWING = u'reviewing' #: Unused. STATUS_CONFIRMED = u'confirmed' #: payment was cancelled, for instance the payments of the group was changed, or #: the group was cancelled. STATUS_CANCELLED = u'cancelled' statuses = collections.OrderedDict([ (STATUS_PREVIEW, _(u'Preview')), (STATUS_PENDING, _(u'To Pay')), (STATUS_PAID, _(u'Paid')), (STATUS_REVIEWING, _(u'Reviewing')), (STATUS_CONFIRMED, _(u'Confirmed')), (STATUS_CANCELLED, _(u'Cancelled')), ]) #: type of payment :obj:`.TYPE_IN` or :obj:`.TYPE_OUT` payment_type = EnumCol(allow_none=False, default=TYPE_IN) #: 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() #: status, see |payment| for more information. status = EnumCol(allow_none=False, default=STATUS_PREVIEW) #: description payment, usually something like "1/3 Money for Sale 1234" description = UnicodeCol(default=None) # FIXME: use TransactionTimestamp() instead to avoid server/client date # inconsistencies #: when this payment was opened open_date = DateTimeCol(default_factory=localnow) #: when this payment is due due_date = DateTimeCol() #: when this payment was paid paid_date = DateTimeCol(default=None) #: when this payment was cancelled cancel_date = DateTimeCol(default=None) # FIXME: Figure out when and why this differs from value #: base value base_value = PriceCol(default=None) #: value of the payment value = PriceCol() #: the actual amount that was paid, including penalties, interest, discount etc. paid_value = PriceCol(default=None) #: interest of this payment interest = PriceCol(default=0) #: discount, an absolute value with the difference between the #: sales price and :obj:`.value` discount = PriceCol(default=0) #: penalty of the payment penalty = PriceCol(default=0) # FIXME: Figure out what this is used for #: number of the payment payment_number = UnicodeCol(default=None) branch_id = IdCol(allow_none=False) #: |branch| associated with this payment. #: For a :obj:`.TYPE_IN` payment, this is the branch that will receive #: the money. For a :obj:`.TYPE_IN` payment, this is the branch that #: will make the payment branch = Reference(branch_id, 'Branch.id') station_id = IdCol(allow_none=False) #: The station this object was created at station = Reference(station_id, 'BranchStation.id') method_id = IdCol() #: |paymentmethod| for this payment #: payment method = Reference(method_id, 'PaymentMethod.id') group_id = IdCol() #: |paymentgroup| for this payment group = Reference(group_id, 'PaymentGroup.id') category_id = IdCol() #: |paymentcategory| this payment belongs to, can be None category = Reference(category_id, 'PaymentCategory.id') #: list of :class:`comments <stoqlib.domain.payment.comments.PaymentComment>` for #: this payment comments = ReferenceSet('id', 'PaymentComment.payment_id') #: :class:`check data <stoqlib.domain.payment.method.CheckData>` for #: this payment check_data = Reference('id', 'CheckData.payment_id', on_remote=True) #: |accounttransaction| for this payment transaction = Reference('id', 'AccountTransaction.payment_id', on_remote=True) card_data = Reference('id', 'CreditCardData.payment_id', on_remote=True) #: indicates if a bill has been received. They are usually delivered by #: mail before the due date. This is not indicating whether the payment has #: been paid, just that the receiver has notified the payer somehow. bill_received = BoolCol(default=False) attachment_id = IdCol() #: |attachment| for this payment attachment = Reference(attachment_id, 'Attachment.id') def __init__(self, store, branch, **kw): from stoqlib.domain.person import Branch assert isinstance(branch, Branch) if not 'value' in kw: raise TypeError('You must provide a value argument') if not 'base_value' in kw or not kw['base_value']: kw['base_value'] = kw['value'] super(Payment, self).__init__(store=store, branch=branch, **kw) def _check_status(self, status, operation_name): fmt = 'Invalid status for %s operation: %s' assert self.status == status, ( fmt % (operation_name, self.statuses[self.status])) # # ORMObject hooks # def delete(self): # First call hooks, do this first so the hook # have access to everything it needs self.method.operation.payment_delete(self) # FIXME: BUG 5581 check if it is really safe to remove the payment # when using with synced databases self.store.remove(self) @classmethod def create_repeated(cls, store, payment, repeat_type, start_date, end_date, temporary_identifiers=False): """Create a set of repeated payments. Given a type of interval (*repeat_type*), a start date and an end_date, this creates a list of payments for that interval. Note, this will also update the description of the payment that's passed in. :param store: a store :param payment: the payment to repeat :param repeat_type: the kind of repetition (weekly, monthly etc) :param start_date: the date to start this repetition :param end_date: the date to end this repetition :param temporary_identifiers: If the payments should be created with temporary identifiers :returns: a list of repeated payments """ dates = create_date_interval(interval_type=repeat_type, start_date=start_date, end_date=end_date) n_dates = dates.count() if n_dates == 1: raise AssertionError description = payment.description payment.description = u'1/%d %s' % (n_dates, description) payment.due_date = dates[0] payments = [] for i, date in enumerate(dates[1:]): temporary_identifier = None if temporary_identifiers: temporary_identifier = Payment.get_temporary_identifier(store) p = Payment(open_date=payment.open_date, identifier=temporary_identifier, branch=payment.branch, station=payment.station, payment_type=payment.payment_type, status=payment.status, description=u'%d/%d %s' % (i + 2, n_dates, description), value=payment.value, base_value=payment.base_value, due_date=date, method=payment.method, group=payment.group, category=payment.category, store=store) payments.append(p) return payments # # Properties # @property def comments_number(self): """The number of |paymentcomments| for this payment""" return self.comments.count() @property def bank_account_number(self): """For check payments, the :class:`bank account <BankAccount>` number""" # This is used by test_payment_method, and is a convenience # property, ideally we should move it to payment operation # somehow if self.method.method_name == u'check': data = self.method.operation.get_check_data_by_payment(self) bank_account = data.bank_account if bank_account: return bank_account.bank_number @property def installment_number(self): payments = self.group.get_valid_payments().order_by(Payment.identifier) for i, payment in enumerate(payments): if self == payment: return i + 1 @property def status_str(self): """The :obj:`Payment.status` as a translated string""" if not self.status in self.statuses: raise DatabaseInconsistency('Invalid status for Payment ' 'instance, got %d' % self.status) return self.statuses[self.status] def get_days_late(self): """For due payments, the number of days late this payment is :returns: the number of days late """ if self.status == Payment.STATUS_PAID: return 0 days_late = localtoday().date() - self.due_date.date() if days_late.days < 0: return 0 return days_late.days def set_pending(self): """Set a :obj:`.STATUS_PREVIEW` payment as :obj:`.STATUS_PENDING`. This also means that this is valid payment and its owner actually can charge it """ self._check_status(self.STATUS_PREVIEW, u'set_pending') self.status = self.STATUS_PENDING def set_not_paid(self, change_entry): """Set a :obj:`.STATUS_PAID` payment as :obj:`.STATUS_PENDING`. This requires clearing paid_date and paid_value :param change_entry: a :class:`PaymentChangeHistory` object, that will hold the changes information """ self._check_status(self.STATUS_PAID, u'set_not_paid') if self.transaction: self.transaction.create_reverse() change_entry.last_status = self.STATUS_PAID change_entry.new_status = self.STATUS_PENDING sale = self.group and self.group.sale if sale and sale.can_set_not_paid(): sale.set_not_paid() self.status = self.STATUS_PENDING self.paid_date = None self.paid_value = None def pay(self, paid_date=None, paid_value=None, source_account=None, destination_account=None, account_transaction_number=None): """Pay the current payment set its status as :obj:`.STATUS_PAID` If this payment belongs to a sale, and all other payments from the sale are paid then the sale will be set as paid. """ if self.status != Payment.STATUS_PENDING: raise ValueError(_(u"This payment is already paid.")) self._check_status(self.STATUS_PENDING, u'pay') paid_value = paid_value or (self.value - self.discount + self.interest) self.paid_value = paid_value self.paid_date = paid_date or TransactionTimestamp() self.status = self.STATUS_PAID if (self.is_separate_payment() or self.method.operation.create_transaction()): AccountTransaction.create_from_payment( self, code=account_transaction_number, source_account=source_account, destination_account=destination_account) sale = self.group and self.group.sale if sale: sale.create_commission(self) # When paying payments of a sale, check if the other payments are # paid. If they are, this means you can change the sale status to # paid as well. if sale.can_set_paid(): sale.set_paid() if self.value == self.paid_value: msg = _( u"{method} payment with value {value:.2f} was paid").format( method=self.method.method_name, value=self.value) else: msg = _(u"{method} payment with value original value " u"{original_value:.2f} was paid with value " u"{value:.2f}").format(method=self.method.method_name, original_value=self.value, value=self.paid_value) Event.log(self.store, Event.TYPE_PAYMENT, msg.capitalize()) def cancel(self, change_entry=None): """Cancel the payment, set it's status to :obj:`.STATUS_CANCELLED` """ # TODO Check for till entries here and call cancel_till_entry if # it's possible. Bug 2598 if not self.can_cancel(): raise StoqlibError( _(u"Invalid status for cancel operation, " u"got %s") % self.status_str) if self.transaction: self.transaction.create_reverse() old_status = self.status self.status = self.STATUS_CANCELLED self.cancel_date = TransactionTimestamp() if change_entry is not None: change_entry.last_status = old_status change_entry.new_status = self.status msg = _( u"{method} payment with value {value:.2f} was cancelled").format( method=self.method.method_name, value=self.value) Event.log(self.store, Event.TYPE_PAYMENT, msg.capitalize()) def change_due_date(self, new_due_date): """Changes the payment due date. :param new_due_date: The new due date for the payment. :rtype: datetime.date """ if self.status in [Payment.STATUS_PAID, Payment.STATUS_CANCELLED]: raise StoqlibError( _(u"Invalid status for change_due_date operation, " u"got %s") % self.status_str) self.due_date = new_due_date def update_value(self, new_value): """Update the payment value. """ self.value = new_value def can_cancel(self): return self.status in (Payment.STATUS_PREVIEW, Payment.STATUS_PENDING, Payment.STATUS_PAID) def get_payable_value(self): """Returns the calculated payment value with the daily interest. Note that the payment group daily_interest must be between 0 and 100. :returns: the payable value """ if self.status in [self.STATUS_PREVIEW, self.STATUS_CANCELLED]: return self.value if self.status in [ self.STATUS_PAID, self.STATUS_REVIEWING, self.STATUS_CONFIRMED ]: return self.paid_value return self.value + self.get_interest() def get_penalty(self, date=None): """Calculate the penalty in an absolute value :param date: date of payment :returns: penalty :rtype: :class:`kiwi.currency.currency` """ if date is None: date = localtoday().date() elif date < self.open_date.date(): raise ValueError(_(u"Date can not be less then open date")) elif date > localtoday().date(): raise ValueError(_(u"Date can not be greather then future date")) if not self.method.penalty: return currency(0) # Don't add penalty if we pay in time! if self.due_date.date() >= date: return currency(0) return currency(self.method.penalty / 100 * self.value) def get_interest(self, date=None, pay_penalty=True): """Calculate the interest in an absolute value :param date: date of payment :returns: interest :rtype: :class:`kiwi.currency.currency` """ if date is None: date = localtoday().date() elif date < self.open_date.date(): raise ValueError(_(u"Date can not be less then open date")) elif date > localtoday().date(): raise ValueError(_(u"Date can not be greather then future date")) if not self.method.daily_interest: return currency(0) days = (date - self.due_date.date()).days if days <= 0: return currency(0) base_value = self.value + (pay_penalty and self.get_penalty(date=date)) return currency(days * self.method.daily_interest / 100 * base_value) def has_commission(self): """Check if this |payment| already has a |commission|""" from stoqlib.domain.commission import Commission return self.store.find(Commission, payment=self).any() def is_paid(self): """Check if the payment is paid. :returns: ``True`` if the payment is paid """ return self.status == Payment.STATUS_PAID def is_pending(self): """Check if the payment is pending. :returns: ``True`` if the payment is pending """ return self.status == Payment.STATUS_PENDING def is_preview(self): """Check if the payment is in preview state :returns: ``True`` if the payment is paid """ return self.status == Payment.STATUS_PREVIEW def is_cancelled(self): """Check if the payment was cancelled. :returns: ``True`` if the payment was cancelled """ return self.status == Payment.STATUS_CANCELLED def get_paid_date_string(self): """Get a paid date string :returns: the paid date string or PAID DATE if the payment isn't paid """ if self.paid_date: return self.paid_date.date().strftime('%x') return _(u'NOT PAID') def get_open_date_string(self): """Get a open date string :returns: the open date string or empty string """ if self.open_date: return self.open_date.date().strftime('%x') return u"" def is_inpayment(self): """Find out if a payment is :obj:`incoming <.TYPE_IN>` :returns: ``True`` if it's incoming """ return self.payment_type == self.TYPE_IN def is_outpayment(self): """Find out if a payment is :obj:`outgoing <.TYPE_OUT>` :returns: ``True`` if it's outgoing """ return self.payment_type == self.TYPE_OUT def is_separate_payment(self): """Find out if this payment is created separately from a sale, purchase or renegotiation :returns: ``True`` if it's separate. """ # FIXME: This is a hack, we should rather store a flag # in the database that tells us how the payment was # created. group = self.group if not group: # Should never happen return False if group.sale: return False elif group.purchase: return False elif group._renegotiation: return False return True def is_of_method(self, method_name): """Find out if the payment was made with a certain method :returns: ``True`` if it's a payment of that method """ return self.method.method_name == method_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 OpticalPatientHistory(Domain): __storm_table__ = 'optical_patient_history' #: Never used lenses before TYPE_FIRST_USER = u'first-user' #: Is currently a user TYPE_SECOND_USER = u'second-user' #: Has used lenses before, but stopped TYPE_EX_USER = u'ex-user' user_types = collections.OrderedDict([ (TYPE_FIRST_USER, _('First User')), (TYPE_SECOND_USER, _('Second User')), (TYPE_EX_USER, _('Ex-User')), ]) create_date = DateTimeCol(default_factory=StatementTimestamp) client_id = IdCol(allow_none=False) #: The related client client = Reference(client_id, 'Client.id') responsible_id = IdCol(allow_none=False) #: The user that registred this information responsible = Reference(responsible_id, 'LoginUser.id') # # Section 1: General questions # #: If the patient is a first time user for contact lenses or not. user_type = EnumCol(allow_none=False, default=TYPE_FIRST_USER) #: What is the occupation of the patient occupation = UnicodeCol() #: Details about the work environment (if it as air conditioning, dust, #: chemical products) work_environment = UnicodeCol() # # First time user # #: If the patient has ever tested any contact lenses has_tested = UnicodeCol() #: What brands the patient has tested tested_brand = UnicodeCol() #: If previous tests irritated the eye eye_irritation = UnicodeCol() #: What is the main purpose for using contact lenses? purpose_of_use = UnicodeCol() #: How many hours per day the patient intends to use the contact lenses intended_hour_usage = UnicodeCol() # # Second time / ex user # #: Previous brand of the client. previous_brand = UnicodeCol() #: What the previous brand felt like previous_feeling = UnicodeCol() #: Have ever had any cornea issues cornea_issues = UnicodeCol() #: How many hours per day the client used the lenses hours_per_day_usage = UnicodeCol() # # Second time user # #: For how long is a user user_since = UnicodeCol() #: Bring the previous lenses? has_previous_lenses = UnicodeCol() #: Previous lenses observations previous_lenses_notes = UnicodeCol() # # Ex User # #: How long since the last use. last_use = UnicodeCol() #: why stopped using stop_reason = UnicodeCol() #: Did frequent removal of proteins? protein_removal = UnicodeCol() #: What cleaning product used? cleaning_product = UnicodeCol() #: Free notes. history_notes = UnicodeCol() # # Section 2: Adaptation test # #: If the patient ever had eye injuries eye_injury = UnicodeCol() #: Any kind of recent pathology, like pink-eye recent_pathology = UnicodeCol() #: Is currently using eye drops using_eye_drops = UnicodeCol() #: Does the patient have health problems health_problems = UnicodeCol() #: Is the patient is using any kind of medicament using_medicament = UnicodeCol() #: Does the patient family has any health problems family_health_problems = UnicodeCol() #: How the eyes feel at the end of the day (burn, itch, etc...) end_of_day_feeling = UnicodeCol() #: Free notes. adaptation_notes = UnicodeCol() @property def responsible_name(self): return self.responsible.get_description()
class PaymentRenegotiation(Domain): """Class for payments renegotiations """ __storm_table__ = 'payment_renegotiation' (STATUS_CONFIRMED, STATUS_PAID, STATUS_RENEGOTIATED) = range(3) statuses = collections.OrderedDict([ (STATUS_CONFIRMED, _(u'Confirmed')), (STATUS_PAID, _(u'Paid')), (STATUS_RENEGOTIATED, _(u'Renegotiated')), ]) #: 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() status = IntCol(default=STATUS_CONFIRMED) notes = UnicodeCol(default=None) open_date = DateTimeCol(default_factory=localnow) close_date = DateTimeCol(default=None) discount_value = PriceCol(default=0) surcharge_value = PriceCol(default=0) total = PriceCol(default=0) responsible_id = IdCol() responsible = Reference(responsible_id, 'LoginUser.id') client_id = IdCol(default=None) client = Reference(client_id, 'Client.id') branch_id = IdCol(default=None) branch = Reference(branch_id, 'Branch.id') group_id = IdCol() group = Reference(group_id, 'PaymentGroup.id') # # Public API # def can_set_renegotiated(self): """Only sales with status confirmed can be renegotiated. :returns: True if the sale can be renegotiated, False otherwise. """ # This should be as simple as: # return self.status == Sale.STATUS_CONFIRMED # But due to bug 3890 we have to check every payment. return any([ payment.status == Payment.STATUS_PENDING for payment in self.payments ]) def get_client_name(self): if not self.client: return u"" return self.client.person.name def get_responsible_name(self): return self.responsible.person.name def get_status_name(self): return self.statuses[self.status] def get_subtotal(self): return currency(self.total + self.discount_value - self.surcharge_value) def set_renegotiated(self): """Set the sale as renegotiated. The sale payments have been renegotiated and the operations will be done in other payment group.""" assert self.can_set_renegotiated() self.close_date = TransactionTimestamp() self.status = PaymentRenegotiation.STATUS_RENEGOTIATED @property def payments(self): return self.group.get_valid_payments() # # IContainer Implementation # def add_item(self, payment): # TODO: pass def remove_item(self, payment): # TODO: pass def get_items(self): return self.store.find(PaymentGroup, renegotiation=self)
class OpticalPatientMeasures(Domain): __storm_table__ = 'optical_patient_measures' EYE_LEFT = u'left' EYE_RIGHT = u'right' eye_options = { EYE_LEFT: _('Left Eye'), EYE_RIGHT: _('Right Eye'), } create_date = DateTimeCol(default_factory=StatementTimestamp) client_id = IdCol(allow_none=False) #: The related client client = Reference(client_id, 'Client.id') responsible_id = IdCol(allow_none=False) #: The user that registred this information responsible = Reference(responsible_id, 'LoginUser.id') dominant_eye = EnumCol(allow_none=False, default=EYE_LEFT) le_keratometer_horizontal = UnicodeCol() le_keratometer_vertical = UnicodeCol() le_keratometer_axis = UnicodeCol() re_keratometer_horizontal = UnicodeCol() re_keratometer_vertical = UnicodeCol() re_keratometer_axis = UnicodeCol() le_eyebrown = UnicodeCol() le_eyelash = UnicodeCol() le_conjunctiva = UnicodeCol() le_sclerotic = UnicodeCol() le_iris_diameter = UnicodeCol() le_eyelid = UnicodeCol() le_eyelid_opening = UnicodeCol() le_cornea = UnicodeCol() #: Tear breakup time. How much time the eye takes to produce a tear le_tbut = UnicodeCol() #: test that checks how much tear the eye produces le_schirmer = UnicodeCol() re_eyebrown = UnicodeCol() re_eyelash = UnicodeCol() re_conjunctiva = UnicodeCol() re_sclerotic = UnicodeCol() re_iris_diameter = UnicodeCol() re_eyelid = UnicodeCol() re_eyelid_opening = UnicodeCol() re_cornea = UnicodeCol() #: Tear breakup time. How much time the eye takes to produce a tear re_tbut = UnicodeCol() #: test that checks how much tear the eye produces re_schirmer = UnicodeCol() notes = UnicodeCol() @property def responsible_name(self): return self.responsible.get_description()
class SellableCategory(Domain): """ A Sellable category. A way to group several |sellables| together, like "Shoes", "Consumer goods", "Services". A category can define markup, tax and commission, the values of the category will only be used when the sellable itself lacks a value. Sellable categories can be grouped recursively. See also: `schema <http://doc.stoq.com.br/schema/tables/sellable_category.html>`__ """ __storm_table__ = 'sellable_category' #: The category description description = UnicodeCol() #: A color that will be used in reports and interface for this category color = UnicodeCol() #: The sort order of this category. Usefull when you need a category to appear before all other #: categories sort_order = IntCol(default=0) #: Define the suggested markup when calculating the sellable's price. suggested_markup = PercentCol(default=0) #: A percentage comission suggested for all the sales which products #: belongs to this category. salesperson_commission = PercentCol(default=0) category_id = IdCol(default=None) #: base category of this category, ``None`` for base categories themselves category = Reference(category_id, 'SellableCategory.id') tax_constant_id = IdCol(default=None) #: the |sellabletaxconstant| for this sellable category tax_constant = Reference(tax_constant_id, 'SellableTaxConstant.id') #: the children of this category children = ReferenceSet('id', 'SellableCategory.category_id') # # Properties # @property def full_description(self): """The full description of the category, including its parents, for instance: u"Clothes:Shoes:Black Shoe 14 SL" """ descriptions = [self.description] parent = self.category while parent: descriptions.append(parent.description) parent = parent.category return u':'.join(reversed(descriptions)) # # Public API # def get_children_recursively(self): """Return all the children from this category, recursively This will return all children recursively, e.g.:: A / \ B C / \ D E In this example, calling this from A will return ``set([B, C, D, E])`` """ children = set(self.children) if not len(children): # Base case for the leafs return set() for child in list(children): children |= child.get_children_recursively() return children def get_commission(self): """Returns the commission for this category. If it's unset, return the value of the base category, if any :returns: the commission """ if self.category: return (self.salesperson_commission or self.category.get_commission()) return self.salesperson_commission def get_markup(self): """Returns the markup for this category. If it's unset, return the value of the base category, if any :returns: the markup """ if self.category: # Compare to None as markup can be '0' if self.suggested_markup is not None: return self.suggested_markup return self.category.get_markup() return self.suggested_markup def get_tax_constant(self): """Returns the tax constant for this category. If it's unset, return the value of the base category, if any :returns: the tax constant """ if self.category: return self.tax_constant or self.category.get_tax_constant() return self.tax_constant # # IDescribable # def get_description(self): return self.description # # Classmethods # @classmethod def get_base_categories(cls, store): """Returns all available base categories :param store: a store :returns: categories """ return store.find(cls, category_id=None) # # Domain hooks # def on_create(self): CategoryCreateEvent.emit(self) def on_update(self): CategoryEditEvent.emit(self)
class OpticalPatientTest(Domain): __storm_table__ = 'optical_patient_test' create_date = DateTimeCol(default_factory=StatementTimestamp) client_id = IdCol(allow_none=False) #: The related client client = Reference(client_id, 'Client.id') responsible_id = IdCol(allow_none=False) #: The user that registred this information responsible = Reference(responsible_id, 'LoginUser.id') #: The contact lens that is being tested. This could be a reference to a #: |product in the future le_item = UnicodeCol() #: The brand of the tested contact lenses le_brand = UnicodeCol() #: Curva Base - CB le_base_curve = UnicodeCol() le_spherical_degree = UnicodeCol() le_cylindrical = UnicodeCol() le_axis = UnicodeCol() le_diameter = UnicodeCol() le_movement = UnicodeCol() le_centralization = UnicodeCol() le_spin = UnicodeCol() le_fluorescein = UnicodeCol() #: Sobre refração - SRF le_over_refraction = UnicodeCol() le_bichrome = UnicodeCol() #: If the client is satisfied with this product le_client_approved = BoolCol() #: If the client has purchased this product after the test. le_client_purchased = BoolCol() #: If the product being tested was delivered to the client le_delivered = BoolCol() re_item = UnicodeCol() re_brand = UnicodeCol() re_base_curve = UnicodeCol() re_spherical_degree = UnicodeCol() re_cylindrical = UnicodeCol() re_axis = UnicodeCol() re_diameter = UnicodeCol() re_movement = UnicodeCol() re_centralization = UnicodeCol() re_spin = UnicodeCol() re_fluorescein = UnicodeCol() re_over_refraction = UnicodeCol() re_bichrome = UnicodeCol() re_client_approved = BoolCol() re_client_purchased = BoolCol() re_delivered = BoolCol() #: Free notes notes = UnicodeCol() @property def responsible_name(self): return self.responsible.get_description()
class ReturnedSale(Domain): """Holds information about a returned |sale|. This can be: * *trade*, a |client| is returning the |sale| and buying something new with that credit. In that case the returning sale is :obj:`.sale` and the replacement |sale| is in :obj:`.new_sale`. * *return sale* or *devolution*, a |client| is returning the |sale| without making a new |sale|. Normally the old sale which is returned is :obj:`.sale`, however it might be ``None`` in some situations for example, if the |sale| was done at a different |branch| that hasn't been synchronized or is using another system. """ __storm_table__ = 'returned_sale' #: This returned sale was received on another branch, but is not yet #: confirmed. A product goes back to stock only after confirmation STATUS_PENDING = u'pending' #: This return was confirmed, meaning the product stock was increased. STATUS_CONFIRMED = u'confirmed' #: This returned sale was canceled, ie, The product stock is decreased back #: and the original sale still have the products. STATUS_CANCELLED = 'cancelled' statuses = collections.OrderedDict([ (STATUS_PENDING, _(u'Pending')), (STATUS_CONFIRMED, _(u'Confirmed')), (STATUS_CANCELLED, _(u'Cancelled')), ]) #: 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() #: Status of the returned sale status = EnumCol(default=STATUS_PENDING) #: the date this return was done return_date = DateTimeCol(default_factory=localnow) #: the date that the |returned sale| with the status pending was received confirm_date = DateTimeCol(default=None) # When this returned sale was undone undo_date = DateTimeCol(default=None) # FIXME: Duplicated from Invoice. Remove it #: the invoice number for this returning invoice_number = IntCol(default=None) #: the reason why this return was made reason = UnicodeCol(default=u'') #: The reason this returned sale was undone undo_reason = UnicodeCol(default=u'') sale_id = IdCol(default=None) #: the |sale| we're returning sale = Reference(sale_id, 'Sale.id') new_sale_id = IdCol(default=None) #: if not ``None``, :obj:`.sale` was traded for this |sale| new_sale = Reference(new_sale_id, 'Sale.id') responsible_id = IdCol() #: the |loginuser| responsible for doing this return responsible = Reference(responsible_id, 'LoginUser.id') confirm_responsible_id = IdCol() #: the |loginuser| responsible for receiving the pending return confirm_responsible = Reference(confirm_responsible_id, 'LoginUser.id') undo_responsible_id = IdCol() #: the |loginuser| responsible for undoing this returned sale. undo_responsible = Reference(undo_responsible_id, 'LoginUser.id') branch_id = IdCol() #: the |branch| in which this return happened branch = Reference(branch_id, 'Branch.id') #: a list of all items returned in this return returned_items = ReferenceSet('id', 'ReturnedSaleItem.returned_sale_id') #: |payments| generated by this returned sale payments = None #: |transporter| used in returned sale transporter = None invoice_id = IdCol() #: The |invoice| generated by the returned sale invoice = Reference(invoice_id, 'Invoice.id') def __init__(self, store=None, **kwargs): kwargs['invoice'] = Invoice(store=store, invoice_type=Invoice.TYPE_IN) super(ReturnedSale, self).__init__(store=store, **kwargs) @property def group(self): """|paymentgroup| for this return sale. Can return: * For a *trade*, use the |paymentgroup| from the replacement |sale|. * For a *devolution*, use the |paymentgroup| from the returned |sale|. """ if self.new_sale: return self.new_sale.group if self.sale: return self.sale.group return None @property def client(self): """The |client| of this return Note that this is the same as :obj:`.sale.client` """ return self.sale and self.sale.client @property def sale_total(self): """The current total amount of the |sale|. This is calculated by getting the :attr:`total amount <stoqlib.domain.sale.Sale.total_amount>` of the returned sale and subtracting the sum of :obj:`.returned_total` of all existing returns for the same sale. """ if not self.sale: return currency(0) # TODO: Filter by status returned = self.store.find(ReturnedSale, sale=self.sale) # This will sum the total already returned for this sale, # excluiding *self* within the same store returned_total = sum([ returned_sale.returned_total for returned_sale in returned if returned_sale != self ]) return currency(self.sale.total_amount - returned_total) @property def paid_total(self): """The total paid for this sale Note that this is the same as :meth:`stoqlib.domain.sale.Sale.get_total_paid` """ if not self.sale: return currency(0) return self.sale.get_total_paid() @property def returned_total(self): """The total being returned on this return This is done by summing the :attr:`ReturnedSaleItem.total` of all of this :obj:`returned items <.returned_items>` """ return currency(sum([item.total for item in self.returned_items])) @property def total_amount(self): """The total amount for this return See :meth:`.return_` for details of how this is used. """ return currency(self.sale_total - self.paid_total - self.returned_total) @property def total_amount_abs(self): """The absolute total amount for this return This is the same as abs(:attr:`.total_amount`). Useful for displaying it on a gui, just changing it's label to show if it's 'overpaid' or 'missing'. """ return currency(abs(self.total_amount)) # # IContainer implementation # def add_item(self, returned_item): assert not returned_item.returned_sale returned_item.returned_sale = self def get_items(self): return self.returned_items def remove_item(self, item): item.returned_sale = None self.store.maybe_remove(item) # # IInvoice implementation # @property def comments(self): return self.reason @property def discount_value(self): return currency(0) @property def invoice_subtotal(self): return self.returned_total @property def invoice_total(self): return self.returned_total @property def recipient(self): if self.sale.client: return self.sale.client.person return None @property def operation_nature(self): # TODO: Save the operation nature in new returned_sale table field. return _(u"Sale Return") # # Public API # @classmethod def get_pending_returned_sales(cls, store, branch): """Returns a list of pending |returned_sale| :param store: a store :param branch: the |branch| where the sale was made """ from stoqlib.domain.sale import Sale tables = [cls, Join(Sale, cls.sale_id == Sale.id)] # We want the returned_sale which sale was made on the branch # So we are comparing Sale.branch with |branch| to build the query return store.using(*tables).find( cls, And(cls.status == cls.STATUS_PENDING, Sale.branch == branch)) def is_pending(self): return self.status == ReturnedSale.STATUS_PENDING def is_undone(self): return self.status == ReturnedSale.STATUS_CANCELLED def can_undo(self): return self.status == ReturnedSale.STATUS_CONFIRMED def return_(self, method_name=u'money', login_user=None): """Do the return of this returned sale. :param unicode method_name: The name of the payment method that will be used to create this payment. If :attr:`.total_amount` is: * > 0, the client is returning more than it paid, we will create a |payment| with that value so the |client| can be reversed. * == 0, the |client| is returning the same amount that needs to be paid, so existing payments will be cancelled and the |client| doesn't owe anything to us. * < 0, than the payments need to be readjusted before calling this. .. seealso: :meth:`stoqlib.domain.sale.Sale.return_` as that will be called after that payment logic is done. """ assert self.sale and self.sale.can_return() self._clean_not_used_items() payment = None if self.total_amount == 0: # The client does not owe anything to us self.group.cancel() elif self.total_amount < 0: # The user has paid more than it's returning for payment in self.group.get_pending_payments(): if payment.is_inpayment(): # We are returning money to client, that means he doesn't owe # us anything, we do now. Cancel pending payments payment.cancel() method = PaymentMethod.get_by_name(self.store, method_name) description = _(u'%s returned for sale %s') % ( method.description, self.sale.identifier) payment = method.create_payment(Payment.TYPE_OUT, payment_group=self.group, branch=self.branch, value=self.total_amount_abs, description=description) payment.set_pending() if method_name == u'credit': payment.pay() # FIXME: For now, we are not reverting the comission as there is a # lot of things to consider. See bug 5215 for information about it. self._revert_fiscal_entry() self.sale.return_(self) # Save invoice number, operation_nature and branch in Invoice table. self.invoice.invoice_number = self.invoice_number self.invoice.operation_nature = self.operation_nature self.invoice.branch = self.branch if self.sale.branch == self.branch: self.confirm(login_user) def trade(self): """Do a trade for this return Almost the same as :meth:`.return_`, but unlike it, this won't generate reversed payments to the client. Instead, it'll generate an inpayment using :obj:`.returned_total` value, so it can be used as an "already paid quantity" on :obj:`.new_sale`. """ assert self.new_sale if self.sale: assert self.sale.can_return() self._clean_not_used_items() store = self.store group = self.group method = PaymentMethod.get_by_name(store, u'trade') description = _(u'Traded items for sale %s') % ( self.new_sale.identifier, ) value = self.returned_total self._return_items() value_as_discount = sysparam.get_bool('USE_TRADE_AS_DISCOUNT') if value_as_discount: self.new_sale.discount_value = self.returned_total else: payment = method.create_payment(Payment.TYPE_IN, group, self.branch, value, description=description) payment.set_pending() payment.pay() self._revert_fiscal_entry() if self.sale: self.sale.return_(self) def remove(self): """Remove this return and it's items from the database""" # XXX: Why do we remove this object from the database # We must remove children_items before we remove its parent_item for item in self.returned_items.find( Eq(ReturnedSaleItem.parent_item_id, None)): [ self.remove_item(child) for child in getattr(item, 'children_items') ] self.remove_item(item) self.store.remove(self) def confirm(self, login_user): """Receive the returned_sale_items from a pending |returned_sale| :param user: the |login_user| that received the pending returned sale """ assert self.status == self.STATUS_PENDING self._return_items() self.status = self.STATUS_CONFIRMED self.confirm_responsible = login_user self.confirm_date = localnow() def undo(self, reason): """Undo this returned sale. This includes removing the returned items from stock again (updating the quantity decreased on the sale). :param reason: The reason for this operation. """ assert self.can_undo() for item in self.get_items(): item.undo() # We now need to create a new in payment for the total amount of this # returned sale. method_name = self._guess_payment_method() method = PaymentMethod.get_by_name(self.store, method_name) description = _(u'%s return undone for sale %s') % ( method.description, self.sale.identifier) payment = method.create_payment(Payment.TYPE_IN, payment_group=self.group, branch=self.branch, value=self.returned_total, description=description) payment.set_pending() payment.pay() self.status = self.STATUS_CANCELLED self.cancel_date = localnow() self.undo_reason = reason # if the sale status is returned, we must reset it to confirmed (only # confirmed sales can be returned) if self.sale.is_returned(): self.sale.set_not_returned() # # Private # def _guess_payment_method(self): """Guesses the payment method used in this returned sale. """ value = self.returned_total # Now look for the out payment, ie, the payment that we possibly created # for the returned value. payments = list( self.sale.payments.find(payment_type=Payment.TYPE_OUT, value=value)) if len(payments) == 1: # There is only one payment that matches our criteria, we can trust it # is the one we are looking for. method = payments[0].method.method_name elif len(payments) == 0: # This means that the returned sale didn't endup creating any return # payment for the client. Let's just create a money payment then method = u'money' else: # This means that we found more than one return payment for this # value. This probably means that the user has returned multiple # items in different returns. methods = set(payment.method.method_name for payment in payments) if len(methods) == 1: # All returns were using the same method. Lets use that one them method = methods.pop() else: # The previous returns used different methods, let's pick money method = u'money' return method def _return_items(self): # We must have at least one item to return assert self.returned_items.count() # FIXME branch = get_current_branch(self.store) for item in self.returned_items: item.return_(branch) def _get_returned_percentage(self): return Decimal(self.returned_total / self.sale.total_amount) def _clean_not_used_items(self): store = self.store for item in self.returned_items: if not item.quantity: # Removed items not marked for return item.delete(item.id, store=store) def _revert_fiscal_entry(self): entry = self.store.find(FiscalBookEntry, payment_group=self.group, is_reversal=False).one() if not entry: return # FIXME: Instead of doing a partial reversion of fiscal entries, # we should be reverting the exact tax for each returned item. returned_percentage = self._get_returned_percentage() entry.reverse_entry(self.invoice_number, icms_value=entry.icms_value * returned_percentage, iss_value=entry.iss_value * returned_percentage, ipi_value=entry.ipi_value * returned_percentage)
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() # # Class methods # @classmethod def get_from_product(cls, product): return product.store.find(cls, product=product).one()
class Image(Domain): """Class responsible for storing images and it's description See also: `schema <http://doc.stoq.com.br/schema/tables/image.html>`__ """ __storm_table__ = 'image' (THUMBNAIL_SIZE_HEIGHT, THUMBNAIL_SIZE_WIDTH) = (128, 128) #: the image itself in a bin format image = BLOBCol(default=None) #: the image thumbnail in a bin format thumbnail = BLOBCol(default=None) #: the image description description = UnicodeCol(default=u'') #: The image filename filename = UnicodeCol(default=u'') #: The date that this image was uploaded to the database create_date = DateTimeCol(default_factory=StatementTimestamp) #: Some keywords for this image keywords = UnicodeCol(default=u'') #: Some notes about the image notes = UnicodeCol(default=u'') #: If this is the main image. Only makes sense if :obj:`.sellable` #: is not `None` is_main = BoolCol(default=False) #: If this image is only for internal use (i.e. it won't be synchronized #: to any e-commerce website to be displayed publicly) internal_use = BoolCol(default=False) sellable_id = IdCol(default=None) #: The |sellable| that this image belongs to sellable = Reference(sellable_id, 'Sellable.id') category_id = IdCol(default=None) #: The |category| that this image belongs to category = Reference(category_id, 'SellableCategory.id') station_type_id = IdCol(default=None) #: The station type this image should be used instead of the main image. station_type = Reference(station_type_id, 'StationType.id') # # Public API # def get_base64_encoded(self): return base64.b64encode(self.image).decode() # # IDescribable implementation # def get_description(self): if self.description: return self.description return _(u"Stoq image") # # ORMObject # @classmethod def delete(cls, id, store): image = store.get(cls, id) ImageRemoveEvent.emit(image) store.remove(image) # # Domain # def on_create(self): ImageCreateEvent.emit(self) def on_update(self): ImageEditEvent.emit(self)
class InventoryItem(Domain): """The InventoryItem belongs to an Inventory. It contains the recorded quantity and the actual quantity related to a specific product. If those quantities are not identitical, it will also contain a reason and a cfop describing that. See also: `schema <http://doc.stoq.com.br/schema/tables/inventory_item.html>`__ """ __storm_table__ = 'inventory_item' product_id = IntCol() #: the item product = Reference(product_id, 'Product.id') batch_id = IntCol() #: If the product is a storable, the |batch| of the product that is being #: inventored batch = Reference(batch_id, 'StorableBatch.id') #: the recorded quantity of a product recorded_quantity = QuantityCol() #: the actual quantity of a product actual_quantity = QuantityCol(default=None) #: the product's cost when the product was adjusted. product_cost = PriceCol() #: the reason of why this item has been adjusted reason = UnicodeCol(default=u"") cfop_data_id = IntCol(default=None) #: the cfop used to adjust this item, this is only set when #: an adjustment is done cfop_data = Reference(cfop_data_id, 'CfopData.id') inventory_id = IntCol() #: the inventory process that contains this item inventory = Reference(inventory_id, 'Inventory.id') def _add_inventory_fiscal_entry(self, invoice_number): inventory = self.inventory return FiscalBookEntry( entry_type=FiscalBookEntry.TYPE_INVENTORY, invoice_number=inventory.invoice_number, branch=inventory.branch, cfop=self.cfop_data, store=self.store) def adjust(self, invoice_number): """Create an entry in fiscal book registering the adjustment with the related cfop data and change the product quantity available in stock. :param invoice_number: invoice number to register """ storable = self.product.storable if storable is None: raise TypeError( "The adjustment item must be a storable product.") adjustment_qty = self.get_adjustment_quantity() if not adjustment_qty: return elif adjustment_qty > 0: storable.increase_stock(adjustment_qty, self.inventory.branch, StockTransactionHistory.TYPE_INVENTORY_ADJUST, self.id) else: storable.decrease_stock(abs(adjustment_qty), self.inventory.branch, StockTransactionHistory.TYPE_INVENTORY_ADJUST, self.id) self._add_inventory_fiscal_entry(invoice_number) def adjusted(self): """Find out if this item has been adjusted. :returns: ``True`` if the item have already been adjusted, ``False`` otherwise. """ # We check reason and cfop_data attributes because they only # exist after the item be adjusted return self.reason and self.cfop_data def get_code(self): """Get the product code of this item :returns: the product code """ return self.product.sellable.code def get_description(self): """Returns the product description""" return self.product.sellable.get_description() def get_fiscal_description(self): """Returns a description of the product tax constant""" return self.product.sellable.tax_constant.get_description() def get_unit_description(self): """Returns the product unit description or None if it's not set """ sellable = self.product.sellable if sellable.unit: return sellable.unit.description def get_adjustment_quantity(self): """Returns the adjustment quantity, the actual quantity minus the recorded quantity or None if there is no actual quantity yet. """ if self.actual_quantity is not None: return self.actual_quantity - self.recorded_quantity def get_total_cost(self): """Returns the total cost of this item, the actual quantity multiplied by the product cost in the moment it was adjusted. If the item was not adjusted yet, the total cost will be zero. """ if not self.adjusted(): return Decimal(0) return self.product_cost * self.actual_quantity
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 Invoice(Domain): """Stores information about invoices""" __storm_table__ = 'invoice' TYPE_IN = u'in' TYPE_OUT = u'out' NFE_MODE = u'nfe' NFCE_MODE = u'nfce' MODES = {55: NFE_MODE, 65: NFCE_MODE} #: the invoice number invoice_number = IntCol() #: the operation nature operation_nature = UnicodeCol() #: the invoice type, representing an IN/OUT operation invoice_type = EnumCol(allow_none=False) #: the invoice series series = IntCol(default=None) #: the invoice mode mode = EnumCol(default=None) #: the key generated by NF-e plugin key = UnicodeCol() #: numeric code randomly generated for each NF-e cnf = UnicodeCol() branch_id = IdCol() #: the |branch| where this invoice was generated branch = Reference(branch_id, 'Branch.id') def __init__(self, **kw): if not 'branch' in kw: kw['branch'] = get_current_branch(kw.get('store')) super(Invoice, self).__init__(**kw) # The mode and series are only set if the nfce plugin is active, since # the invoice_number will be also set by it. if get_plugin_manager().is_active('nfce'): mode = InvoiceGetModeEvent.emit() assert mode self.mode = mode # TODO Handle series number self.series = 1 @classmethod def get_next_invoice_number(cls, store, mode=None): return Invoice.get_last_invoice_number(store, mode) + 1 @classmethod def get_last_invoice_number(cls, store, mode=None): """Returns the last invoice number. If there is not an invoice number used, the returned value will be zero. :param store: a store :returns: an integer representing the last sale invoice number """ current_branch = get_current_branch(store) last = store.find(cls, branch=current_branch, mode=mode).max(cls.invoice_number) return last or 0 def save_nfe_info(self, cnf, key): """ Save the CNF and KEY generated in NF-e. """ self.cnf = cnf self.key = key def check_unique_invoice_number_by_branch(self, invoice_number, branch, mode): """Check if the invoice_number is used in determined branch """ queries = { Invoice.invoice_number: invoice_number, Invoice.branch_id: branch.id, Invoice.mode: mode } return self.check_unique_tuple_exists(queries) def check_invoice_info_consistency(self): """If the invoice number is set, series and mode should also be. We should have a database constraint for this, but since these three data isn't saved at once, the constraint would brake every time. """ assert ((self.invoice_number and self.series and self.mode) or (not self.invoice_number and not self.series and not self.mode)) def on_create(self): self.check_invoice_info_consistency() def on_update(self): self.check_invoice_info_consistency()
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 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)