Beispiel #1
0
class BugSummary(Storm):
    """BugSummary Storm database class."""

    __storm_table__ = 'combinedbugsummary'

    id = Int(primary=True)
    count = Int()

    product_id = Int(name='product')
    product = Reference(product_id, Product.id)

    productseries_id = Int(name='productseries')
    productseries = Reference(productseries_id, ProductSeries.id)

    distribution_id = Int(name='distribution')
    distribution = Reference(distribution_id, Distribution.id)

    distroseries_id = Int(name='distroseries')
    distroseries = Reference(distroseries_id, DistroSeries.id)

    sourcepackagename_id = Int(name='sourcepackagename')
    sourcepackagename = Reference(sourcepackagename_id, SourcePackageName.id)

    milestone_id = Int(name='milestone')
    milestone = Reference(milestone_id, Milestone.id)

    status = EnumCol(dbName='status',
                     schema=(BugTaskStatus, BugTaskStatusSearch))

    importance = EnumCol(dbName='importance', schema=BugTaskImportance)

    tag = Unicode()

    viewed_by_id = Int(name='viewed_by')
    viewed_by = Reference(viewed_by_id, Person.id)
    access_policy_id = Int(name='access_policy')
    access_policy = Reference(access_policy_id, AccessPolicy.id)

    has_patch = Bool()
Beispiel #2
0
class ProductIcmsTemplate(BaseICMS):
    __storm_table__ = 'product_icms_template'

    product_tax_template_id = IdCol()
    product_tax_template = Reference(product_tax_template_id,
                                     'ProductTaxTemplate.id')

    # Simples Nacional
    p_cred_sn_valid_until = DateTimeCol(default=None)

    def is_p_cred_sn_valid(self):
        """Returns if p_cred_sn has expired."""
        if not self.p_cred_sn_valid_until:
            # If we don't have a valid_until, means p_cred_sn will never
            # expire. Therefore, p_cred_sn is valid.
            return True
        elif self.p_cred_sn_valid_until.date() < localtoday().date():
            return False

        return True
Beispiel #3
0
class FiscalDayHistory(Domain):
    """This represents the information that needs to be used to
    generate a Sintegra file of type 60A.
    """

    __storm_table__ = 'fiscal_day_history'

    emission_date = DateTimeCol()
    station_id = IdCol()
    station = Reference(station_id, 'BranchStation.id')
    serial = UnicodeCol()
    serial_id = IntCol()
    coupon_start = IntCol()
    coupon_end = IntCol()
    cro = IntCol()
    crz = IntCol()
    period_total = PriceCol()
    total = PriceCol()
    taxes = ReferenceSet('id', 'FiscalDayTax.fiscal_day_history_id')
    reduction_date = DateTimeCol()
Beispiel #4
0
class OpticalMedic(Domain):
    """Information about the Medic (Ophtamologist)"""

    __storm_table__ = 'optical_medic'

    person_id = IdCol(allow_none=False)
    person = Reference(person_id, 'Person.id')

    # TODO: Find out a better name for crm
    crm_number = UnicodeCol()

    #: If this medic is a partner of the store, ie, if they recomend clients to
    #: this store
    partner = BoolCol()

    #
    # IDescribable implementation
    #

    @classmethod
    def get_person_by_crm(cls, store, document):
        query = cls.crm_number == document

        tables = [Person,
                  Join(OpticalMedic, Person.id == OpticalMedic.person_id)]
        return store.using(*tables).find(Person, query).one()

    def get_description(self):
        return _('%s (upid: %s)') % (self.person.name, self.crm_number)

    @DomainMergeEvent.connect
    @classmethod
    def on_domain_merge(cls, obj, other):
        if type(obj) != Person:
            return
        this_facet = obj.store.find(cls, person=obj).one()
        other_facet = obj.store.find(cls, person=other).one()
        if not this_facet and not other_facet:
            return
        obj.merge_facet(this_facet, other_facet)
        return set([('optical_medic', 'person_id')])
Beispiel #5
0
class Folder(object):
    __storm_table__ = 'folder'

    id = UUID(primary = True, default_factory = uuid.uuid4)
    root = Bool(default = False)
    name = Unicode()
    path = Unicode() # unique
    created = DateTime(default_factory = now)
    has_cover_art = Bool(default = False)
    last_scan = Int(default = 0)

    parent_id = UUID() # nullable
    parent = Reference(parent_id, id)
    children = ReferenceSet(id, parent_id)

    def as_subsonic_child(self, user):
        info = {
            'id': str(self.id),
            'isDir': True,
            'title': self.name,
            'album': self.name,
            'created': self.created.isoformat()
        }
        if not self.root:
            info['parent'] = str(self.parent_id)
            info['artist'] = self.parent.name
        if self.has_cover_art:
            info['coverArt'] = str(self.id)

        starred = Store.of(self).get(StarredFolder, (user.id, self.id))
        if starred:
            info['starred'] = starred.date.isoformat()

        rating = Store.of(self).get(RatingFolder, (user.id, self.id))
        if rating:
            info['userRating'] = rating.rating
        avgRating = Store.of(self).find(RatingFolder, RatingFolder.rated_id == self.id).avg(RatingFolder.rating)
        if avgRating:
            info['averageRating'] = avgRating

        return info
Beispiel #6
0
class UIField(Domain):
    """This describes a field in form a.
    Can be used makae fields mandatory or hide them completely.
    """
    __storm_table__ = 'ui_field'

    ui_form_id = IdCol()
    ui_form = Reference(ui_form_id, 'UIForm.id')
    field_name = UnicodeCol()
    description = UnicodeCol()
    visible = BoolCol()
    mandatory = BoolCol()

    def update_field(self, mandatory=False, visible=False):
        """This method changes some properties of the field

        :param mandatory: A boolean indicating if the field is mandatory
        :param visible: A boolean indicating if the field is visible
        """
        self.mandatory = mandatory
        self.visible = visible
Beispiel #7
0
class BankAccount(Domain):
    """Information specific to a bank

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

    __storm_table__ = 'bank_account'

    account_id = IdCol()

    #: the |account| for this bank account
    account = Reference(account_id, 'Account.id')

    # FIXME: This is brazil specific, should probably be replaced by a
    #        bank reference to a separate class with name in addition to
    #        the bank number
    #: an identify for the bank type of this account,
    bank_number = IntCol(default=0)

    #: an identifier for the bank branch/agency which is responsible
    #: for this
    bank_branch = UnicodeCol(default=None)

    #: an identifier for this bank account
    bank_account = UnicodeCol(default=None)

    @property
    def options(self):
        """Get the bill options for this bank account
        :returns: a list of :class:`BillOption`
        """
        return self.store.find(BillOption, bank_account=self)

    def add_bill_option(self, name, value):
        return BillOption(store=self.store,
                          option=name,
                          value=value,
                          bank_account_id=self.id)
Beispiel #8
0
class FiscalDayTax(Domain):
    """This represents the information that needs to be used to
    generate a Sintegra file of type 60M.
    """

    __storm_table__ = 'fiscal_day_tax'

    fiscal_day_history_id = IdCol()
    fiscal_day_history = Reference(fiscal_day_history_id, 'FiscalDayHistory.id')

    #: four bytes, either the percental of the tax, 1800 for 18% or one of:
    #:
    #: * ``I``: Isento
    #: * ``F``: Substitucao
    #: * ``N``: Nao tributado
    #: * ``ISS``: ISS
    #: * ``CANC``: Cancelled
    #: * ``DESC``: Discount
    code = UnicodeCol()

    value = PriceCol()
    type = UnicodeCol()
Beispiel #9
0
class ProfileSettings(Domain):
    """Profile settings for user profile instances. Each instance of this
    class stores information about the access availability in a certain
    application."""

    __storm_table__ = 'profile_settings'
    app_dir_name = UnicodeCol()
    has_permission = BoolCol(default=False)
    user_profile_id = IntCol()
    user_profile = Reference(user_profile_id, 'UserProfile.id')

    @classmethod
    def set_permission(cls, store, profile, app, permission):
        """
        Set the permission for a user profile to use a application
        :param store: a store
        :param profile: a UserProfile
        :param app: name of the application
        :param permission: a boolean of the permission
        """
        setting = store.find(cls, user_profile=profile,
                             app_dir_name=app).one()
        setting.has_permission = permission
Beispiel #10
0
class BookPublisher(Domain):
    """An institution created to publish books"""

    (STATUS_ACTIVE, STATUS_INACTIVE) = range(2)

    statuses = {STATUS_ACTIVE: _(u'Active'), STATUS_INACTIVE: _(u'Inactive')}

    __storm_table__ = 'book_publisher'

    person_id = IntCol()
    person = Reference(person_id, 'Person.id')
    status = IntCol(default=STATUS_ACTIVE)

    #
    # IActive implementation
    #

    def inactivate(self):
        assert self.is_active, ('This person facet is already inactive')
        self.is_active = False

    def activate(self):
        assert not self.is_active, ('This personf facet is already active')
        self.is_active = True

    def get_status_string(self):
        if self.is_active:
            return _('Active')
        return _('Inactive')

    #
    # IDescribable implementation
    #

    def get_description(self):
        return self.person.name
Beispiel #11
0
class Address(Domain):
    """An Address is a class that stores a physical street location
    for a |person|.

    A Person can have many addresses.
    The city, state and country is found in |citylocation|.

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

    __storm_table__ = 'address'

    #: street of the address, something like ``"Wall street"``
    street = UnicodeCol(default=u'')

    #: streetnumber, eg ``100``
    streetnumber = IntCol(default=None)

    #: district, eg ``"Manhattan"``
    district = UnicodeCol(default=u'')

    #: postal code, eg ``"12345-678"``
    postal_code = UnicodeCol(default=u'')

    #: complement, eg ``"apartment 35"``
    complement = UnicodeCol(default=u'')

    #: If this is the primary address for the |person|, this is set
    #: when you register a person for the first time.
    is_main_address = BoolCol(default=False)

    person_id = IdCol()

    #: the |person| who resides at this address
    person = Reference(person_id, 'Person.id')

    city_location_id = IntCol()

    #: the |citylocation| this address is in
    city_location = Reference(city_location_id, 'CityLocation.id')

    #
    # IDescribable
    #

    def get_description(self):
        """See `IDescribable.get_description()`"""
        return self.get_address_string()

    # Public API

    def is_valid_model(self):
        """Verifies if this model is properly filled in,
        that there's a street, district and valid |citylocation| set.

        :returns: ``True`` if this address is filled in.
        """

        # FIXME: This should probably take uiforms into account.
        return (self.street and self.district
                and self.city_location.is_valid_model())

    def get_city(self):
        """Get the city for this address. It's fetched from
        the |citylocation|.

        :returns: the city
        """
        return self.city_location.city

    def get_country(self):
        """Get the country for this address. It's fetched from
        the |citylocation|.

        :returns: the country
        """
        return self.city_location.country

    def get_state(self):
        """Get the state for this address. It's fetched from
        the |citylocation|.

        :returns: the state
        """

        return self.city_location.state

    def get_postal_code_number(self):
        """Get the postal code without any non-numeric characters.

        :returns: the postal code as a number
        """
        if not self.postal_code:
            return 0
        return int(''.join([c for c in self.postal_code
                            if c in u'1234567890']))

    def get_address_string(self):
        """Formats the address as a string

        :returns: the formatted address
        """
        return format_address(self)

    def get_details_string(self):
        """ Returns a string like ``postal_code - city - state``.
        If city or state are missing, return only postal_code; and
        if postal_code is missing, return ``city - state``, otherwise,
        return an empty string

        :returns: the detailed string
        """
        details = []
        if self.postal_code:
            details.append(self.postal_code)
        if self.city_location.city and self.city_location.state:
            details.extend([self.city_location.city, self.city_location.state])
        details = u" - ".join(details)
        return details
Beispiel #12
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)
Beispiel #13
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))
Beispiel #14
0
class TransferOrderItem(Domain):
    """Transfer order item

    """

    __storm_table__ = 'transfer_order_item'

    sellable_id = IdCol()

    # FIXME: This should be a product, since it does not make sense to transfer
    # serviçes
    #: The |sellable| to transfer
    sellable = Reference(sellable_id, 'Sellable.id')

    batch_id = IdCol()

    #: If the sellable is a storable, the |batch| that was transfered
    batch = Reference(batch_id, 'StorableBatch.id')

    transfer_order_id = IdCol()

    #: The |transfer| this item belongs to
    transfer_order = Reference(transfer_order_id, 'TransferOrder.id')

    #: The quantity to transfer
    quantity = QuantityCol()

    #: Average cost of the item in the source branch at the time of transfer.
    stock_cost = PriceCol(default=0)

    #
    # Public API
    #

    def get_total(self):
        """Returns the total cost of a transfer item eg quantity * cost"""
        return self.quantity * self.sellable.cost

    def send(self):
        """Sends this item to it's destination |branch|.
        This method should never be used directly, and to send a transfer you
        should use TransferOrder.send().
        """
        storable = self.sellable.product_storable
        storable.decrease_stock(self.quantity,
                                self.transfer_order.source_branch,
                                StockTransactionHistory.TYPE_TRANSFER_TO,
                                self.id,
                                batch=self.batch)
        ProductHistory.add_transfered_item(self.store,
                                           self.transfer_order.source_branch,
                                           self)

    def receive(self):
        """Receives this item, increasing the quantity in the stock.
        This method should never be used directly, and to receive a transfer
        you should use TransferOrder.receive().
        """
        storable = self.sellable.product_storable
        storable.increase_stock(self.quantity,
                                self.transfer_order.destination_branch,
                                StockTransactionHistory.TYPE_TRANSFER_FROM,
                                self.id,
                                unit_cost=self.stock_cost,
                                batch=self.batch)
Beispiel #15
0
class Service(Domain):
    """Class responsible to store basic service informations."""
    __storm_table__ = 'service'

    sellable_id = IdCol()

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

    def remove(self):
        """Removes this service from the database."""
        self.store.remove(self)

    #
    # Sellable helpers
    #

    def can_remove(self):
        if self == sysparam(self.store).DELIVERY_SERVICE:
            # The delivery item cannot be deleted as it's important
            # for creating deliveries.
            return False

        # False if the service is used in a production.
        if self.store.find(ProductionService, service=self).count():
            return False

        return True

    def can_close(self):
        # The delivery item cannot be closed as it will be
        # used for deliveries.
        return self != sysparam(self.store).DELIVERY_SERVICE

    #
    # 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
Beispiel #16
0
class AccountTransaction(Domain):
    """Transaction between two accounts.

    A transaction is a transfer of money from the
    :obj:`~.source_account` to the
    :obj:`~.account`.

    It removes a negative amount of money from the source and increases
    the account by the same amount.
    There's only one value, but depending on the view it's either negative
    or positive, it can never be zero though.
    A transaction can optionally be tied to a |payment|

    See also:
    `schema <http://doc.stoq.com.br/schema/tables/account_transaction.html>`__
    `manual <http://doc.stoq.com.br/manual/transaction.html>`__

    """

    __storm_table__ = 'account_transaction'

    # operation_type values
    TYPE_IN = u'in'
    TYPE_OUT = u'out'

    # FIXME: It's way to tricky to calculate the direction and it's
    #        values for an AccountTransaction due to the fact that
    #        we're only store one value. We should store two values,
    #        one for how much the current account should be increased
    #        with and another one which is how much the other account
    #        should be increased with. For split transaction we might
    #        want to store more values, so it might make sense to allow
    #        N values per transaction.

    account_id = IdCol()

    #: destination |account|
    account = Reference(account_id, 'Account.id')

    source_account_id = IdCol()

    #: source |account|
    source_account = Reference(source_account_id, 'Account.id')

    #: short human readable summary of the transaction
    description = UnicodeCol()

    #: identifier of this transaction within a account
    code = UnicodeCol()

    #: value transfered
    value = PriceCol(default=0)

    #: date the transaction was done
    date = DateTimeCol()

    payment_id = IdCol(default=None)

    #: |payment| this transaction relates to, can also be ``None``
    payment = Reference(payment_id, 'Payment.id')

    #: operation_type represents the type of transaction (debit/credit)
    operation_type = EnumCol(allow_none=False, default=TYPE_IN)

    class sqlmeta:
        lazyUpdate = True

    @classmethod
    def get_inverted_operation_type(cls, operation_type):
        """ Get the inverted operation_type (IN->OUT / OUT->IN)

        :param operation_type: the type of transaction
        :returns: the inverted transaction type
        """
        if operation_type == cls.TYPE_IN:
            return cls.TYPE_OUT
        return cls.TYPE_IN

    @classmethod
    def create_from_payment(cls,
                            payment,
                            code=None,
                            source_account=None,
                            destination_account=None):
        """Create a new transaction based on a |payment|.
        It's normally used when creating a transaction which represents
        a payment, for instance when you receive a bill or a check from
        a |client| which will enter a |bankaccount|.

        :param payment: the |payment| to create the transaction for.
        :param code: the code for the transaction. If not provided,
            the payment identifier will be used by default
        :param source_account: the source |account| for the transaction.
        :param destination_account: the destination |account| for the transaction.
        :returns: the transaction
        """
        if not payment.is_paid():
            raise PaymentError(_("Payment needs to be paid"))
        store = payment.store
        value = payment.paid_value
        if payment.is_outpayment():
            operation_type = cls.TYPE_OUT
            source = source_account or payment.method.destination_account
            destination = (destination_account
                           or sysparam.get_object(store, 'IMBALANCE_ACCOUNT'))
        else:
            operation_type = cls.TYPE_IN
            source = (source_account
                      or sysparam.get_object(store, 'IMBALANCE_ACCOUNT'))
            destination = (destination_account
                           or payment.method.destination_account)

        code = code if code is not None else unicode(payment.identifier)
        return cls(source_account=source,
                   account=destination,
                   value=value,
                   description=payment.description,
                   code=code,
                   date=payment.paid_date,
                   store=store,
                   payment=payment,
                   operation_type=operation_type)

    def create_reverse(self):
        """Reverse this transaction, this happens when a payment
        is set as not paid.

        :returns: the newly created account transaction representing
           the reversal
        """

        # We're effectively canceling the old transaction here,
        # to avoid having more than one transaction referencing the same
        # payment we reset the payment to None.
        #
        # It would be nice to have all of them reference the same payment,
        # but it makes it harder to create the reversal.

        self.payment = None
        new_type = self.get_inverted_operation_type(self.operation_type)
        return AccountTransaction(source_account=self.account,
                                  account=self.source_account,
                                  value=self.value,
                                  description=_(u"Reverted: %s") %
                                  (self.description),
                                  code=self.code,
                                  date=TransactionTimestamp(),
                                  store=self.store,
                                  payment=None,
                                  operation_type=new_type)

    def invert_transaction_type(self):
        """ Invert source/destination accounts and operation_type

        When change a incoming transaction to outgoing or vice-versa. The source and
        destination accounts must be inverted. Thus, the outgoing value always
        will belong to the source account.
        """
        temp_account = self.account
        operation_type = self.operation_type

        self.account = self.source_account
        self.source_account = temp_account
        self.operation_type = self.get_inverted_operation_type(operation_type)

    def get_other_account(self, account):
        """Get the other end of a transaction

        :param account: an |account|
        :returns: the other end
        """
        if self.source_account == account:
            return self.account
        elif self.account == account:
            return self.source_account
        else:
            raise AssertionError

    def set_other_account(self, other, account):
        """Set the other end of a transaction

        :param other: an |account| which we do not want to set
        :param account: the |account| to set
        """
        other = self.store.fetch(other)
        if self.source_account == other:
            self.account = account
        elif self.account == other:
            self.source_account = account
        else:
            raise AssertionError
Beispiel #17
0
class TransferOrder(Domain):
    """ Transfer Order class
    """
    __storm_table__ = 'transfer_order'

    (STATUS_PENDING, STATUS_SENT, STATUS_RECEIVED) = range(3)

    statuses = {
        STATUS_PENDING: _(u'Pending'),
        STATUS_SENT: _(u'Sent'),
        STATUS_RECEIVED: _(u'Received')
    }

    status = IntCol(default=STATUS_PENDING)

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

    #: The date the order was created
    open_date = DateTimeCol(default_factory=localnow)

    #: The date the order was received
    receival_date = DateTimeCol()

    source_branch_id = IdCol()

    #: The |branch| sending the stock
    source_branch = Reference(source_branch_id, 'Branch.id')

    destination_branch_id = IdCol()

    #: The |branch| receiving the stock
    destination_branch = Reference(destination_branch_id, 'Branch.id')

    source_responsible_id = IdCol()

    #: The |employee| responsible for the |transfer| at source |branch|
    source_responsible = Reference(source_responsible_id, 'Employee.id')

    destination_responsible_id = IdCol()

    #: The |employee| responsible for the |transfer| at destination |branch|
    destination_responsible = Reference(destination_responsible_id,
                                        'Employee.id')

    #
    # IContainer implementation
    #

    def get_items(self):
        return self.store.find(TransferOrderItem, transfer_order=self)

    def add_item(self, item):
        assert self.status == self.STATUS_PENDING
        item.transfer_order = self

    def remove_item(self, item):
        if item.transfer_order is not self:
            raise ValueError(
                _('The item does not belong to this '
                  'transfer order'))
        self.store.remove(item)

    #
    # Public API
    #

    @property
    def branch(self):
        return self.source_branch

    @property
    def status_str(self):
        return (self.statuses[self.status])

    def add_sellable(self, sellable, batch, quantity=1):
        """Add the given |sellable| to this |transfer|.

        :param sellable: The |sellable| we are transfering
        :param batch: What |batch| of the storable (represented by sellable) we
          are transfering.
        :param quantity: The quantity of this product that is being transfered.
        """
        assert self.status == self.STATUS_PENDING

        self.validate_batch(batch, sellable=sellable)

        stock_item = sellable.product_storable.get_stock_item(
            self.source_branch, batch)

        return TransferOrderItem(store=self.store,
                                 transfer_order=self,
                                 sellable=sellable,
                                 batch=batch,
                                 quantity=quantity,
                                 stock_cost=stock_item.stock_cost)

    def can_send(self):
        return (self.status == self.STATUS_PENDING
                and self.get_items().count() > 0)

    def can_receive(self):
        return self.status == self.STATUS_SENT

    def send(self):
        """Sends a transfer order to the destination branch.
        """
        assert self.can_send()

        for item in self.get_items():
            item.send()

        self.status = self.STATUS_SENT

    def receive(self, responsible, receival_date=None):
        """Confirms the receiving of the transfer order.
        """
        assert self.can_receive()

        for item in self.get_items():
            item.receive()

        self.receival_date = receival_date or localnow()
        self.destination_responsible = responsible
        self.status = self.STATUS_RECEIVED

    def get_source_branch_name(self):
        """Returns the source |branch| name"""
        return self.source_branch.person.name

    def get_destination_branch_name(self):
        """Returns the destination |branch| name"""
        return self.destination_branch.person.name

    def get_source_responsible_name(self):
        """Returns the name of the |employee| responsible for the transfer
           at source |branch|
        """
        return self.source_responsible.person.name

    def get_destination_responsible_name(self):
        """Returns the name of the |employee| responsible for the transfer
           at destination |branch|
        """
        if not self.destination_responsible:
            return u''

        return self.destination_responsible.person.name

    def get_total_items_transfer(self):
        """Retuns the |transferitems| quantity
        """
        return sum([item.quantity for item in self.get_items()], 0)
Beispiel #18
0
class RatingFolder(BaseRating):
    __storm_table__ = 'rating_folder'

    rated = Reference(BaseRating.rated_id, Folder.id)
Beispiel #19
0
class Account(Domain):
    """An account, a collection of |accounttransactions| that may be controlled
    by a bank.

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

    __storm_table__ = 'account'

    #: Bank
    TYPE_BANK = u'bank'

    #: Cash/Till
    TYPE_CASH = u'cash'

    #: Assets, like investement account
    TYPE_ASSET = u'asset'

    #: Credit
    TYPE_CREDIT = u'credit'

    #: Income/Salary
    TYPE_INCOME = u'income'

    #: Expenses
    TYPE_EXPENSE = u'expense'

    #: Equity, like unbalanced
    TYPE_EQUITY = u'equity'

    account_labels = {
        TYPE_BANK: (_(u"Deposit"), _(u"Withdrawal")),
        TYPE_CASH: (_(u"Receive"), _(u"Spend")),
        TYPE_ASSET: (_(u"Increase"), _(u"Decrease")),
        TYPE_CREDIT: (_(u"Payment"), _(u"Charge")),
        TYPE_INCOME: (
            _(u"Income"),
            _(u"Charge"),
        ),
        TYPE_EXPENSE: (_(u"Rebate"), _(u"Expense")),
        TYPE_EQUITY: (_(u"Increase"), _(u"Decrease")),
    }

    account_type_descriptions = [
        (_(u"Bank"), TYPE_BANK),
        (_(u"Cash"), TYPE_CASH),
        (_(u"Asset"), TYPE_ASSET),
        (_(u"Credit"), TYPE_CREDIT),
        (_(u"Income"), TYPE_INCOME),
        (_(u"Expense"), TYPE_EXPENSE),
        (_(u"Equity"), TYPE_EQUITY),
    ]

    #: name of the account
    description = UnicodeCol(default=None)

    #: code which identifies the account
    code = UnicodeCol(default=None)

    #: parent account id, can be None
    parent_id = IdCol(default=None)

    #: parent account
    parent = Reference(parent_id, 'Account.id')

    station_id = IdCol(default=None)

    #: the |branchstation| tied
    #: to this account, mainly for TYPE_CASH accounts
    station = Reference(station_id, 'BranchStation.id')

    #: kind of account, one of the TYPE_* defines in this class
    account_type = EnumCol(allow_none=False, default=TYPE_BANK)

    #: |bankaccount| for this account, used by TYPE_BANK accounts
    bank = Reference('id', 'BankAccount.account_id', on_remote=True)

    #
    # IDescribable implementation
    #

    def get_description(self):
        return self.description

    #
    # Class Methods
    #

    @classmethod
    def get_by_station(cls, store, station):
        """Fetch the account assoicated with a station

        :param store: a store
        :param station: a |branchstation|
        :returns: the account
        """
        if station is None:
            raise TypeError("station cannot be None")
        if not isinstance(station, BranchStation):
            raise TypeError("station must be a BranchStation, not %r" %
                            (station, ))
        return store.find(cls, station=station).one()

    @classmethod
    def get_children_for(cls, store, parent):
        """Get a list of child accounts for

        :param store:
        :param |account| parent: parent account
        :returns: the child accounts
        :rtype: resultset
        """
        return store.find(cls, parent=parent)

    @classmethod
    def get_accounts(cls, store):
        """Get a list of all accounts

        :param store: a store
        :returns all accounts
        :rtype: resultset
        """
        return store.find(cls)

    #
    # Properties
    #

    @property
    def long_description(self):
        """Get a long description, including all the parent accounts,
        such as Tills:cotovia"""
        parts = []
        account = self
        while account:
            if not account in parts:
                parts.append(account)
                account = account.parent
        return u':'.join([a.description for a in reversed(parts)])

    @property
    def transactions(self):
        """Returns a list of transactions to this account.

        :returns: list of |accounttransaction|
        """
        return self.store.find(
            AccountTransaction,
            Or(self.id == AccountTransaction.account_id,
               self.id == AccountTransaction.source_account_id))

    #
    # Public API
    #

    def get_total_for_interval(self, start, end):
        """Fetch total value for a given interval

        :param datetime start: beginning of interval
        :param datetime end: of interval
        :returns: total value or one
        """
        if not isinstance(start, datetime.datetime):
            raise TypeError("start must be a datetime.datetime, not %s" %
                            (type(start), ))
        if not isinstance(end, datetime.datetime):
            raise TypeError("end must be a datetime.datetime, not %s" %
                            (type(end), ))

        query = And(
            Date(AccountTransaction.date) >= start,
            Date(AccountTransaction.date) <= end,
            AccountTransaction.source_account_id !=
            AccountTransaction.account_id)

        transactions = self.store.find(AccountTransaction, query)
        incoming = transactions.find(AccountTransaction.account_id == self.id)
        outgoing = transactions.find(
            AccountTransaction.source_account_id == self.id)

        positive_values = incoming.sum(AccountTransaction.value) or 0
        negative_values = outgoing.sum(AccountTransaction.value) or 0

        return currency(positive_values - negative_values)

    def can_remove(self):
        """If the account can be removed.
        Not all accounts can be removed, some are internal to Stoq
        and cannot be removed"""
        # Can't remove accounts that are used in a parameter
        if (sysparam.compare_object('IMBALANCE_ACCOUNT', self)
                or sysparam.compare_object('TILLS_ACCOUNT', self)
                or sysparam.compare_object('BANKS_ACCOUNT', self)):
            return False

        # Can't remove station accounts
        if self.station:
            return False

        # When we remove this Account, all the related AccountTransaction will
        # be assigned to the IMBALANCE_ACCOUNT and BankAccount will be removed,
        # so we need to skip them here
        return super(Account, self).can_remove(skip=[(
            'account_transaction',
            'account_id'), ('account_transaction',
                            'source_account_id'), ('bank_account',
                                                   'account_id')])

    def remove(self, store):
        """Remove the current account. This updates all transactions which
        refers to this account and removes them.

        :param store: a store
        """
        if not self.can_remove():
            raise TypeError("Account %r cannot be removed" % (self, ))

        imbalance_account_id = sysparam.get_object_id('IMBALANCE_ACCOUNT')

        for transaction in store.find(AccountTransaction, account=self):
            transaction.account_id = imbalance_account_id
            store.flush()

        for transaction in store.find(AccountTransaction, source_account=self):
            transaction.source_account_id = imbalance_account_id
            store.flush()

        bank = self.bank
        if bank:
            for option in bank.options:
                store.remove(option)
            store.remove(bank)

        self.store.remove(self)

    def has_child_accounts(self):
        """If this account has child accounts

        :returns: True if any other accounts has this account as a parent"""
        return not self.store.find(Account, parent=self).is_empty()

    def get_type_label(self, out):
        """Returns the label to show for the increases/decreases
        for transactions of this account.
        See :obj:`~..account_labels`

        :param out: if the transaction is going out
        """
        return self.account_labels[self.account_type][int(out)]

    def matches(self, account_id):
        """Check if this account or it's parent account is the same
        as another account id.

        :param account_id: the account id to compare with
        :returns: if the accounts matches.
        """
        if self.id == account_id:
            return True
        if self.parent_id and self.parent_id == account_id:
            return True
        return False
Beispiel #20
0
class ReceivingOrder(Domain):
    """Receiving order definition.
    """

    __storm_table__ = 'receiving_order'

    #: Products in the order was not received or received partially.
    STATUS_PENDING = u'pending'

    #: All products in the order has been received then the order is closed.
    STATUS_CLOSED = u'closed'

    FREIGHT_FOB_PAYMENT = u'fob-payment'
    FREIGHT_FOB_INSTALLMENTS = u'fob-installments'
    FREIGHT_CIF_UNKNOWN = u'cif-unknown'
    FREIGHT_CIF_INVOICE = u'cif-invoice'

    freight_types = collections.OrderedDict([
        (FREIGHT_FOB_PAYMENT, _(u"FOB - Freight value on a new payment")),
        (FREIGHT_FOB_INSTALLMENTS, _(u"FOB - Freight value on installments")),
        (FREIGHT_CIF_UNKNOWN, _(u"CIF - Freight value is unknown")),
        (FREIGHT_CIF_INVOICE,
         _(u"CIF - Freight value highlighted on invoice")),
    ])

    FOB_FREIGHTS = (
        FREIGHT_FOB_PAYMENT,
        FREIGHT_FOB_INSTALLMENTS,
    )
    CIF_FREIGHTS = (FREIGHT_CIF_UNKNOWN, FREIGHT_CIF_INVOICE)

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

    #: status of the order
    status = EnumCol(allow_none=False, default=STATUS_PENDING)

    #: Date that order has been closed.
    receival_date = DateTimeCol(default_factory=localnow)

    #: Date that order was send to Stock application.
    confirm_date = DateTimeCol(default=None)

    #: Some optional additional information related to this order.
    notes = UnicodeCol(default=u'')

    #: Type of freight
    freight_type = EnumCol(allow_none=False, default=FREIGHT_FOB_PAYMENT)

    #: Total of freight paid in receiving order.
    freight_total = PriceCol(default=0)

    surcharge_value = PriceCol(default=0)

    #: Discount value in receiving order's payment.
    discount_value = PriceCol(default=0)

    #: Secure value paid in receiving order's payment.
    secure_value = PriceCol(default=0)

    #: Other expenditures paid in receiving order's payment.
    expense_value = PriceCol(default=0)

    # This is Brazil-specific information
    icms_total = PriceCol(default=0)
    ipi_total = PriceCol(default=0)

    #: The invoice number of the order that has been received.
    invoice_number = IntCol()

    #: The invoice total value of the order received
    invoice_total = PriceCol(default=None)

    #: The invoice key of the order received
    invoice_key = UnicodeCol()

    cfop_id = IdCol()
    cfop = Reference(cfop_id, 'CfopData.id')

    responsible_id = IdCol()
    responsible = Reference(responsible_id, 'LoginUser.id')

    supplier_id = IdCol()
    supplier = Reference(supplier_id, 'Supplier.id')

    branch_id = IdCol()
    branch = Reference(branch_id, 'Branch.id')

    transporter_id = IdCol(default=None)
    transporter = Reference(transporter_id, 'Transporter.id')

    purchase_orders = ReferenceSet('ReceivingOrder.id',
                                   'PurchaseReceivingMap.receiving_id',
                                   'PurchaseReceivingMap.purchase_id',
                                   'PurchaseOrder.id')

    def __init__(self, store=None, **kw):
        Domain.__init__(self, store=store, **kw)
        # These miss default parameters and needs to be set before
        # cfop, which triggers an implicit flush.
        self.branch = kw.pop('branch', None)
        self.supplier = kw.pop('supplier', None)
        if not 'cfop' in kw:
            self.cfop = sysparam.get_object(store, 'DEFAULT_RECEIVING_CFOP')

    #
    #  Public API
    #

    def confirm(self):
        for item in self.get_items():
            item.add_stock_items()

        purchases = list(self.purchase_orders)
        # XXX: Maybe FiscalBookEntry should not reference the payment group, but
        # lets keep this way for now until we refactor the fiscal book related
        # code, since it will pretty soon need a lot of changes.
        group = purchases[0].group
        FiscalBookEntry.create_product_entry(self.store, group, self.cfop,
                                             self.invoice_number,
                                             self.icms_total, self.ipi_total)

        self.invoice_total = self.total

        for purchase in purchases:
            if purchase.can_close():
                purchase.close()

    def add_purchase(self, order):
        return PurchaseReceivingMap(store=self.store,
                                    purchase=order,
                                    receiving=self)

    def add_purchase_item(self,
                          item,
                          quantity=None,
                          batch_number=None,
                          parent_item=None):
        """Add a |purchaseitem| on this receiving order

        :param item: the |purchaseitem|
        :param decimal.Decimal quantity: the quantity of that item.
            If ``None``, it will be get from the item's pending quantity
        :param batch_number: a batch number that will be used to
            get or create a |batch| it will be get from the item's
            pending quantity or ``None`` if the item's |storable|
            is not controlling batches.
        :raises: :exc:`ValueError` when validating the quantity
            and testing the item's order for equality with :obj:`.order`
        """
        pending_quantity = item.get_pending_quantity()
        if quantity is None:
            quantity = pending_quantity

        if not (0 < quantity <= item.quantity):
            raise ValueError("The quantity must be higher than 0 and lower "
                             "than the purchase item's quantity")
        if quantity > pending_quantity:
            raise ValueError("The quantity must be lower than the item's "
                             "pending quantity")

        sellable = item.sellable
        storable = sellable.product_storable
        if batch_number is not None:
            batch = StorableBatch.get_or_create(self.store,
                                                storable=storable,
                                                batch_number=batch_number)
        else:
            batch = None

        self.validate_batch(batch, sellable)

        return ReceivingOrderItem(store=self.store,
                                  sellable=item.sellable,
                                  batch=batch,
                                  quantity=quantity,
                                  cost=item.cost,
                                  purchase_item=item,
                                  receiving_order=self,
                                  parent_item=parent_item)

    def update_payments(self, create_freight_payment=False):
        """Updates the payment value of all payments realated to this
        receiving. If create_freight_payment is set, a new payment will be
        created with the freight value. The other value as the surcharges and
        discounts will be included in the installments.

        :param create_freight_payment: True if we should create a new payment
                                       with the freight value, False otherwise.
        """
        difference = self.total - self.products_total
        if create_freight_payment:
            difference -= self.freight_total

        if difference != 0:
            # Get app pending payments for the purchases associated with this
            # receiving, and update them.
            payments = self.payments.find(status=Payment.STATUS_PENDING)
            payments_number = payments.count()
            if payments_number > 0:
                # XXX: There is a potential rounding error here.
                per_installments_value = difference / payments_number
                for payment in payments:
                    new_value = payment.value + per_installments_value
                    payment.update_value(new_value)

        if self.freight_total and create_freight_payment:
            self._create_freight_payment()

    def _create_freight_payment(self):
        store = self.store
        money_method = PaymentMethod.get_by_name(store, u'money')
        # If we have a transporter, the freight payment will be for him
        # (and in another payment group).
        purchases = list(self.purchase_orders)
        if len(purchases) == 1 and self.transporter is None:
            group = purchases[0].group
        else:
            if self.transporter:
                recipient = self.transporter.person
            else:
                recipient = self.supplier.person
            group = PaymentGroup(store=store, recipient=recipient)

        description = _(u'Freight for receiving %s') % (self.identifier, )
        payment = money_method.create_payment(Payment.TYPE_OUT,
                                              group,
                                              self.branch,
                                              self.freight_total,
                                              due_date=localnow(),
                                              description=description)
        payment.set_pending()
        return payment

    def get_items(self, with_children=True):
        store = self.store
        query = ReceivingOrderItem.receiving_order == self
        if not with_children:
            query = And(query, Eq(ReceivingOrderItem.parent_item_id, None))
        return store.find(ReceivingOrderItem, query)

    def remove_items(self):
        for item in self.get_items():
            item.receiving_order = None

    def remove_item(self, item):
        assert item.receiving_order == self
        type(item).delete(item.id, store=self.store)

    def is_totally_returned(self):
        return all(item.is_totally_returned() for item in self.get_items())

    #
    # Properties
    #

    @property
    def payments(self):
        tables = [PurchaseReceivingMap, PurchaseOrder, Payment]
        query = And(PurchaseReceivingMap.receiving_id == self.id,
                    PurchaseReceivingMap.purchase_id == PurchaseOrder.id,
                    Payment.group_id == PurchaseOrder.group_id)
        return self.store.using(tables).find(Payment, query)

    @property
    def supplier_name(self):
        if not self.supplier:
            return u""
        return self.supplier.get_description()

    #
    # Accessors
    #

    @property
    def cfop_code(self):
        return self.cfop.code

    @property
    def transporter_name(self):
        if not self.transporter:
            return u""
        return self.transporter.get_description()

    @property
    def branch_name(self):
        return self.branch.get_description()

    @property
    def responsible_name(self):
        return self.responsible.get_description()

    @property
    def products_total(self):
        total = sum([item.get_total() for item in self.get_items()],
                    currency(0))
        return currency(total)

    @property
    def receival_date_str(self):
        return self.receival_date.strftime("%x")

    @property
    def total_surcharges(self):
        """Returns the sum of all surcharges (purchase & receiving)"""
        total_surcharge = 0
        if self.surcharge_value:
            total_surcharge += self.surcharge_value
        if self.secure_value:
            total_surcharge += self.secure_value
        if self.expense_value:
            total_surcharge += self.expense_value

        for purchase in self.purchase_orders:
            total_surcharge += purchase.surcharge_value

        if self.ipi_total:
            total_surcharge += self.ipi_total

        # CIF freights don't generate payments.
        if (self.freight_total and self.freight_type
                not in (self.FREIGHT_CIF_UNKNOWN, self.FREIGHT_CIF_INVOICE)):
            total_surcharge += self.freight_total

        return currency(total_surcharge)

    @property
    def total_quantity(self):
        """Returns the sum of all received quantities"""
        return sum(item.quantity
                   for item in self.get_items(with_children=False))

    @property
    def total_discounts(self):
        """Returns the sum of all discounts (purchase & receiving)"""
        total_discount = 0
        if self.discount_value:
            total_discount += self.discount_value

        for purchase in self.purchase_orders:
            total_discount += purchase.discount_value

        return currency(total_discount)

    @property
    def total(self):
        """Fetch the total, including discount and surcharge for both the
        purchase order and the receiving order.
        """
        total = self.products_total
        total -= self.total_discounts
        total += self.total_surcharges

        return currency(total)

    def guess_freight_type(self):
        """Returns a freight_type based on the purchase's freight_type"""
        purchases = list(self.purchase_orders)
        assert len(purchases) == 1

        purchase = purchases[0]
        if purchase.freight_type == PurchaseOrder.FREIGHT_FOB:
            if purchase.is_paid():
                freight_type = ReceivingOrder.FREIGHT_FOB_PAYMENT
            else:
                freight_type = ReceivingOrder.FREIGHT_FOB_INSTALLMENTS
        elif purchase.freight_type == PurchaseOrder.FREIGHT_CIF:
            if purchase.expected_freight:
                freight_type = ReceivingOrder.FREIGHT_CIF_INVOICE
            else:
                freight_type = ReceivingOrder.FREIGHT_CIF_UNKNOWN

        return freight_type

    def _get_percentage_value(self, percentage):
        if not percentage:
            return currency(0)
        subtotal = self.products_total
        percentage = Decimal(percentage)
        return subtotal * (percentage / 100)

    @property
    def discount_percentage(self):
        discount_value = self.discount_value
        if not discount_value:
            return currency(0)
        subtotal = self.products_total
        assert subtotal > 0, (u'the subtotal should not be zero '
                              u'at this point')
        total = subtotal - discount_value
        percentage = (1 - total / subtotal) * 100
        return quantize(percentage)

    @discount_percentage.setter
    def discount_percentage(self, value):
        """Discount by percentage.
        Note that percentage must be added as an absolute value not as a
        factor like 1.05 = 5 % of surcharge
        The correct form is 'percentage = 3' for a discount of 3 %
        """
        self.discount_value = self._get_percentage_value(value)

    @property
    def surcharge_percentage(self):
        """Surcharge by percentage.
        Note that surcharge must be added as an absolute value not as a
        factor like 0.97 = 3 % of discount.
        The correct form is 'percentage = 3' for a surcharge of 3 %
        """
        surcharge_value = self.surcharge_value
        if not surcharge_value:
            return currency(0)
        subtotal = self.products_total
        assert subtotal > 0, (u'the subtotal should not be zero '
                              u'at this point')
        total = subtotal + surcharge_value
        percentage = ((total / subtotal) - 1) * 100
        return quantize(percentage)

    @surcharge_percentage.setter
    def surcharge_percentage(self, value):
        self.surcharge_value = self._get_percentage_value(value)
Beispiel #21
0
class Domain(ORMObject):
    """The base domain for Stoq.

    This builds on top of :class:`stoqlib.database.properties.ORMObject` and adds:

    * created and modified |transactionentry|, a log which is mainly used
      by database synchonization, it allows us to find out what has been
      modified and created within a specific time frame.
    * create/update/modify hooks, which some domain objects implement to
      fire of events that can be used to find out when a specific
      domain event is triggered, eg a |sale| is created, a |product| is
      modified. etc.
    * cloning of domains, when you want to create a new similar domain
    * function to check if an value of a column is unique within a domain.

    Not all domain objects need to subclass Domain, some, such as
    :class:`stoqlib.domain.system.SystemTable` inherit from ORMObject.
    """

    # FIXME: this is only used by pylint
    __storm_table__ = 'invalid'

    #: A list of fields from this object that should de added on the
    #: representation of this object (when calling repr())
    repr_fields = []

    #: id of this domain class, it's usually the primary key.
    #: it will automatically update when a new insert is created.
    #: Note that there might be holes in the sequence numbers, which happens
    #: due to aborted transactions
    id = IdCol(primary=True, default=AutoReload)

    te_id = IntCol(default=AutoReload)

    #: a |transactionentry| for when the domain object was created and last
    #: modified
    te = Reference(te_id, TransactionEntry.id)

    def __init__(self, *args, **kwargs):
        self._listen_to_events()
        ORMObject.__init__(self, *args, **kwargs)

    def __repr__(self):
        parts = ['%r' % self.id]
        for field in self.repr_fields:
            parts.append('%s=%r' % (field, getattr(self, field)))

        desc = ' '.join(parts)
        return '<%s %s>' % (self.__class__.__name__, desc)

    def __storm_loaded__(self):
        self._listen_to_events()

    def __storm_pre_flush__(self):
        obj_info = get_obj_info(self)
        pending = obj_info.get("pending")
        stoq_pending = obj_info.get('stoq-status')
        store = obj_info.get("store")

        if pending is PENDING_ADD:
            obj_info['stoq-status'] = _OBJ_CREATED
        elif pending is PENDING_REMOVE:
            obj_info['stoq-status'] = _OBJ_DELETED
        else:
            # This is storm's approach to check if the obj has pending changes,
            # but only makes sense if the obj is not being created/deleted.
            if (store._get_changes_map(obj_info, True)
                    and stoq_pending not in [_OBJ_CREATED, _OBJ_DELETED]):
                obj_info['stoq-status'] = _OBJ_UPDATED

    #
    #   Private
    #

    def _listen_to_events(self):
        event = get_obj_info(self).event
        event.hook('changed', self._on_object_changed)
        event.hook('before-removed', self._on_object_before_removed)
        event.hook('before-commited', self._on_object_before_commited)

    def _on_object_changed(self, obj_info, variable, old_value, value,
                           from_db):
        # Only call the hook if the value is coming from python, and is a valid
        # value (not auto reload) and the value actually changed.
        if from_db or value is AutoReload or old_value == value:
            return

        self.on_object_changed(variable.column.name, old_value, value)

    def _on_object_before_removed(self, obj_info):
        # If the obj was created and them removed, nothing needs to be done.
        # It never really got into the database.
        if obj_info.get('stoq-status') == _OBJ_CREATED:
            obj_info['stoq-status'] = None
        else:
            self.on_delete()

        # This is emited right before the object is removed from the store.
        # We must also remove the transaction entry, but the entry should be
        # deleted *after* the object is deleted, thats why we need to specify
        # the flush order.
        store = obj_info.get("store")
        store.remove(self.te)
        store.add_flush_order(self, self.te)

    def _on_object_before_commited(self, obj_info):
        # on_create/on_update hooks can modify the object and make it be
        # flushed again, so lets reset pending before calling them
        stoq_pending = obj_info.get('stoq-status')
        obj_info['stoq-status'] = None

        if stoq_pending == _OBJ_CREATED:
            self.on_create()
        elif stoq_pending == _OBJ_UPDATED:
            self.on_update()

    #
    # Public API
    #

    def serialize(self):
        """Returns the object as a dictionary"""
        dictionary = collections.OrderedDict()
        for cls in self.__class__.__mro__:
            for key, value in cls.__dict__.items():
                if isinstance(value, Property):
                    attribute = getattr(self, key)
                    # Handle Identifier Columns as string instead of int
                    if isinstance(attribute, Identifier):
                        attribute = str(attribute)
                    dictionary[key] = attribute
        return dictionary

    def on_create(self):
        """Called when *self* is about to be created on the database

        This hook can be overridden on child classes for improved functionality.

        A trick you may want to use: Use :meth:`ORMObject.get_store` to get the
        :class:`store <stoqlib.database.runtime.StoqlibStore>` in which
        *self* lives and do your modifications in it.
        """

    def on_update(self):
        """Called when *self* is about to be updated on the database

        This hook can be overridden on child classes for improved
        functionality.

        A trick you may want to use: Use :meth:`ORMObject.get_store` to get the
        :class:`store <stoqlib.database.runtime.StoqlibStore>` in which
        *self* lives and do your modifications in it.
        """

    def on_delete(self):
        """Called when *self* is about to be removed from the database

        This hook can be overridden on child classes for improved
        functionality.

        A trick you may want to use: Use :meth:`ORMObject.get_store` to get the
        :class:`store <stoqlib.database.runtime.StoqlibStore>` in which
        *self* lives and use it to do a cleanup in other objects for instance,
        so if the store is rolled back, your cleanup will be rolled back too.

        Note that modifying *self* doesn't make sense since it will soon be
        removed from the database.
        """

    def on_object_changed(self, attr, old_value, value):
        """Hook for when an attribute of this object changes.

        This is called when any property of the object has the value changed.
        This will only be called when the value is known (ie, not AutoReload)
        and the value comes from python, not from the database, and the value
        has actually changed from the previous value.
        """
        pass

    def clone(self):
        """Get a persistent copy of an existent object. Remember that we can
        not use copy because this approach will not activate ORMObject
        methods which allow creating persitent objects. We also always
        need a new id for each copied object.

        :returns: the copy of ourselves
        """
        warnings.warn("don't use this", DeprecationWarning, stacklevel=2)
        kwargs = {}
        for column in get_cls_info(self.__class__).columns:
            # FIXME: Make sure this is cloning correctly
            name = column.name
            if name in ['id', 'identifier', 'te_id']:
                continue
            if name.endswith('_id'):
                name = name[:-3]
            kwargs[name] = getattr(self, name)

        klass = type(self)
        return klass(store=self.store, **kwargs)

    def check_unique_value_exists(self, attribute, value, case_sensitive=True):
        """Check database for attribute/value precense

        Check if we already the given attribute/value pair in the database,
        but ignoring this object's ones.

        :param attribute: the attribute that should be unique
        :param value: value that we will check if exists in the database
        :param case_sensitive: If the checking should be case sensitive or
          not.
        :returns: the existing object or ``None``
        """
        return self.check_unique_tuple_exists({attribute: value},
                                              case_sensitive)

    def check_unique_tuple_exists(self, values, case_sensitive=True):
        """Check database for values presence

        Check if we already the given attributes and values in the database,
        but ignoring this object's ones.

        :param values: dictionary of attributes:values that we will check if
          exists in the database.
        :param case_sensitive: If the checking should be case sensitive or
          not.
        :returns: the existing object or ``None``
        """
        if all([value in ['', None] for value in values.values()]):
            return None

        clauses = []
        for attr, value, in values.items():
            self.__class__.validate_attr(attr)

            if not isinstance(value, str) or case_sensitive:
                clauses.append(attr == value)
            else:
                clauses.append(Like(attr, value, case_sensitive=False))

        cls = type(self)
        # Remove myself from the results.
        if hasattr(self, 'id'):
            clauses.append(cls.id != self.id)
        query = And(*clauses)

        try:
            return self.store.find(cls, query).one()
        except NotOneError:
            # FIXME: Instead of breaking stoq if more than one tuple exists,
            # simply return the first object, but log a warning about the
            # database issue. We should have UNIQUE constraints in more places
            # to be sure that this would never happen
            values_str = ["%s => %s" % (k.name, v) for k, v in values.items()]
            log.warning(
                "more than one result found when trying to "
                "check_unique_tuple_exists on table '%s' for values: %r" %
                (self.__class__.__name__, ', '.join(sorted(values_str))))
            return self.store.find(cls, query).any()

    def merge_with(self, other, skip=None, copy_empty_values=True):
        """Does automatic references updating when merging two objects.

        This will update all tables that reference the `other` object and make
        them reference `self` instead.

        After this it should be safe to remove the `other` object. Since there
        is no one referencing it anymore.

        :param skip: A set of (table, column) that should be skiped by the
          automatic update. This are normally tables that require a special
          treatment, like when there are constraints.
        :param copy_empty_values: If True, attributes that are either null or an
          empty string in self will be updated with the value from the other
          object (given that the other attribute is not empty as well)
        """
        skip = skip or set()
        event_skip = DomainMergeEvent.emit(self, other)
        if event_skip:
            skip = skip.union(event_skip)

        if copy_empty_values:
            self.copy_empty_values(other)

        refs = self.store.list_references(type(self).id)
        for (table, column, other_table, other_column, u, d) in refs:
            if (table, column) in skip:
                continue

            clause = Field(table, column) == other.id
            self.store.execute(Update({column: self.id}, clause, table))

    def copy_empty_values(self, other):
        """Copies the values from other object if missing in self

        This will copy all values from the other object that are missing from
        this one.
        """
        empty_values = [None, u'']
        for attr_property, column in self._storm_columns.items():
            self_value = getattr(self, column.name)
            other_value = getattr(other, column.name)
            if self_value in empty_values and other_value not in empty_values:
                setattr(self, column.name, other_value)

    def can_remove(self, skip=None):
        """Check if this object can be removed from the database

        This will check if there's any object referencing self

        :param skip: an itarable containing the (table, column) to skip
            the check. Use this to avoid false positives when you will
            delete those skipped by hand before self.
        """
        skip = skip or set()
        selects = []
        refs = self.store.list_references(self.__class__.id)

        for t_name, c_name, ot_name, oc_name, u, d in refs:
            if (t_name, c_name) in skip:
                continue

            column = Field(t_name, c_name)
            selects.append(
                Select(columns=[1],
                       tables=[t_name],
                       where=column == self.id,
                       limit=1))

        # If everything was skipped, there's no query to execute
        if not len(selects):
            return True

        # We can only pass limit=1 to UnionAll if there's more than one select.
        # If not, storm will put the limit anyway and it will join with the
        # select's limit producing an error: multiple LIMIT clauses not allowed
        if len(selects) > 1:
            extra = {'limit': 1}
        else:
            extra = {}

        return not any(self.store.execute(UnionAll(*selects, **extra)))

    #
    #  Classmethods
    #

    @classmethod
    def get_temporary_identifier(cls, store):
        """Returns a temporary negative identifier

        This should be used when working with syncronized databases and a
        purchase order is being created in a branch different than the
        destination branch.

        The sincronizer will be responsible for setting the definitive
        identifier once the order arives at the destination
        """
        lower_value = store.find(cls).min(cls.identifier)
        return min(lower_value or 0, 0) - 1

    @classmethod
    def find_distinct_values(cls, store, attr, exclude_empty=True):
        """Find distinct values for a given attr

        :param store: a store
        :param attr: the attr we are going to get distinct values for
        :param exclude_empty: if ``True``, empty results (``None`` or
            empty strings) will be removed from the results
        :returns: an iterator of the results
        """
        cls.validate_attr(attr)

        results = store.find(cls)
        results.config(distinct=True)
        for value in results.values(attr):
            if exclude_empty and not value:
                continue
            yield value

    @classmethod
    def get_max_value(cls, store, attr, validate_attr=True, query=Undef):
        """Get the maximum value for a given attr

        On text columns, trying to find the max value for them using MAX()
        on postgres would result in some problems, like '9' being considered
        greater than '10' (because the comparison is done from left to right).

        This will 0-"pad" the values for the comparison, making it compare
        the way we want. Note that because of that, in the example above,
        it would return '09' instead of '9'

        :para store: a store
        :param attr: the attribute to find the max value for
        :returns: the maximum value for the attr
        """
        if validate_attr:
            cls.validate_attr(attr, expected_type=UnicodeCol)

        max_length = Alias(
            Select(columns=[Alias(Max(CharLength(attr)), 'max_length')],
                   tables=[cls],
                   where=query), '_max_length')
        # Using LPad with max_length will workaround most of the cases where
        # the string comparison fails. For example, the common case would
        # consider '9' to be greater than '10'. We could test just strings
        # with length equal to max_length, but than '010' would be greater
        # than '001' would be greater than '10' (that would be excluded from
        # the comparison). By doing lpad, '09' is lesser than '10' and '001'
        # is lesser than '010', working around those cases
        if query is not Undef and query is not None:
            data = store.using(cls, max_length).find(cls, query)
        else:
            data = store.using(cls, max_length).find(cls)
        max_batch = data.max(
            LPad(attr, Field('_max_length', 'max_length'), u'0'))

        # Make the api consistent and return an ampty string instead of None
        # if there's no batch registered on the database
        return max_batch or u''

    @classmethod
    def get_or_create(cls, store, **kwargs):
        """Get the object from the database that matches the given criteria, and if
        it doesn't exist, create a new object, with the properties given already
        set.

        The properties given ideally should be the primary key, or a candidate
        key (unique values).

        :returns: an object matching the query or a newly created one if a
          matching one couldn't be found.
        """
        obj = store.find(cls, **kwargs).one()
        if obj is not None:
            return obj

        obj = cls(store=store)
        # Use setattr instead of passing it to the constructor, since not all
        # constructors accept all properties. There is no check if the
        # properties are valid since it will fail in the store.find() call above.
        for key, value in kwargs.items():
            setattr(obj, key, value)

        return obj

    @classmethod
    def validate_attr(cls, attr, expected_type=None):
        """Make sure attr belongs to cls and has the expected type

        :param attr: the attr we will check if it is on cls
        :param expected_type: the expected type for the attr to be
            an instance of. If ``None`` will default to Property
        :raises: :exc:`TypeError` if the attr is not an instance
            of expected_type
        :raises: :exc:`ValueError` if the attr does not belong to
            this class
        """
        expected_type = expected_type or Property
        if not issubclass(expected_type, Property):
            raise TypeError("expected_type %s needs to be a %s subclass" %
                            (expected_type, Property))

        # We need to iterate over cls._storm_columns to find the
        # attr's property because there's no reference to that property
        # (the descriptor) on the attr
        for attr_property, column in cls._storm_columns.items():
            if column is attr:
                break
        else:
            attr_property = None  # pylint
            raise ValueError("Domain %s does not have a column %s" %
                             (cls.__name__, attr.name))

        if not isinstance(attr_property, expected_type):
            raise TypeError("attr %s needs to be a %s instance" %
                            (attr.name, expected_type))

    @classmethod
    def validate_batch(cls, batch, sellable, storable=None):
        """Verifies if the given |batch| is valid for the given |sellable|.

        :param batch: A |storablebatch| that is being validated
        :param sellable: A |sellable| that we should use to validate against the batch
        :param storable: If provided, the corresponding |storable| for the given
            batch, to avoid unecessary queries.
         """
        if not storable:
            product = sellable.product
            storable = product and product.storable or None

        if not batch:
            # When batch is not given, the only accepted scenarios are that storable is
            # None or the storable does not require batches
            if not storable or not storable.is_batch:
                return
            raise ValueError('Batch should not be None for %r' % sellable)

        # From now on, batch is not None
        if not storable:
            raise ValueError('Batches should only be used with storables, '
                             'but %r is not one' % sellable)
        if not storable.is_batch:
            raise ValueError('This storable %r does not require a batch' %
                             storable)
        if batch.storable != storable:
            raise ValueError('Given batch %r and storable %r are not related' %
                             (batch, storable))
Beispiel #22
0
class StockDecrease(Domain):
    """Stock Decrease object implementation.

    Stock Decrease is when the user need to manually decrease the stock
    quantity, for some reason that is not a sale, transfer or other cases
    already covered in stoqlib.
    """

    __storm_table__ = 'stock_decrease'

    #: Stock Decrease is still being edited
    STATUS_INITIAL = 0

    #: Stock Decrease is confirmed and stock items have been decreased.
    STATUS_CONFIRMED = 1

    statuses = {
        STATUS_INITIAL: _(u'Opened'),
        STATUS_CONFIRMED: _(u'Confirmed')
    }

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

    #: status of the sale
    status = IntCol(default=STATUS_INITIAL)

    reason = UnicodeCol(default=u'')

    #: Some optional additional information related to this sale.
    notes = UnicodeCol(default=u'')

    #: the date sale was created
    confirm_date = DateTimeCol(default_factory=localnow)

    responsible_id = IdCol()

    #: who should be blamed for this
    responsible = Reference(responsible_id, 'LoginUser.id')

    removed_by_id = IdCol()

    removed_by = Reference(removed_by_id, 'Employee.id')

    branch_id = IdCol()

    #: branch where the sale was done
    branch = Reference(branch_id, 'Branch.id')

    cfop_id = IdCol()

    cfop = Reference(cfop_id, 'CfopData.id')

    group_id = IdCol()

    #: the payment group related to this stock decrease
    group = Reference(group_id, 'PaymentGroup.id')

    cost_center_id = IdCol()

    #: the |costcenter| that the cost of the products decreased in this stock
    #: decrease should be accounted for. When confirming a stock decrease with
    #: a |costcenter| set, a |costcenterentry| will be created for each product
    #: decreased.
    cost_center = Reference(cost_center_id, 'CostCenter.id')

    #
    # Classmethods
    #

    @classmethod
    def get_status_name(cls, status):
        if not status in cls.statuses:
            raise DatabaseInconsistency(_(u"Invalid status %d") % status)
        return cls.statuses[status]

    def add_item(self, item):
        assert not item.stock_decrease
        item.stock_decrease = self

    def get_items(self):
        return self.store.find(StockDecreaseItem, stock_decrease=self)

    def remove_item(self, item):
        self.store.remove(item)

    # Status

    def can_confirm(self):
        """Only ordered sales can be confirmed

        :returns: ``True`` if the sale can be confirmed, otherwise ``False``
        """
        return self.status == StockDecrease.STATUS_INITIAL

    def confirm(self):
        """Confirms the sale

        """
        assert self.can_confirm()
        assert self.branch

        store = self.store
        branch = self.branch
        for item in self.get_items():
            if item.sellable.product:
                ProductHistory.add_decreased_item(store, branch, item)
            item.decrease(branch)

        self.status = StockDecrease.STATUS_CONFIRMED

        if self.group:
            self.group.confirm()

    #
    # Accessors
    #

    def get_branch_name(self):
        return self.branch.get_description()

    def get_responsible_name(self):
        return self.responsible.get_description()

    def get_removed_by_name(self):
        if not self.removed_by:
            return u''

        return self.removed_by.get_description()

    def get_total_items_removed(self):
        return sum([item.quantity for item in self.get_items()], 0)

    def get_cfop_description(self):
        return self.cfop.get_description()

    def get_total_cost(self):
        return self.get_items().sum(StockDecreaseItem.cost *
                                    StockDecreaseItem.quantity)

    # Other methods

    def add_sellable(self, sellable, cost=None, quantity=1, batch=None):
        """Adds a new sellable item to a stock decrease

        :param sellable: the |sellable|
        :param cost: the cost for the decrease. If ``None``, sellable.cost
            will be used instead
        :param quantity: quantity to add, defaults to ``1``
        :param batch: the |batch| this sellable comes from, if the sellable is a
          storable. Should be ``None`` if it is not a storable or if the storable
          does not have batches.
        """
        self.validate_batch(batch, sellable=sellable)
        if cost is None:
            cost = sellable.cost

        return StockDecreaseItem(store=self.store,
                                 quantity=quantity,
                                 stock_decrease=self,
                                 sellable=sellable,
                                 batch=batch,
                                 cost=cost)
Beispiel #23
0
class RatingTrack(BaseRating):
    __storm_table__ = 'rating_track'

    rated = Reference(BaseRating.rated_id, Track.id)
Beispiel #24
0
class Playlist(object):
    __storm_table__ = 'playlist'

    id = UUID(primary = True, default_factory = uuid.uuid4)
    user_id = UUID()
    name = Unicode()
    comment = Unicode() # nullable
    public = Bool(default = False)
    created = DateTime(default_factory = now)
    tracks = Unicode()

    user = Reference(user_id, User.id)

    def as_subsonic_playlist(self, user):
        tracks = self.get_tracks()
        info = {
            'id': str(self.id),
            'name': self.name if self.user_id == user.id else '[%s] %s' % (self.user.name, self.name),
            'owner': self.user.name,
            'public': self.public,
            'songCount': len(tracks),
            'duration': sum(map(lambda t: t.duration, tracks)),
            'created': self.created.isoformat()
        }
        if self.comment:
            info['comment'] = self.comment
        return info

    def get_tracks(self):
        if not self.tracks:
            return []

        tracks = []
        should_fix = False
        store = Store.of(self)

        for t in self.tracks.split(','):
            try:
                tid = uuid.UUID(t)
                track = store.get(Track, tid)
                if track:
                    tracks.append(track)
                else:
                    should_fix = True
            except:
                should_fix = True

        if should_fix:
            self.tracks = ','.join(map(lambda t: str(t.id), tracks))
            store.commit()

        return tracks

    def clear(self):
        self.tracks = ""

    def add(self, track):
        if isinstance(track, uuid.UUID):
            tid = track
        elif isinstance(track, Track):
            tid = track.id
        elif isinstance(track, basestring):
            tid = uuid.UUID(track)

        if self.tracks and len(self.tracks) > 0:
            self.tracks = "{},{}".format(self.tracks, tid)
        else:
            self.tracks = str(tid)

    def remove_at_indexes(self, indexes):
        tracks = self.tracks.split(',')
        for i in indexes:
            if i < 0 or i >= len(tracks):
                continue
            tracks[i] = None

        self.tracks = ','.join(t for t in tracks if t)
Beispiel #25
0
class StockDecreaseItem(Domain):
    """An item in a stock decrease object.

    Note that objects of this type should not be created manually, only by
    calling :meth:`StockDecrease.add_sellable`
    """

    __storm_table__ = 'stock_decrease_item'

    stock_decrease_id = IdCol(default=None)

    #: The stock decrease this item belongs to
    stock_decrease = Reference(stock_decrease_id, 'StockDecrease.id')

    sellable_id = IdCol()

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

    batch_id = IdCol()

    #: If the sellable is a storable, the |batch| that it was removed from
    batch = Reference(batch_id, 'StorableBatch.id')

    #: the cost of the |sellable| on the moment this decrease was created
    cost = PriceCol(default=0)

    #: the quantity decreased for this item
    quantity = QuantityCol()

    def __init__(self, store=None, **kw):
        if not 'kw' in kw:
            if not 'sellable' in kw:
                raise TypeError('You must provide a sellable argument')
        Domain.__init__(self, store=store, **kw)

    #
    #  Public API
    #

    def decrease(self, branch):
        # FIXME: We should not be receiving a branch here. We should be using
        # self.stock_decrease.branch for that.
        assert branch

        storable = self.sellable.product_storable
        if storable:
            storable.decrease_stock(
                self.quantity,
                branch,
                StockTransactionHistory.TYPE_STOCK_DECREASE,
                self.id,
                cost_center=self.stock_decrease.cost_center,
                batch=self.batch)

    #
    # Accessors
    #

    def get_total(self):
        return currency(self.cost * self.quantity)

    def get_quantity_unit_string(self):
        return u"%s %s" % (self.quantity, self.sellable.get_unit_description())

    def get_description(self):
        return self.sellable.get_description()
Beispiel #26
0
class Commission(Domain):
    """Commission object implementation

    A Commission is the commission received by a |salesperson|
    for a specific |payment| made by a |sale|.

    There is one Commission for each |payment| of a |sale|.

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

    __storm_table__ = 'commission'

    #: use direct commission to calculate the commission amount
    DIRECT = 0

    #: use installments commission to calculate the commission amount
    INSTALLMENTS = 1

    commission_type = IntCol(default=DIRECT)

    #: The commission amount
    value = PriceCol(default=0)

    salesperson_id = IdCol()

    #: who sold the |sale| this commission applies to
    salesperson = Reference(salesperson_id, 'SalesPerson.id')

    sale_id = IdCol()

    #: the |sale| this commission applies to
    sale = Reference(sale_id, 'Sale.id')

    payment_id = IdCol()

    #: the |payment| this commission applies to
    payment = Reference(payment_id, 'Payment.id')

    #
    #  Domain
    #

    def __init__(self, store=None, **kwargs):
        need_calculate_value = not 'value' in kwargs
        super(Commission, self).__init__(store=store, **kwargs)
        if need_calculate_value:
            self._calculate_value()

    #
    #  Private
    #

    def _calculate_value(self):
        """Calculates the commission amount to be paid"""

        relative_percentage = self._get_payment_percentage()

        # The commission is calculated for all sellable items
        # in sale; a relative percentage is given for each payment
        # of the sale.
        # Eg:
        #   If a customer decides to pay a sale in two installments,
        #   Let's say divided in 20%, and 80% of the total value of the items
        #   which was bought in the sale. Then the commission received by the
        #   sales person is also going to be 20% and 80% of the complete
        #   commission amount for the sale when that specific payment is payed.
        value = Decimal(0)
        for sellable_item in self.sale.get_items():
            value += (self._get_commission(sellable_item.sellable) *
                      sellable_item.get_total() * relative_percentage)

        # The calculation above may have produced a number with more than two
        # digits. Round it to only two
        self.value = quantize(value)

    def _get_payment_percentage(self):
        """Return the payment percentage of sale"""
        total = self.sale.get_sale_subtotal()
        if total == 0:
            return 0
        else:
            return self.payment.value / total

    def _get_commission(self, sellable):
        """Return the properly commission percentage to be used to
        calculate the commission amount, for a given sellable.
        """

        store = self.store
        source = store.find(CommissionSource, sellable=sellable).one()
        if not source and sellable.category:
            source = self._get_category_commission(sellable.category)

        value = 0
        if source:
            if self.commission_type == self.DIRECT:
                value = source.direct_value
            else:
                value = source.installments_value
            value /= Decimal(100)

        return value

    def _get_category_commission(self, category):
        if category:
            store = self.store
            source = store.find(CommissionSource, category=category).one()
            if not source:
                return self._get_category_commission(category.category)
            return source
Beispiel #27
0
class ProductIpiTemplate(BaseIPI):
    __storm_table__ = 'product_ipi_template'
    product_tax_template_id = IntCol()
    product_tax_template = Reference(product_tax_template_id,
                                     'ProductTaxTemplate.id')
Beispiel #28
0
class Invoice(Domain):
    """Stores information about invoices"""

    __storm_table__ = 'invoice'

    TYPE_IN = u'in'
    TYPE_OUT = u'out'

    LEGACY_MODE = u'legacy'
    NFE_MODE = u'nfe'
    NFCE_MODE = u'nfce'

    MODES = {55: NFE_MODE, 65: NFCE_MODE}

    MODE_NAMES = {NFE_MODE: _('NF-e'), NFCE_MODE: _('NFC-e')}

    #: the invoice number
    invoice_number = IntCol()

    #: the operation nature
    operation_nature = UnicodeCol()

    #: the invoice type, representing an IN/OUT operation
    invoice_type = EnumCol(allow_none=False)

    #: the invoice series
    series = IntCol(default=None)

    #: the invoice mode
    mode = EnumCol(default=None)

    #: the key generated by NF-e plugin
    key = UnicodeCol()

    #: numeric code randomly generated for each NF-e
    cnf = UnicodeCol()

    branch_id = IdCol()

    #: the |branch| where this invoice was generated
    branch = Reference(branch_id, 'Branch.id')

    def __init__(self, **kw):
        if not 'branch' in kw:
            kw['branch'] = get_current_branch(kw.get('store'))
        super(Invoice, self).__init__(**kw)

    @classmethod
    def get_next_invoice_number(cls, store, mode=None, series=None):
        return Invoice.get_last_invoice_number(store, series, mode) + 1

    @classmethod
    def get_last_invoice_number(cls, store, series=None, mode=None):
        """Returns the last invoice number. If there is not an invoice
        number used, the returned value will be zero.

        :param store: a store
        :returns: an integer representing the last sale invoice number
        """
        current_branch = get_current_branch(store)
        last = store.find(cls, branch=current_branch, series=series,
                          mode=mode).max(cls.invoice_number)
        return last or 0

    def save_nfe_info(self, cnf, key):
        """ Save the CNF and KEY generated in NF-e.
        """
        self.cnf = cnf
        self.key = key

    def check_unique_invoice_number_by_branch(self, invoice_number, branch,
                                              mode):
        """Check if the invoice_number is used in determined branch
        """
        queries = {
            Invoice.invoice_number: invoice_number,
            Invoice.branch_id: branch.id,
            Invoice.mode: mode
        }
        return self.check_unique_tuple_exists(queries)

    def check_invoice_info_consistency(self):
        """If the invoice number is set, series and mode should also be.

        We should have a database constraint for this, but since these three data
        isn't saved at once, the constraint would brake every time.
        """
        # FIXME: The nfce plugin is responsible for generating those
        # information now, but that broke our nfe plugin since it
        # needs an invoice number, but not the mode an the series.
        # We should find a better way of handling this since we don't
        # want to polute the nfe/nfce invoice number namespace.
        pg = get_plugin_manager()
        if pg.is_active('nfe'):  # pragma nocoverage
            return

        # FIXME: We should not use assert in this kind of code since
        # there's an optimization flag on python that removes all the asserts
        assert ((self.invoice_number and self.series and self.mode)
                or (not self.invoice_number and not self.series
                    and not self.mode))

    def on_create(self):
        self.check_invoice_info_consistency()

    def on_update(self):
        self.check_invoice_info_consistency()
Beispiel #29
0
class ReceivingOrderItem(Domain):
    """This class stores information of the purchased items.

    Note that objects of this type should not be created manually, only by
    calling Receiving
    """

    __storm_table__ = 'receiving_order_item'

    #: the total quantity received for a certain |product|
    quantity = QuantityCol()

    #: the cost for each |product| received
    cost = PriceCol()

    purchase_item_id = IdCol()

    purchase_item = Reference(purchase_item_id, 'PurchaseItem.id')

    # FIXME: This could be a product instead of a sellable, since we only buy
    # products from the suppliers.
    sellable_id = IdCol()

    #: the |sellable|
    sellable = Reference(sellable_id, 'Sellable.id')

    batch_id = IdCol()

    #: If the sellable is a storable, the |batch| that it was received in
    batch = Reference(batch_id, 'StorableBatch.id')

    receiving_order_id = IdCol()

    receiving_order = Reference(receiving_order_id, 'ReceivingOrder.id')

    parent_item_id = IdCol()
    parent_item = Reference(parent_item_id, 'ReceivingOrderItem.id')

    children_items = ReferenceSet('id', 'ReceivingOrderItem.parent_item_id')

    #
    # Properties
    #

    @property
    def unit_description(self):
        unit = self.sellable.unit
        return u"%s" % (unit and unit.description or u"")

    @property
    def returned_quantity(self):
        return self.store.find(StockDecreaseItem,
                               receiving_order_item=self).sum(
                                   StockDecreaseItem.quantity) or Decimal('0')

    #
    # Accessors
    #

    def get_remaining_quantity(self):
        """Get the remaining quantity from the purchase order this item
        is included in.
        :returns: the remaining quantity
        """
        return self.purchase_item.get_pending_quantity()

    def get_total(self):
        # We need to use the the purchase_item cost, since the current cost
        # might be different.
        cost = self.purchase_item.cost
        return currency(self.quantity * cost)

    def get_quantity_unit_string(self):
        unit = self.sellable.unit
        data = u"%s %s" % (self.quantity, unit and unit.description or u"")
        # The unit may be empty
        return data.strip()

    def add_stock_items(self):
        """This is normally called from ReceivingOrder when
        a the receving order is confirmed.
        """
        store = self.store
        if self.quantity > self.get_remaining_quantity():
            raise ValueError(u"Quantity received (%d) is greater than "
                             u"quantity ordered (%d)" %
                             (self.quantity, self.get_remaining_quantity()))

        branch = self.receiving_order.branch
        storable = self.sellable.product_storable
        purchase = self.purchase_item.order
        if storable is not None:
            storable.increase_stock(
                self.quantity,
                branch,
                StockTransactionHistory.TYPE_RECEIVED_PURCHASE,
                self.id,
                self.cost,
                batch=self.batch)
        purchase.increase_quantity_received(self.purchase_item, self.quantity)
        ProductHistory.add_received_item(store, branch, self)

    def is_totally_returned(self):
        children = self.children_items
        if children.count():
            return all(child.quantity == child.returned_quantity
                       for child in children)

        return self.quantity == self.returned_quantity
Beispiel #30
0
class FiscalBookEntry(Domain):

    __storm_table__ = 'fiscal_book_entry'

    (TYPE_PRODUCT, TYPE_SERVICE, TYPE_INVENTORY) = range(3)

    date = DateTimeCol(default_factory=localnow)
    is_reversal = BoolCol(default=False)
    invoice_number = IntCol()
    cfop_id = IdCol()
    cfop = Reference(cfop_id, 'CfopData.id')
    branch_id = IdCol()
    branch = Reference(branch_id, 'Branch.id')
    drawee_id = IdCol(default=None)
    drawee = Reference(drawee_id, 'Person.id')
    payment_group_id = IdCol(default=None)
    payment_group = Reference(payment_group_id, 'PaymentGroup.id')
    iss_value = PriceCol(default=None)
    icms_value = PriceCol(default=None)
    ipi_value = PriceCol(default=None)
    entry_type = IntCol(default=None)

    @classmethod
    def has_entry_by_payment_group(cls, store, payment_group, entry_type):
        return bool(
            cls.get_entry_by_payment_group(store, payment_group, entry_type))

    @classmethod
    def get_entry_by_payment_group(cls, store, payment_group, entry_type):
        return store.find(cls,
                          payment_group=payment_group,
                          is_reversal=False,
                          entry_type=entry_type).one()

    @classmethod
    def _create_fiscal_entry(cls,
                             store,
                             entry_type,
                             group,
                             cfop,
                             invoice_number,
                             iss_value=0,
                             icms_value=0,
                             ipi_value=0):
        return FiscalBookEntry(entry_type=entry_type,
                               iss_value=iss_value,
                               ipi_value=ipi_value,
                               icms_value=icms_value,
                               invoice_number=invoice_number,
                               cfop=cfop,
                               drawee=group.recipient,
                               branch=get_current_branch(store),
                               date=TransactionTimestamp(),
                               payment_group=group,
                               store=store)

    @classmethod
    def create_product_entry(cls,
                             store,
                             group,
                             cfop,
                             invoice_number,
                             value,
                             ipi_value=0):
        """Creates a new product entry in the fiscal book

        :param store: a store
        :param group: payment group
        :type  group: :class:`PaymentGroup`
        :param cfop: cfop for the entry
        :type  cfop: :class:`CfopData`
        :param invoice_number: payment invoice number
        :param value: value of the payment
        :param ipi_value: ipi value of the payment
        :returns: a fiscal book entry
        :rtype: :class:`FiscalBookEntry`
        """
        return cls._create_fiscal_entry(
            store,
            FiscalBookEntry.TYPE_PRODUCT,
            group,
            cfop,
            invoice_number,
            icms_value=value,
            ipi_value=ipi_value,
        )

    @classmethod
    def create_service_entry(cls, store, group, cfop, invoice_number, value):
        """Creates a new service entry in the fiscal book

        :param store: a store
        :param group: payment group
        :type  group: :class:`PaymentGroup`
        :param cfop: cfop for the entry
        :type  cfop: :class:`CfopData`
        :param invoice_number: payment invoice number
        :param value: value of the payment
        :returns: a fiscal book entry
        :rtype: :class:`FiscalBookEntry`
        """
        return cls._create_fiscal_entry(
            store,
            FiscalBookEntry.TYPE_SERVICE,
            group,
            cfop,
            invoice_number,
            iss_value=value,
        )

    def reverse_entry(self,
                      invoice_number,
                      iss_value=None,
                      icms_value=None,
                      ipi_value=None):
        store = self.store
        icms_value = icms_value if icms_value is not None else self.icms_value
        iss_value = iss_value if iss_value is not None else self.iss_value
        ipi_value = ipi_value if ipi_value is not None else self.ipi_value

        return FiscalBookEntry(
            entry_type=self.entry_type,
            iss_value=iss_value,
            icms_value=icms_value,
            ipi_value=ipi_value,
            cfop_id=sysparam.get_object_id('DEFAULT_SALES_CFOP'),
            branch=self.branch,
            invoice_number=invoice_number,
            drawee=self.drawee,
            is_reversal=True,
            payment_group=self.payment_group,
            store=store)