class CommissionSource(Domain): """Commission Source object implementation A CommissionSource is tied to a |sellablecategory| or |sellable|, it's used to determine the value of a commission for a certain item which is sold. There are two different commission values defined here, one which is used when the item is sold directly, eg one installment and another one which is used when the item is sold in installments. The category and the sellable should not exist when sellable exists and the opposite is true. See also: `schema <http://doc.stoq.com.br/schema/tables/commission_source.html>`__, """ __storm_table__ = 'commission_source' #: the commission value to be used in a |sale| with one installment direct_value = PercentCol() #: the commission value to be used in a |sale| with multiple installments installments_value = PercentCol() category_id = IdCol(default=None) #: the |sellablecategory| category = Reference(category_id, 'SellableCategory.id') sellable_id = IdCol(default=None) #: the |sellable| sellable = Reference(sellable_id, 'Sellable.id')
class PaymentMethod(Domain): __storm_table__ = 'payment_method' method_name = UnicodeCol() is_active = BoolCol(default=True) daily_penalty = PercentCol(default=0) interest = PercentCol(default=0) payment_day = IntCol(default=None) closing_day = IntCol(default=None) max_installments = IntCol(default=1) destination_account_id = IntCol()
class BaseICMS(BaseTax): """NfeProductIcms stores the default values that will be used when creating NfeItemIcms objects """ # FIXME: this is only used by pylint __storm_table__ = 'invalid' orig = IntCol(default=None) cst = IntCol(default=None) mod_bc = IntCol(default=None) p_icms = PercentCol(default=None) mod_bc_st = IntCol(default=None) p_mva_st = PercentCol(default=None) p_red_bc_st = PercentCol(default=None) p_icms_st = PercentCol(default=None) p_red_bc = PercentCol(default=None) bc_include_ipi = BoolCol(default=True) bc_st_include_ipi = BoolCol(default=True) # Funco de Combate à Pobreza p_fcp = PercentCol(default=None) p_fcp_st = PercentCol(default=None) # Simples Nacional csosn = IntCol(default=None) p_cred_sn = PercentCol(default=None)
class SellableTaxConstant(Domain): """A tax constant tied to a sellable """ implements(IDescribable) __storm_table__ = 'sellable_tax_constant' description = UnicodeCol() tax_type = IntCol() tax_value = PercentCol(default=None) _mapping = { int(TaxType.NONE): u'TAX_NONE', # Não tributado - ICMS int(TaxType.EXEMPTION): u'TAX_EXEMPTION', # Isento - ICMS int(TaxType.SUBSTITUTION): u'TAX_SUBSTITUTION', # Substituição tributária - ICMS int(TaxType.SERVICE): u'TAX_SERVICE', # ISS } def get_value(self): return SellableTaxConstant._mapping.get(self.tax_type, self.tax_value) @classmethod def get_by_type(cls, tax_type, store): """Fetch the tax constant for tax_type :param tax_type: the tax constant to fetch :param store: a store :returns: a |sellabletaxconstant| or ``None`` if none is found """ return store.find(SellableTaxConstant, tax_type=int(tax_type)).one() # IDescribable def get_description(self): return self.description
class SellableBranchOverride(Domain): __storm_table__ = 'sellable_branch_override' status = EnumCol() base_price = PriceCol() price_last_updated = DateTimeCol() max_discount = PercentCol() tax_constant_id = IdCol() tax_constant = Reference(tax_constant_id, 'SellableTaxConstant.id') default_sale_cfop_id = IdCol() default_sale_cfop = Reference(default_sale_cfop_id, 'CfopData.id') on_sale_price = PriceCol() on_sale_start_date = DateTimeCol() on_sale_end_date = DateTimeCol() branch_id = IdCol() branch = Reference(branch_id, 'Branch.id') sellable_id = IdCol() sellable = Reference(sellable_id, 'Sellable.id')
class SellableBranchOverride(Domain): __storm_table__ = 'sellable_branch_override' status = EnumCol() base_price = PriceCol() price_last_updated = DateTimeCol() max_discount = PercentCol() tax_constant_id = IdCol() tax_constant = Reference(tax_constant_id, 'SellableTaxConstant.id') default_sale_cfop_id = IdCol() default_sale_cfop = Reference(default_sale_cfop_id, 'CfopData.id') on_sale_price = PriceCol() on_sale_start_date = DateTimeCol() on_sale_end_date = DateTimeCol() branch_id = IdCol() branch = Reference(branch_id, 'Branch.id') sellable_id = IdCol() sellable = Reference(sellable_id, 'Sellable.id') #: specifies whether the product requires kitchen production requires_kitchen_production = BoolCol() @classmethod def find_by_sellable(cls, sellable, branch): return sellable.store.find(cls, sellable=sellable, branch=branch).one()
class UserProfile(Domain): """User profile definition.""" __storm_table__ = 'user_profile' #: Name of the user profile. name = UnicodeCol() #: Profile settings that describes the access this profile has to an app. profile_settings = ReferenceSet('id', 'ProfileSettings.user_profile_id') #: Maximum discount this profile can allow to sale items. max_discount = PercentCol(default=0) @classmethod def create_profile_template(cls, store, name, has_full_permission=False): profile = cls(store=store, name=name) descr = get_utility(IApplicationDescriptions) for app_dir in descr.get_application_names(): ProfileSettings(store=store, has_permission=has_full_permission, app_dir_name=app_dir, user_profile=profile) return profile @classmethod def get_default(cls, store): # FIXME: We need a way to set the default profile in the interface, # instead of relying on the name (the user may change it) profile = store.find(cls, name=_(u'Salesperson')).one() # regression: check if it was not created in english. if not profile: profile = store.find(cls, name=u'Salesperson').one() # Just return any other profile, so that the user is created with # one. if not profile: profile = store.find(cls).any() return profile def add_application_reference(self, app_name, has_permission=False): store = self.store ProfileSettings(store=store, app_dir_name=app_name, has_permission=has_permission, user_profile=self) def check_app_permission(self, app_name): """Check if the user has permission to use an application :param app_name: name of application to check """ store = self.store return bool( store.find(ProfileSettings, user_profile=self, app_dir_name=app_name, has_permission=True).one())
class CardOperationCost(Domain): __storm_table__ = 'card_operation_cost' device_id = IntCol(default=None) device = Reference(device_id, CardPaymentDevice.id) provider_id = IntCol(default=None) card_type = IntCol(default=0) installment_start = IntCol(default=1) installment_end = IntCol(default=1) payment_days = IntCol(default=30) fee = PercentCol(default=0) fare = PriceCol(default=0)
class ServiceBranchOverride(Domain): __storm_table__ = 'service_branch_override' city_taxation_code = UnicodeCol() service_list_item_code = UnicodeCol() p_iss = PercentCol() branch_id = IdCol() branch = Reference(branch_id, 'Branch.id') service_id = IdCol() service = Reference(service_id, 'Service.id')
class BaseCOFINS(BaseTax): """Contains attributes to be used to calculate PIS tax in Brazil.""" CALC_PERCENTAGE = u'percentage' CALC_VALUE = u'value' cst = IntCol(default=None) #: Operation type (percentage or value) calculo = EnumCol(default=CALC_PERCENTAGE, allow_none=False) #: Aliquot in percentage p_cofins = PercentCol(default=None)
class BaseIPI(BaseTax): (CALC_ALIQUOTA, CALC_UNIDADE) = range(2) cl_enq = UnicodeCol(default=u'') cnpj_prod = UnicodeCol(default=u'') c_selo = UnicodeCol(default=u'') q_selo = IntCol(default=None) c_enq = UnicodeCol(default=u'') cst = IntCol(default=None) p_ipi = PercentCol(default=None) q_unid = QuantityCol(default=None) calculo = IntCol(default=CALC_ALIQUOTA)
class BaseIPI(BaseTax): CALC_ALIQUOTA = u'aliquot' CALC_UNIDADE = u'unit' cl_enq = UnicodeCol(default=u'') cnpj_prod = UnicodeCol(default=u'') c_selo = UnicodeCol(default=u'') q_selo = IntCol(default=None) c_enq = UnicodeCol(default=u'') cst = IntCol(default=None) p_ipi = PercentCol(default=None) q_unid = QuantityCol(default=None) calculo = EnumCol(default=CALC_ALIQUOTA, allow_none=False)
class SellableTaxConstant(Domain): """A tax constant tied to a sellable See also: `schema <http://doc.stoq.com.br/schema/tables/sellable_tax_constant.html>`__ """ __storm_table__ = 'sellable_tax_constant' #: description of this constant description = UnicodeCol() #: a TaxType constant, used by ECF tax_type = IntCol() #: the percentage value of the tax tax_value = PercentCol(default=None) _mapping = { int(TaxType.NONE): u'TAX_NONE', # Não tributado - ICMS int(TaxType.EXEMPTION): u'TAX_EXEMPTION', # Isento - ICMS int(TaxType.SUBSTITUTION): u'TAX_SUBSTITUTION', # Substituição tributária - ICMS int(TaxType.SERVICE): u'TAX_SERVICE', # ISS } def get_value(self): """ :returns: the value to pass to ECF """ return SellableTaxConstant._mapping.get(self.tax_type, self.tax_value) @classmethod def get_by_type(cls, tax_type, store): """Fetch the tax constant for tax_type :param tax_type: the tax constant to fetch :param store: a store :returns: a |sellabletaxconstant| or ``None`` if none is found """ return store.find(SellableTaxConstant, tax_type=int(tax_type)).one() # IDescribable def get_description(self): return self.description
class ClientCategoryPrice(Domain): """A table that stores special prices for |clients| based on their |clientcategory|. See also: `schema <http://doc.stoq.com.br/schema/tables/client_category_price.html>`__ """ __storm_table__ = 'client_category_price' sellable_id = IdCol() #: The |sellable| that has a special price sellable = Reference(sellable_id, 'Sellable.id') category_id = IdCol() #: The |clientcategory| that has the special price category = Reference(category_id, 'ClientCategory.id') #: The price for this (|sellable|, |clientcategory|) price = PriceCol(default=0) #: The max discount that may be applied. max_discount = PercentCol(default=0) @property def markup(self): if self.sellable.cost == 0: return Decimal(0) return ((self.price / self.sellable.cost) - 1) * 100 @markup.setter def markup(self, markup): self.price = self.sellable._get_price_by_markup(markup) @property def category_name(self): return self.category.name def remove(self): """Removes this client category price from the database.""" self.store.remove(self)
class ClientCategoryPrice(Domain): """A table that stores special prices for clients based on their category. """ __storm_table__ = 'client_category_price' sellable_id = IntCol() #: The sellable that has a special price sellable = Reference(sellable_id, 'Sellable.id') category_id = IntCol() #: The category that has the special price category = Reference(category_id, 'ClientCategory.id') #: The price for this (sellable, category) price = PriceCol(default=0) #: The max discount that may be applied. max_discount = PercentCol(default=0) def _get_markup(self): if self.sellable.cost == 0: return Decimal(0) return ((self.price / self.sellable.cost) - 1) * 100 def _set_markup(self, markup): self.price = self.sellable._get_price_by_markup(markup) markup = property(_get_markup, _set_markup) @property def category_name(self): return self.category.name def remove(self): """Removes this client category price from the database.""" self.store.remove(self)
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 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 = {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) #: 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') image_id = IdCol(default=None) #: the |image|, a picture representing the sellable image = Reference(image_id, 'Image.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) 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 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: 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'), ('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 max(user_discount, info.max_discount) 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): """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.icms_template SellableCheckTaxesEvent.emit(self) 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 and sysparam.get_bool( 'UPDATE_PRODUCT_COST_ON_COMPONENT_UPDATE')): self.product.update_production_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() 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 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() #: 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 SellableCategory(Domain): """ Sellable category. This class can represents a sellable's category as well its base category. """ __storm_table__ = 'sellable_category' #: The category description description = UnicodeCol() #: 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 = IntCol(default=None) #: base category of this category, ``None`` for base categories themselves category = Reference(category_id, 'SellableCategory.id') tax_constant_id = IntCol(default=None) tax_constant = Reference(tax_constant_id, 'SellableTaxConstant.id') children = ReferenceSet('id', 'SellableCategory.category_id') implements(IDescribable) # # Properties # @property def full_description(self): 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 Sellable(Domain): """ Sellable information of a certain item such a product or a service. Note that sellable is not actually a concrete item but only its reference as a sellable. Concrete items are created by IContainer routines. """ __storm_table__ = 'sellable' implements(IDescribable) #: the sellable is available and can be used on a |purchase|/|sale| STATUS_AVAILABLE = 0 #: 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 = 1 statuses = {STATUS_AVAILABLE: _(u'Available'), STATUS_CLOSED: _(u'Closed')} #: an internal code identifying the sellable in Stoq 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 = IntCol(default=STATUS_AVAILABLE) #: cost of the sellable cost = PriceCol(default=0) #: price of sellable, how much the client is charged base_price = PriceCol(default=0) #: full description of sallable description = UnicodeCol(default=u'') #: maximum discount allowed max_discount = PercentCol(default=0) #: commission to pay after selling this sellable commission = PercentCol(default=0) notes = UnicodeCol(default=u'') unit_id = IntCol(default=None) #: unit of the sellable, kg/l etc unit = Reference(unit_id, 'SellableUnit.id') image_id = IntCol(default=None) image = Reference(image_id, 'Image.id') category_id = IntCol(default=None) #: a reference to category table category = Reference(category_id, 'SellableCategory.id') tax_constant_id = IntCol(default=None) tax_constant = Reference(tax_constant_id, 'SellableTaxConstant.id') product = Reference('id', 'Product.sellable_id', on_remote=True) service = Reference('id', 'Service.sellable_id', on_remote=True) default_sale_cfop_id = IntCol(default=None) 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) on_sale_start_date = DateTimeCol(default=None) on_sale_end_date = DateTimeCol(default=None) 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))))) def _get_status_string(self): if not self.status in self.statuses: raise DatabaseInconsistency( _('Invalid status for product got %d') % self.status) return self.statuses[self.status] # # Properties # @property def product_storable(self): """If this is a |product| and has stock, fetch the |storable| for this. This is a shortcut to avoid having to do multiple queries and check if |product| is set before fetching the |storable|. :returns: The |storable| or ``None`` if there isn't one """ from stoqlib.domain.product import Product, Storable return self.store.find( Storable, And(Storable.product_id == Product.id, Product.sellable_id == self.id)).one() @property def has_image(self): return bool(self.image and self.image.image) def _get_markup(self): if self.cost == 0: return Decimal(0) return ((self.price / self.cost) - 1) * 100 def _set_markup(self, markup): self.price = self._get_price_by_markup(markup) #: ((cost/price)-1)*100 markup = property(_get_markup, _set_markup) def _get_price(self): if self.on_sale_price: today = localnow() start_date = self.on_sale_start_date end_date = self.on_sale_end_date if is_date_in_interval(today, start_date, end_date): return self.on_sale_price return self.base_price def _set_price(self, price): if price < 0: # Just a precaution for gui validation fails. price = 0 if self.on_sale_price: today = localnow() start_date = self.on_sale_start_date end_date = self.on_sale_end_date if is_date_in_interval(today, start_date, end_date): self.on_sale_price = price return self.base_price = price price = property(_get_price, _set_price) # # Accessors # def is_available(self): """Whether the sellable is available and can be sold. :returns: if the item can be sold :rtype: boolean """ # FIXME: Perhaps this should be done elsewhere. Johan 2008-09-26 if self.service == sysparam(self.store).DELIVERY_SERVICE: return True return self.status == self.STATUS_AVAILABLE def set_available(self): """Mark the sellable as 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""" if self.is_closed(): raise ValueError('This sellable is already closed') assert self.can_close() self.status = Sellable.STATUS_CLOSED 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| """ from stoqlib.domain.sale import SaleItem if self.store.find(SaleItem, sellable=self).count(): # FIXME: Find a better way of doing this. # Quotes (and maybe other cases) don't go to the history, # so make sure there's nothing left on SaleItem referencing # this sellable. return False # If the product is in a purchase. from stoqlib.domain.purchase import PurchaseItem if self.store.find(PurchaseItem, sellable=self).count(): return False if self.product: return self.product.can_remove() elif self.service: return self.service.can_remove() return False 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_short_description(self): """Returns a short description of the current sellable :returns: description :rtype: string """ return u'%s %s' % (self.id, self.description) 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_unit_description(self): """Returns the sellable category description :returns: the category description or an empty string if no category was set. """ return self.unit and self.unit.description or u"" 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 """ 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. """ 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 check_code_exists(self, code): """Returns ``True`` if we already have a sellable with the given code in the database. """ return self.check_unique_value_exists(Sellable.code, code) def check_barcode_exists(self, barcode): """Returns ``True`` if we already have a sellable with the given barcode in the database. """ return self.check_unique_value_exists(Sellable.barcode, barcode) def check_taxes_validity(self): """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. This method will raise TaxError if there are any issues with the sellable taxes. """ icms_template = self.product and self.product.icms_template 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_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. :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): """Returns True if the new price respects the maximum discount configured for the sellable, otherwise returns ``False``. :param newprice: The new price that we are trying to sell this sellable for. :param category: Optionally define a category that we will get the price info from. """ info = None if category: info = self.get_category_price_info(category) if not info: info = self if newprice < info.price - (info.price * info.max_discount / 100): return False return True # # 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() # # Classmethods # def remove(self): """Remove this sellable from the database (including the |product| or |service|). """ 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() if self.product: self.product.remove() elif self.service: self.service.remove() self.store.remove(self) @classmethod def get_available_sellables_query(cls, store): service_sellable = sysparam(store).DELIVERY_SERVICE.sellable return And(cls.id != service_sellable.id, cls.status == cls.STATUS_AVAILABLE) @classmethod def get_available_sellables(cls, store): """Returns sellable objects which can be added in a |sale|. By default a delivery sellable can not be added manually by users since a separate dialog is responsible for that. """ 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 """ from stoqlib.domain.product import Product, ProductSupplierInfo query = And(cls.get_available_sellables_query(store), cls.id == Product.sellable_id, Product.consignment == consigned) if storable: from stoqlib.domain.product import Storable query = And(query, Sellable.id == Product.sellable_id, Storable.product_id == Product.id) if supplier: query = And(query, Sellable.id == Product.sellable_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| """ query = cls.get_unblocked_sellables_query(store, storable, supplier, consigned) return store.find(cls, query) @classmethod def get_unblocked_by_categories(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 """ # FIXME: This query should be faster, waiting for #3696 if include_uncategorized: categories.append(None) for sellable in cls.get_unblocked_sellables(store): if sellable.category in categories: yield sellable
class InvoiceItemIcms(BaseICMS): __storm_table__ = 'invoice_item_icms' v_bc = PriceCol(default=None) v_icms = PriceCol(default=None) v_bc_st = PriceCol(default=None) v_icms_st = PriceCol(default=None) # Fundo de Combate à Pobreza v_fcp = PriceCol(default=None) v_fcp_st = PriceCol(default=None) v_fcp_st_ret = PriceCol(default=None) # Alíquota suportada pelo Consumidor Final (FCP + ICMS) p_st = PercentCol(default=None) # Simples Nacional v_cred_icms_sn = PriceCol(default=None) v_bc_st_ret = PriceCol(default=None) v_icms_st_ret = PriceCol(default=None) def _calc_cred_icms_sn(self, invoice_item): if self.p_cred_sn >= 0: self.v_cred_icms_sn = invoice_item.get_total() * self.p_cred_sn / 100 def _calc_st(self, invoice_item): self.v_bc_st = quantize(invoice_item.price * invoice_item.quantity) if self.bc_st_include_ipi and invoice_item.ipi_info: self.v_bc_st += invoice_item.ipi_info.v_ipi if self.p_red_bc_st is not None: self.v_bc_st -= self.v_bc_st * self.p_red_bc_st / 100 if self.p_mva_st is not None: self.v_bc_st += self.v_bc_st * self.p_mva_st / 100 if self.v_bc_st is not None and self.p_icms_st is not None: self.v_icms_st = self.v_bc_st * self.p_icms_st / 100 if self.v_icms is not None and self.v_icms_st is not None: self.v_icms_st -= self.v_icms if self.v_bc_st is not None and self.p_fcp_st is not None: self.v_fcp_st = self.v_bc_st * self.p_fcp_st / 100 if self.v_fcp is not None and self.v_fcp_st is not None: self.v_fcp_st -= self.v_fcp def _calc_normal(self, invoice_item): self.v_bc = quantize(invoice_item.price * invoice_item.quantity) if self.bc_include_ipi and invoice_item.ipi_info: self.v_bc += invoice_item.ipi_info.v_ipi if self.p_red_bc is not None: self.v_bc -= self.v_bc * self.p_red_bc / 100 if self.p_icms is not None and self.v_bc is not None: self.v_icms = self.v_bc * self.p_icms / 100 if self.p_fcp is not None and self.v_bc is not None: self.v_fcp = self.v_bc * self.p_fcp / 100 def _update_normal(self, invoice_item): """Atualiza os dados de acordo com os calculos do Regime Tributário Normal (Não simples) """ if self.cst == 0: self.p_red_bc = Decimal(0) self._calc_normal(invoice_item) elif self.cst == 10: self.p_red_bc = Decimal(0) self._calc_normal(invoice_item) self._calc_st(invoice_item) elif self.cst == 20: self._calc_normal(invoice_item) elif self.cst == 30: self.v_icms = 0 self.v_bc = 0 self._calc_st(invoice_item) elif self.cst in (40, 41, 50): self.v_icms = 0 self.v_bc = 0 elif self.cst == 51: self._calc_normal(invoice_item) elif self.cst == 60: self.v_bc_st_ret = 0 self.v_icms_st_ret = 0 self.v_fcp_st_ret = 0 if self.p_fcp_st is not None and self.p_icms_st is not None: self.p_st = self.p_fcp_st + self.p_icms_st elif self.cst in (70, 90): self._calc_normal(invoice_item) self._calc_st(invoice_item) def _update_simples(self, invoice_item): if self.csosn in [300, 400, 500]: self.v_bc_st_ret = 0 self.v_icms_st_ret = 0 self.v_fcp_st_ret = 0 if self.p_fcp_st is not None and self.p_icms_st is not None: self.p_st = self.p_fcp_st + self.p_icms_st if self.csosn in [101, 201]: if self.p_cred_sn is None: self.p_cred_sn = Decimal(0) self._calc_cred_icms_sn(invoice_item) if self.csosn in [201, 202, 203]: self._calc_st(invoice_item) if self.csosn == 900: if self.p_cred_sn is None: self.p_cred_sn = Decimal(0) self._calc_cred_icms_sn(invoice_item) self._calc_normal(invoice_item) self._calc_st(invoice_item) def update_values(self, invoice_item): branch = invoice_item.parent.branch # Simples nacional if branch.crt in [1, 2]: self._update_simples(invoice_item) else: self._update_normal(invoice_item) @classmethod def get_tax_template(cls, invoice_item): return invoice_item.sellable.product.icms_template
class CreditCardData(Domain): """Stores CreditCard specific state related to a payment This state include: * The type of the card used * The |creditprovider| of the card * The |carddevice| used to charge the user * The costs (fare an fee) that the shop was charged from the |creditprovider| for this payment """ __storm_table__ = 'credit_card_data' #: Credit card payment, single installment TYPE_CREDIT = 0 #: Debit card payment TYPE_DEBIT = 1 #: Credit card payment with two or more installments. #: In this case, the shop is responsible for the installments, and will #: receive one payment each month TYPE_CREDIT_INSTALLMENTS_STORE = 2 #: Credit card payment with two or more installments. #: In this case, the credit provider is responsible for the installments and #: the shop will receive the value in only one payment TYPE_CREDIT_INSTALLMENTS_PROVIDER = 3 #: This is a debit card payment, but will be charged on a pre-defined future #: date. Not completely supported in Stoq yet TYPE_DEBIT_PRE_DATED = 4 types = { TYPE_CREDIT: _(u'Credit Card'), TYPE_DEBIT: _(u'Debit Card'), TYPE_CREDIT_INSTALLMENTS_STORE: _(u'Credit Card Installments Store'), TYPE_CREDIT_INSTALLMENTS_PROVIDER: _(u'Credit Card Installments ' u'Provider'), TYPE_DEBIT_PRE_DATED: _(u'Debit Card Pre-dated'), } short_desc = { TYPE_CREDIT: _(u'Credit'), TYPE_DEBIT: _(u'Debit'), # translators: This is 'Credit Card Installments Store, but should be # abbreviated to fit a small space TYPE_CREDIT_INSTALLMENTS_STORE: _(u'Credit Inst. Store'), # translators: This is 'Credit Card Installments Provider, but should be # abbreviated to fit a small space TYPE_CREDIT_INSTALLMENTS_PROVIDER: _(u'Credit Inst. Provider'), TYPE_DEBIT_PRE_DATED: _(u'Debit Pre-dated'), } payment_id = IntCol() #: the |payment| this information is about payment = Reference(payment_id, 'Payment.id') #: int, > 0, < 5 card_type = IntCol(default=TYPE_CREDIT) provider_id = IntCol(default=None) #: the |creditprovider| for this class provider = Reference(provider_id, 'CreditProvider.id') device_id = IntCol(default=None) #: the |carddevice| used for the payment #: If the |carddevice| is excluded in the future, this value will be set to null. device = Reference(device_id, 'CardPaymentDevice.id') #: the fixed value that will be charged for the related |payment| fare = PriceCol(default=0) #: the percentage of the value that will be charged for the related |payment| fee = PercentCol(default=0) #: the fee that will be charged based on the :obj:`.fee` fee_value = PriceCol(default=0) #: this is used by the tef plugin. nsu = IntCol(default=None) #: The authorization number returned by the payment device. This will be #: returned automatically by the tef plugin, but needs to be manually #: informed if not using the plugin. auth = IntCol(default=None) #: the number of installments, used by the tef plugin installments = IntCol(default=1) #: the value of the first installment (when installments > 1), used by the #: tef plugin entrance_value = PriceCol(default=0)
class CardOperationCost(Domain): """The cost of a given operation on the |carddevice| The cost of an operation depend on the following parameters: * The |carddevice| that was used * The |creditprovider| of the card * The type of the card (ie, credit, debit, etc..) * The number of installments """ __storm_table__ = 'card_operation_cost' device_id = IdCol(default=None) #: The card device used to charge the client device = Reference(device_id, 'CardPaymentDevice.id') provider_id = IdCol(default=None) #: The credit provider of the card provider = Reference(provider_id, 'CreditProvider.id') # One of CreditCardData.TYPE_* card_type = EnumCol(allow_none=False, default=u'credit') #: When paid in installments, this fee and fare will only apply if the #: installments number is in the range defined by installment_start and #: installment_end installment_start = IntCol(default=1) #: See :obj:`.installment_start` installment_end = IntCol(default=1) #: How many days the |creditprovider| takes to transfer the shop the money for #: one |payment| payment_days = IntCol(default=30) #: The percentage of each |payment| value that will be charged by the #: |creditprovider| fee = PercentCol(default=0) #: This is a fixed currency value that is charged for each |payment| fare = PriceCol(default=0) # # Properties # def get_description(self): type_desc = CreditCardData.short_desc[self.card_type] desc = u'%s %s' % (self.provider.short_name, type_desc) return desc @property def installment_range_as_string(self): """A string representation of the installments range """ inst_type = [ CreditCardData.TYPE_CREDIT_INSTALLMENTS_STORE, CreditCardData.TYPE_CREDIT_INSTALLMENTS_PROVIDER ] if self.card_type not in inst_type: return u'' return _(u'From %d to %d') % (self.installment_start, self.installment_end) @classmethod def delete_from_device(cls, device_id, store): store.execute(Delete(cls.device_id == device_id, cls)) @classmethod def validate_installment_range(cls, device, provider, card_type, start, end, store, ignore=None): """Checks if a given range is not conflicting with any other operation cost :param device: the |carddevice| that will be used :param provider: the |creditprovider| related to the cost :param card_type: the car type (credit, debit, etc...) :param start: the start of the installment range :param end: the end of the installment range :param ignore: if not ``None``, should be an id of a |cardcost| that should be ignored in the query (ie, the object currently being edited). :returns: ``True`` the range is valid for the given parameters. A valid range means that for every possible installment value in the given range, there are no other |cardcost| objects that matches the installment value. """ assert start <= end, (start, end) # For each possible value in the range, we want to see if there is any # other operation cost that already include this value. # range() end is non inclusive, hence the +1 exprs = [] for i in range(start, end + 1): # start <= i <= end inst_query = And(CardOperationCost.installment_start <= i, i <= CardOperationCost.installment_end) exprs.append(inst_query) query = And(CardOperationCost.device == device, CardOperationCost.card_type == card_type, CardOperationCost.provider == provider, Or(*exprs)) if ignore is not None: query = And(query, CardOperationCost.id != ignore) # For this range to be valid, there should be object matching the # criteria above return store.find(cls, query).is_empty()
class CreditCardData(Domain): """Stores CreditCard specific state related to a payment This state include: * The type of the card used * The |creditprovider| of the card * The |carddevice| used to charge the user * The costs (fare an fee) that the shop was charged from the |creditprovider| for this payment """ __storm_table__ = 'credit_card_data' #: Credit card payment, single installment TYPE_CREDIT = u'credit' #: Debit card payment TYPE_DEBIT = u'debit' #: Credit card payment with two or more installments. #: In this case, the shop is responsible for the installments, and will #: receive one payment each month TYPE_CREDIT_INSTALLMENTS_STORE = u'credit-inst-store' #: Credit card payment with two or more installments. #: In this case, the credit provider is responsible for the installments and #: the shop will receive the value in only one payment TYPE_CREDIT_INSTALLMENTS_PROVIDER = u'credit-inst-provider' #: This is a debit card payment, but will be charged on a pre-defined future #: date. Not completely supported in Stoq yet TYPE_DEBIT_PRE_DATED = u'debit-pre-dated' types = collections.OrderedDict([ (TYPE_CREDIT, _(u'Credit Card')), (TYPE_DEBIT, _(u'Debit Card')), (TYPE_CREDIT_INSTALLMENTS_STORE, _(u'Credit Card Installments Store')), (TYPE_CREDIT_INSTALLMENTS_PROVIDER, _(u'Credit Card Installments ' u'Provider')), (TYPE_DEBIT_PRE_DATED, _(u'Debit Card Pre-dated')), ]) short_desc = { TYPE_CREDIT: _(u'Credit'), TYPE_DEBIT: _(u'Debit'), # translators: This is 'Credit Card Installments Store, but should be # abbreviated to fit a small space TYPE_CREDIT_INSTALLMENTS_STORE: _(u'Credit Inst. Store'), # translators: This is 'Credit Card Installments Provider, but should be # abbreviated to fit a small space TYPE_CREDIT_INSTALLMENTS_PROVIDER: _(u'Credit Inst. Provider'), TYPE_DEBIT_PRE_DATED: _(u'Debit Pre-dated'), } payment_id = IdCol() #: the |payment| this information is about payment = Reference(payment_id, 'Payment.id') card_type = EnumCol(default=TYPE_CREDIT) provider_id = IdCol(default=None) #: the |creditprovider| for this class provider = Reference(provider_id, 'CreditProvider.id') device_id = IdCol(default=None) #: the |carddevice| used for the payment #: If the |carddevice| is excluded in the future, this value will be set to null. device = Reference(device_id, 'CardPaymentDevice.id') #: the fixed value that will be charged for the related |payment| fare = PriceCol(default=0) #: the percentage of the value that will be charged for the related |payment| fee = PercentCol(default=0) #: the fee that will be charged based on the :obj:`.fee` fee_value = PriceCol(default=0) #: this is used by the tef plugin. nsu = IntCol(default=None) #: The authorization number returned by the payment device. This will be #: returned automatically by the tef plugin, but needs to be manually #: informed if not using the plugin. auth = IntCol(default=None) #: the number of installments, used by the tef plugin installments = IntCol(default=1) #: the value of the first installment (when installments > 1), used by the #: tef plugin entrance_value = PriceCol(default=0) def update_card_data(self, device, provider, card_type, installments): """Creates a new |cardcost| based on |carddevice|, |creditprovider|, card_type and installments to update |creditcarddata|. :param device: the payment device :param provider: the credit provider :param card_type: the type of card, may be either credit or debit :param installments: the number of installments """ if device is None or not isinstance(device, CardPaymentDevice): raise TypeError("device must be CardPaymentDevice instance and " "not %r" % (device, )) if provider is None or not isinstance(provider, CreditProvider): raise TypeError("provider must be CreditProvider instance and" " not %r" % (provider, )) if card_type is None: raise ValueError("card_type cannot be None") if installments is None: raise ValueError("installments cannot be None") cost = device.get_provider_cost(provider=provider, card_type=card_type, installments=installments) self.device = device self.provider = provider self.card_type = card_type self.fee = cost.fee if cost else 0 self.fare = cost.fare if cost else 0 self.fee_value = self.fee * self.payment.value / 100
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 Service(Domain): """Class responsible to store basic service informations.""" __storm_table__ = 'service' #: The |sellable| for this service sellable = Reference('id', 'Sellable.id') #: The taxation code for this service in the city city_taxation_code = UnicodeCol() #: The federal service list item code for this service service_list_item_code = UnicodeCol() #: ISS Aliquot in percentage p_iss = PercentCol() def __init__(self, **kwargs): assert 'sellable' in kwargs kwargs['id'] = kwargs['sellable'].id super(Service, self).__init__(**kwargs) def remove(self): """Removes this service from the database.""" self.store.remove(self) def close(self): # We don't have to do anything special when closing a service. pass # # Sellable helpers # def can_remove(self): if sysparam.compare_object('DELIVERY_SERVICE', self): # The delivery item cannot be deleted as it's important # for creating deliveries. return False return super(Service, self).can_remove() def can_close(self): # The delivery item cannot be closed as it will be # used for deliveries. return not sysparam.compare_object('DELIVERY_SERVICE', self) # # IDescribable implementation # def get_description(self): return self.sellable.get_description() # # Domain hooks # def on_create(self): ServiceCreateEvent.emit(self) def on_delete(self): ServiceRemoveEvent.emit(self) def on_update(self): store = self.store emitted_store_list = getattr(self, u'_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: ServiceEditEvent.emit(self) emitted_store_list.add(store) self._emitted_store_list = emitted_store_list