Exemple #1
0
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')
Exemple #2
0
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()
Exemple #3
0
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)
Exemple #4
0
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
Exemple #5
0
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')
Exemple #6
0
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()
Exemple #7
0
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())
Exemple #8
0
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)
Exemple #9
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')
Exemple #10
0
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)
Exemple #11
0
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)
Exemple #12
0
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)
Exemple #13
0
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
Exemple #14
0
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)
Exemple #15
0
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)
Exemple #16
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)
Exemple #17
0
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))
Exemple #18
0
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)
Exemple #19
0
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)
Exemple #20
0
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
Exemple #21
0
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
Exemple #22
0
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)
Exemple #23
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()
Exemple #24
0
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
Exemple #25
0
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)
Exemple #26
0
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