コード例 #1
0
 def __init__(self, otherClass=None, joinColumn=None,
              intermediateTable=None, otherColumn=None, orderBy=None,
              prejoins=_IGNORED):
     if intermediateTable:
         args = ("<primary key>",
                 "%s.%s" % (intermediateTable, joinColumn),
                 "%s.%s" % (intermediateTable, otherColumn),
                 "%s.<primary key>" % otherClass)
     else:
         args = ("<primary key>", "%s.%s" % (otherClass, joinColumn))
     ReferenceSet.__init__(self, *args)
     self._orderBy = orderBy
コード例 #2
0
 def __init__(self, otherClass=None, joinColumn=None,
              intermediateTable=None, otherColumn=None, orderBy=None,
              prejoins=None):
     if intermediateTable:
         args = ("<primary key>",
                 "%s.%s" % (intermediateTable, joinColumn),
                 "%s.%s" % (intermediateTable, otherColumn),
                 "%s.<primary key>" % otherClass)
     else:
         args = ("<primary key>", "%s.%s" % (otherClass, joinColumn))
     ReferenceSet.__init__(self, *args)
     self._orderBy = orderBy
     self._otherClass = otherClass
     self._prejoins = prejoins
コード例 #3
0
class Product(BaseModel):

    __storm_table__ = "sfec_product"

    name = Unicode()

    stock = Int(default=0)

    description = Unicode()

    price = Decimal()

    is_available = Bool(default=False)

    categories = ReferenceSet('Product.id', 'CategoryProduct.product_id',
                              'CategoryProduct.category_id', 'Category.id')

    #
    # Implicit Properties
    #

    @property
    def category_list(self):
        return [c.name for c in self.categories]

    def dict(self):
        pdict = super(Product, self).dict()
        pdict['price'] = float(pdict['price'])  # price is decimal
        return pdict
コード例 #4
0
class MusicAlbum(BaseModel):
    '''A music file container

    Music can be categorized many ways.  Albums are found in the music file's
    ID3 tags
    '''

    __storm_table__ = 'musicalbum'
    id = Int(primary=True)
    artist = Unicode()
    title = Unicode()
    year = Int()
    genre = Unicode()
    tracks = ReferenceSet('MusicAlbum.id', 'MusicTrack.album_id')

    def to_dict(self, recurse=True):
        '''See BaseModel.to_dict.'''
        ret = {
            'id': self.id,
            'artist': self.artist,
            'title': self.title,
            'year': self.year,
            'genre': self.genre,
        }
        if recurse:
            ret['tracks'] = [
                track.to_dict(recurse=False) for track in self.tracks
            ]
        return ret
コード例 #5
0
ファイル: sqlobject.py プロジェクト: Jokymon/timetracker
 def __get__(self, obj, cls=None):
     if obj is None:
         return self
     bound_reference_set = ReferenceSet.__get__(self, obj)
     target_cls = bound_reference_set._target_cls
     where_clause = bound_reference_set._get_where_clause()
     return SQLObjectResultSet(target_cls, where_clause,
                               orderBy=self._orderBy,
                               prejoins=self._prejoins)
コード例 #6
0
 def __get__(self, obj, cls=None):
     if obj is None:
         return self
     bound_reference_set = ReferenceSet.__get__(self, obj)
     target_cls = bound_reference_set._target_cls
     where_clause = bound_reference_set._get_where_clause()
     return SQLObjectResultSet(target_cls, where_clause,
                               orderBy=self._orderBy,
                               prejoins=self._prejoins)
コード例 #7
0
class UserProfile(Domain):
    """User profile definition."""

    __storm_table__ = 'user_profile'

    #: Name of the user profile.
    name = UnicodeCol()

    #: Profile settings that describes the access this profile has to an app.
    profile_settings = ReferenceSet('id', 'ProfileSettings.user_profile_id')

    #: Maximum discount this profile can allow to sale items.
    max_discount = PercentCol(default=0)

    @classmethod
    def create_profile_template(cls, store, name, has_full_permission=False):
        profile = cls(store=store, name=name)
        descr = get_utility(IApplicationDescriptions)
        for app_dir in descr.get_application_names():
            ProfileSettings(store=store,
                            has_permission=has_full_permission,
                            app_dir_name=app_dir,
                            user_profile=profile)
        return profile

    @classmethod
    def get_default(cls, store):
        # FIXME: We need a way to set the default profile in the interface,
        # instead of relying on the name (the user may change it)
        profile = store.find(cls, name=_(u'Salesperson')).one()
        # regression: check if it was not created in english.
        if not profile:
            profile = store.find(cls, name=u'Salesperson').one()

        # Just return any other profile, so that the user is created with
        # one.
        if not profile:
            profile = store.find(cls).any()
        return profile

    def add_application_reference(self, app_name, has_permission=False):
        store = self.store
        ProfileSettings(store=store,
                        app_dir_name=app_name,
                        has_permission=has_permission,
                        user_profile=self)

    def check_app_permission(self, app_name):
        """Check if the user has permission to use an application
        :param app_name: name of application to check
        """
        store = self.store
        return bool(
            store.find(ProfileSettings,
                       user_profile=self,
                       app_dir_name=app_name,
                       has_permission=True).one())
コード例 #8
0
 def __get__(self, obj, cls=None):
     if obj is None:
         return self
     bound_reference_set = ReferenceSet.__get__(self, obj)
     target_cls = bound_reference_set._target_cls
     result_set = bound_reference_set.find()
     if self._orderBy:
         result_set.order_by(*target_cls._parse_orderBy(self._orderBy))
     return SQLObjectResultSet(result_set, target_cls)
コード例 #9
0
 def __get__(self, obj, cls=None):
     if obj is None:
         return self
     bound_reference_set = ReferenceSet.__get__(self, obj)
     target_cls = bound_reference_set._target_cls
     result_set = bound_reference_set.find()
     if self._orderBy:
         result_set.order_by(*target_cls._parse_orderBy(self._orderBy))
     return SQLObjectResultSet(result_set, target_cls)
コード例 #10
0
ファイル: wrapper.py プロジェクト: cogini/storm
 def __get__(self, local, cls=None):
     """
     Wrapper around C{ReferenceSet.__get__}.
     """
     store = Store.of(local)
     if store is None:
         return None
     _thread = store._deferredStore.thread
     boundReference = ReferenceSet.__get__(self, local, cls)
     return DeferredBoundReference(_thread, boundReference)
コード例 #11
0
class Category(BaseModel):

    __storm_table__ = "sfec_category"

    name = Unicode()

    products = ReferenceSet('Category.id', 'CategoryProduct.category_id',
                            'CategoryProduct.product_id', 'Product.id')

    def __init__(self, name):
        self.name = name
コード例 #12
0
class MusicPlaylist(BaseModel):
    '''A music file container

    Users can create and add music tracks to a playlist
    '''

    __storm_table__ = 'musicplaylist'
    id = Int(primary=True)
    title = Unicode()
    tracks = ReferenceSet('MusicPlaylist.id', 'MusicPlaylistTrack.playlist_id',
                          'MusicPlaylistTrack.track_id', 'MusicTrack.id')
コード例 #13
0
ファイル: order.py プロジェクト: thanhtd91/SFECommerce
class Order(BaseModel):

    __storm_table__ = "sfec_order"

    status = Unicode()

    products = ReferenceSet('Order.id', 'OrderProduct.order_id')

    user_id = Int()
    user = Reference(user_id, User.id)

    def __init__(self):
        self.status = u"Buying"
コード例 #14
0
ファイル: db.py プロジェクト: sanderd/supysonic
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
コード例 #15
0
ファイル: devices.py プロジェクト: lucaslamounier/stoq
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()
コード例 #16
0
class PhotoAlbum(BaseModel):
    '''A photo group'''

    __storm_table__ = 'photoalbum'
    id = Int(primary=True)
    title = Unicode()
    description = Unicode()
    creation_date = DateTime()
    images = ReferenceSet('PhotoAlbum.id', 'PhotoImage.album_id')

    def to_dict(self, recurse=True):
        '''See BaseModel.to_dict.'''
        ret = {
            'id': self.id,
            'title': self.title,
            'description': self.description,
            'creation_date': self.creation_date,
        }
        if recurse:
            ret['images'] = [
                image.to_dict(recurse=False) for image in self.images
            ]
        return ret
コード例 #17
0
ファイル: sqlobject.py プロジェクト: Jokymon/timetracker
 def _get_bound_reference_set(self, obj):
     assert obj is not None
     return ReferenceSet.__get__(self, obj)
コード例 #18
0
ファイル: ecfdomain.py プロジェクト: sarkis89/stoq
class ECFPrinter(Domain):
    """
    @param model:
    @param brand:
    @param device_name:
    @param device_serial:
    @param station:
    @param is_active:
    @param constants:
    @param baudrate:
    @cvar last_sale: reference for the last Sale
    @cvar last_till_entry: reference for the last TillEntry
    @cvar user_number: the current registrer user in the printer
    @cvar register_date: when the current user was registred
    @cvar register_cro: cro when the user was registred
    """

    __storm_table__ = 'ecf_printer'

    model = UnicodeCol()
    brand = UnicodeCol()
    device_name = UnicodeCol()
    device_serial = UnicodeCol()
    station_id = IdCol()
    station = Reference(station_id, 'BranchStation.id')
    is_active = BoolCol(default=True)
    baudrate = IntCol()
    last_sale_id = IdCol(default=None)
    last_sale = Reference(last_sale_id, 'Sale.id')
    last_till_entry_id = IdCol(default=None)
    last_till_entry = Reference(last_till_entry_id, 'TillEntry.id')
    user_number = IntCol(default=None)
    register_date = DateTimeCol(default=None)
    register_cro = IntCol(default=None)

    constants = ReferenceSet('id', 'DeviceConstant.printer_id')

    #
    # Public API
    #

    def create_fiscal_printer_constants(self):
        """
        Creates constants for a fiscal printer
        This can be called multiple times
        """
        # We only want to populate 'empty' objects.
        if not self.constants.find().is_empty():
            return

        store = self.store
        driver = self.get_fiscal_driver()
        constants = driver.get_constants()
        for constant in constants.get_items():
            constant_value = None
            if isinstance(constant, PaymentMethodType):
                constant_type = DeviceConstant.TYPE_PAYMENT
            elif isinstance(constant, UnitType):
                constant_type = DeviceConstant.TYPE_UNIT
            else:
                continue

            DeviceConstant(constant_type=constant_type,
                           constant_name=unicode(describe_constant(constant)),
                           constant_value=constant_value,
                           constant_enum=int(constant),
                           device_value=constants.get_value(constant, None),
                           printer=self,
                           store=store)

        for constant, device_value, value in driver.get_tax_constants():
            # FIXME: Looks like this is not used and/or is duplicating code from
            # ecfpriterdialog.py (_populate_constants)
            if constant == TaxType.CUSTOM:
                constant_name = '%0.2f %%' % value
            else:
                constant_name = describe_constant(constant)
            DeviceConstant(constant_type=DeviceConstant.TYPE_TAX,
                           constant_name=unicode(constant_name),
                           constant_value=value,
                           constant_enum=int(constant),
                           device_value=device_value,
                           printer=self,
                           store=store)

    def get_constants_by_type(self, constant_type):
        """
        Fetchs a list of constants for the current ECFPrinter object.
        @param constant_type: type of constant
        @type constant_type: :class:`DeviceConstant`
        @returns: list of constants
        """
        return self.store.find(DeviceConstant, printer=self,
                               constant_type=constant_type)

    def get_payment_constant(self, payment):
        """
        @param payment: the payment whose method we will lookup the constant
        @returns: the payment constant
        @rtype: :class:`DeviceConstant`
        """
        constant_enum = payment.method.operation.get_constant(payment)

        if constant_enum is None:
            raise AssertionError

        return self.store.find(DeviceConstant,
                               printer=self,
                               constant_type=DeviceConstant.TYPE_PAYMENT,
                               constant_enum=int(constant_enum)).one()

    def get_tax_constant_for_device(self, sellable):
        """
        Returns a tax_constant for a device
        Raises DeviceError if a constant is not found

        @param sellable: sellable which has the tax codes
        @type sellable: :class:`stoqlib.domain.sellable.Sellable`
        @returns: the tax constant
        @rtype: :class:`DeviceConstant`
        """

        sellable_constant = sellable.get_tax_constant()
        if sellable_constant is None:
            raise DeviceError("No tax constant set for sellable %r" % sellable)

        store = self.store
        if sellable_constant.tax_type == TaxType.CUSTOM:
            constant = DeviceConstant.get_custom_tax_constant(
                self, sellable_constant.tax_value, store)
            if constant is None:
                raise DeviceError(_(
                    "fiscal printer is missing a constant for the custom "
                    "tax constant '%s'") % (sellable_constant.description, ))
        else:
            constant = DeviceConstant.get_tax_constant(
                self, sellable_constant.tax_type, store)
            if constant is None:
                raise DeviceError(_(
                    "fiscal printer is missing a constant for tax "
                    "constant '%s'") % (sellable_constant.description, ))

        return constant

    def get_fiscal_driver(self):
        if self.brand == 'virtual':
            port = VirtualPort()
        else:
            port = SerialPort(device=self.device_name, baudrate=self.baudrate)
        return FiscalPrinter(brand=self.brand, model=self.model, port=port)

    def set_user_info(self, user_info):
        self.user_number = user_info.user_number
        self.register_cro = user_info.cro
        self.register_date = user_info.register_date

    #
    # IActive implementation
    #

    def inactivate(self):
        self.is_active = False

    def activate(self):
        self.is_active = True

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

    #
    # IDescribable implementation
    #

    def get_description(self):
        # Quick workaround to avoid calling FiscalPrinter.setup(), since that
        # may send commands to the ECF, and we just need the description.
        # TODO: Improve stoqdrivers so we can get this easyer
        port = VirtualPort()
        driver = BasePrinter(brand=self.brand, model=self.model, port=port)
        return driver.get_model_name()

    @classmethod
    def get_last_document(cls, station, store):
        return store.find(cls, station=station, is_active=True).one()
コード例 #19
0
ファイル: production.py プロジェクト: victornovy/stoq
class ProductionProducedItem(Domain):
    """This class represents a composed product that was produced, but
    didn't enter the stock yet. Its used mainly for the quality assurance
    process
    """

    __storm_table__ = 'production_produced_item'

    order_id = IdCol()
    order = Reference(order_id, 'ProductionOrder.id')
    # ProductionItem already has a reference to Product, but we need it for
    # constraint checks UNIQUE(product_id, serial_number)
    product_id = IdCol()
    product = Reference(product_id, 'Product.id')
    produced_by_id = IdCol()
    produced_by = Reference(produced_by_id, 'LoginUser.id')
    produced_date = DateTimeCol()
    serial_number = IntCol()
    entered_stock = BoolCol(default=False)
    test_passed = BoolCol(default=False)
    test_results = ReferenceSet(
        'id', 'ProductionItemQualityResult.produced_item_id')

    def get_pending_tests(self):
        tests_done = set([t.quality_test for t in self.test_results])
        all_tests = set(self.product.quality_tests)
        return list(all_tests.difference(tests_done))

    @classmethod
    def get_last_serial_number(cls, product, store):
        return store.find(cls, product=product).max(cls.serial_number) or 0

    @classmethod
    def is_valid_serial_range(cls, product, first, last, store):
        query = And(cls.product_id == product.id, cls.serial_number >= first,
                    cls.serial_number <= last)
        # There should be no results for the range to be valid
        return store.find(cls, query).is_empty()

    def send_to_stock(self):
        # Already is in stock
        if self.entered_stock:
            return

        storable = self.product.storable
        storable.increase_stock(1, self.order.branch,
                                StockTransactionHistory.TYPE_PRODUCTION_SENT,
                                self.id)
        self.entered_stock = True

    def set_test_result_value(self, quality_test, value, tester):
        store = self.store
        result = store.find(ProductionItemQualityResult,
                            quality_test=quality_test,
                            produced_item=self).one()
        if not result:
            result = ProductionItemQualityResult(store=self.store,
                                                 quality_test=quality_test,
                                                 produced_item=self,
                                                 tested_by=tester,
                                                 result_value=u'')
        else:
            result.tested_by = tester

        result.tested_date = localnow()
        result.set_value(value)
        return result

    def get_test_result(self, quality_test):
        store = self.store
        return store.find(ProductionItemQualityResult,
                          quality_test=quality_test,
                          produced_item=self).one()

    def check_tests(self):
        """Checks if all tests for this produced items passes.

        If all tests passes, sets self.test_passed = True
        """
        results = [i.test_passed for i in self.test_results]

        passed = all(results)
        self.test_passed = (passed and len(results)
                            == self.product.quality_tests.count())
        if self.test_passed:
            self.order.try_finalize_production()
コード例 #20
0
ファイル: production.py プロジェクト: victornovy/stoq
class ProductionOrder(Domain):
    """Production Order object implementation.
    """

    __storm_table__ = 'production_order'

    #: The production order is opened, production items might have been added.
    ORDER_OPENED = u'opened'

    #: The production order is waiting some conditions to start the
    #: manufacturing process.
    ORDER_WAITING = u'waiting'

    #: The production order have already started.
    ORDER_PRODUCING = u'producing'

    #: The production is in quality assurance phase.
    ORDER_QA = u'quality-assurance'

    #: The production have finished.
    ORDER_CLOSED = u'closed'

    #: Production cancelled
    ORDER_CANCELLED = u'cancelled'

    statuses = collections.OrderedDict([
        (ORDER_OPENED, _(u'Opened')),
        (ORDER_WAITING, _(u'Waiting')),
        (ORDER_PRODUCING, _(u'Producing')),
        (ORDER_QA, _(u'Quality Assurance')),
        (ORDER_CLOSED, _(u'Closed')),
        (ORDER_CANCELLED, _(u'Cancelled')),
    ])

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

    #: the production order status
    status = EnumCol(allow_none=False, default=ORDER_OPENED)

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

    #: the date when the production order have been closed
    close_date = DateTimeCol(default=None)

    #: the date when the production order have been cancelled
    cancel_date = DateTimeCol(default=None)

    #: the production order description
    description = UnicodeCol(default=u'')

    expected_start_date = DateTimeCol(default=None)

    start_date = DateTimeCol(default=None)

    responsible_id = IdCol(default=None)

    #: the person responsible for the production order
    responsible = Reference(responsible_id, 'Employee.id')

    branch_id = IdCol()

    #: branch this production belongs to
    branch = Reference(branch_id, 'Branch.id')

    produced_items = ReferenceSet('id', 'ProductionProducedItem.order_id')

    #
    # IContainer implmentation
    #

    def get_items(self):
        return self.store.find(ProductionItem, order=self)

    def add_item(self, sellable, quantity=Decimal(1)):
        return ProductionItem(order=self,
                              product=sellable.product,
                              quantity=quantity,
                              store=self.store)

    def remove_item(self, item):
        assert isinstance(item, ProductionItem)
        if item.order is not self:
            raise ValueError(
                _(u'Argument item must have an order attribute '
                  u'associated with the current production '
                  u'order instance.'))
        item.order = None
        self.store.maybe_remove(item)

    #
    # Public API
    #

    def can_cancel(self):
        """Checks if this order can be cancelled

        Only orders that didn't start yet can be canceled, this means
        only opened and waiting productions.
        """
        return self.status in [self.ORDER_OPENED, self.ORDER_WAITING]

    def can_finalize(self):
        """Checks if this order can be finalized

        Only orders that didn't start yet can be canceled, this means
        only producing and waiting qa productions.
        """
        return self.status in [self.ORDER_PRODUCING, self.ORDER_QA]

    def get_service_items(self):
        """Returns all the services needed by this production.

        :returns: a sequence of :class:`ProductionService` instances.
        """
        return self.store.find(ProductionService, order=self)

    def remove_service_item(self, item):
        assert isinstance(item, ProductionService)
        if item.order is not self:
            raise ValueError(
                _(u'Argument item must have an order attribute '
                  u'associated with the current production '
                  u'order instance.'))
        item.order = None
        self.store.maybe_remove(item)

    def get_material_items(self):
        """Returns all the material needed by this production.

        :returns: a sequence of :class:`ProductionMaterial` instances.
        """
        return self.store.find(
            ProductionMaterial,
            order=self,
        )

    def start_production(self):
        """Start the production by allocating all the material needed.
        """
        assert self.status in [
            ProductionOrder.ORDER_OPENED, ProductionOrder.ORDER_WAITING
        ]

        for material in self.get_material_items():
            material.allocate()

        self.start_date = localtoday()
        self.status = ProductionOrder.ORDER_PRODUCING

    def cancel(self):
        """Cancel the production when this is Open or Waiting.
        """
        assert self.can_cancel()
        self.status = self.ORDER_CANCELLED
        self.cancel_date = localtoday()

    def is_completely_produced(self):
        return all(i.is_completely_produced() for i in self.get_items())

    def is_completely_tested(self):
        # Produced items are only stored if there are quality tests for this
        # product
        produced_items = list(self.produced_items)
        if not produced_items:
            return True

        return all([item.test_passed for item in produced_items])

    def try_finalize_production(self, ignore_completion=False):
        """When all items are completely produced, change the status of the
        production to CLOSED.
        """
        assert self.can_finalize(), self.status

        if ignore_completion:
            is_produced = True
        else:
            is_produced = self.is_completely_produced()
        is_tested = self.is_completely_tested()

        if is_produced and not is_tested:
            # Fully produced but not fully tested. Keep status as QA
            self.status = ProductionOrder.ORDER_QA
        elif is_produced and is_tested:
            # All items must be completely produced and tested
            self.close_date = localtoday()
            self.status = ProductionOrder.ORDER_CLOSED

        # If the order is closed, return the the remaining allocated material to
        # the stock
        if self.status == ProductionOrder.ORDER_CLOSED:
            # Return remaining allocated material to the stock
            for m in self.get_material_items():
                m.return_remaining()

            # Increase the stock for the produced items
            for p in self.produced_items:
                p.send_to_stock()

    def set_production_waiting(self):
        assert self.status == ProductionOrder.ORDER_OPENED

        self.status = ProductionOrder.ORDER_WAITING

    def get_status_string(self):
        return ProductionOrder.statuses[self.status]

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

    def get_responsible_name(self):
        if self.responsible is not None:
            return self.responsible.person.name
        return u''

    #
    # IDescribable implementation
    #

    def get_description(self):
        return self.description
コード例 #21
0
class ReturnedSaleItem(Domain):
    """An item of a :class:`returned sale <ReturnedSale>`

    Note that objects of this type should never be created manually, only by
    calling :meth:`Sale.create_sale_return_adapter`
    """

    __storm_table__ = 'returned_sale_item'

    #: the returned quantity
    quantity = QuantityCol(default=0)

    #: The price which this :obj:`.sale_item` was sold.
    #: When creating this object, if *price* is not passed to the
    #: contructor, it defaults to :obj:`.sale_item.price` or
    #: :obj:`.sellable.price`
    price = PriceCol()

    sale_item_id = IdCol(default=None)

    #: the returned |saleitem|
    sale_item = Reference(sale_item_id, 'SaleItem.id')

    sellable_id = IdCol()

    #: The returned |sellable|
    #: Note that if :obj:`.sale_item` != ``None``, this is the same as
    #: :obj:`.sale_item.sellable`
    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')

    returned_sale_id = IdCol()

    #: the |returnedsale| which this item belongs
    returned_sale = Reference(returned_sale_id, 'ReturnedSale.id')

    #: Id of ICMS tax in product tax template
    icms_info_id = IdCol()

    #: the :class:`stoqlib.domain.taxes.InvoiceItemIcms` tax for *self*
    icms_info = Reference(icms_info_id, 'InvoiceItemIcms.id')

    #: Id of IPI tax in product tax template
    ipi_info_id = IdCol()

    #: the :class:`stoqlib.domain.taxes.InvoiceItemIpi` tax fo *self*
    ipi_info = Reference(ipi_info_id, 'InvoiceItemIpi.id')

    #: Id of PIS tax in product tax template
    pis_info_id = IdCol()

    #: the :class:`stoqlib.domain.taxes.InvoiceItemPis` tax fo *self*
    pis_info = Reference(pis_info_id, 'InvoiceItemPis.id')

    #: Id of COFINS tax in product tax template
    cofins_info_id = IdCol()

    #: the :class:`stoqlib.domain.taxes.InvoiceItemCofins` tax fo *self*
    cofins_info = Reference(cofins_info_id, 'InvoiceItemCofins.id')

    item_discount = Decimal('0')

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

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

    def __init__(self, store=None, **kwargs):
        # TODO: Add batch logic here. (get if from sale_item or check if was
        # passed togheter with sellable)
        sale_item = kwargs.get('sale_item')
        sellable = kwargs.get('sellable')

        if not sale_item and not sellable:
            raise ValueError(
                "A sale_item or a sellable is mandatory to create this object")
        elif sale_item and sellable and sale_item.sellable != sellable:
            raise ValueError("sellable must be the same as sale_item.sellable")
        elif sale_item and not sellable:
            sellable = sale_item.sellable
            kwargs['sellable'] = sellable

        if not 'price' in kwargs:
            # sale_item.price takes priority over sellable.price
            kwargs['price'] = sale_item.price if sale_item else sellable.price

        check_tax_info_presence(kwargs, store)

        super(ReturnedSaleItem, self).__init__(store=store, **kwargs)

        product = self.sellable.product
        if product:
            self.ipi_info.set_item_tax(self)
            self.icms_info.set_item_tax(self)
            self.pis_info.set_item_tax(self)
            self.cofins_info.set_item_tax(self)

    @property
    def total(self):
        """The total being returned

        This is the same as :obj:`.price` * :obj:`.quantity`
        """
        return self.price * self.quantity

    #
    # IInvoiceItem implementation
    #

    @property
    def base_price(self):
        return self.price

    @property
    def parent(self):
        return self.returned_sale

    @property
    def nfe_cfop_code(self):
        sale = self.returned_sale.sale
        client_address = sale.client.person.get_main_address()
        branch_address = sale.branch.person.get_main_address()

        same_state = True
        if branch_address.city_location.state != client_address.city_location.state:
            same_state = False

        if same_state:
            return u'1202'
        else:
            return u'2202'

    #
    #  Public API
    #

    def get_total(self):
        return self.total

    def return_(self, branch):
        """Do the real return of this item

        When calling this, the real return will happen, that is,
        if :obj:`.sellable` is a |product|, it's stock will be
        increased on *branch*.
        """
        storable = self.sellable.product_storable
        if storable:
            storable.increase_stock(self.quantity,
                                    branch,
                                    StockTransactionHistory.TYPE_RETURNED_SALE,
                                    self.id,
                                    batch=self.batch)
        if self.sale_item:
            self.sale_item.quantity_decreased -= self.quantity

    def undo(self):
        """Undo this item return.

        This is the oposite of the return, ie, the item will be removed back
        from stock and the sale item decreased quantity will be restored.
        """
        storable = self.sellable.product_storable
        if storable:
            storable.decrease_stock(
                self.quantity,
                self.returned_sale.branch,
                # FIXME: Create a new type
                StockTransactionHistory.TYPE_RETURNED_SALE,
                self.id,
                batch=self.batch)
        if self.sale_item:
            self.sale_item.quantity_decreased += self.quantity

    def get_component_quantity(self, parent):
        for component in parent.sellable.product.get_components():
            if self.sellable.product == component.component:
                return component.quantity
コード例 #22
0
class ReturnedSale(Domain):
    """Holds information about a returned |sale|.

    This can be:
      * *trade*, a |client| is returning the |sale| and buying something
        new with that credit. In that case the returning sale is :obj:`.sale` and the
        replacement |sale| is in :obj:`.new_sale`.
      * *return sale* or *devolution*, a |client| is returning the |sale|
        without making a new |sale|.

    Normally the old sale which is returned is :obj:`.sale`, however it
    might be ``None`` in some situations for example, if the |sale| was done
    at a different |branch| that hasn't been synchronized or is using another
    system.
    """

    __storm_table__ = 'returned_sale'

    #: This returned sale was received on another branch, but is not yet
    #: confirmed. A product goes back to stock only after confirmation
    STATUS_PENDING = u'pending'

    #: This return was confirmed, meaning the product stock was increased.
    STATUS_CONFIRMED = u'confirmed'

    #: This returned sale was canceled, ie, The product stock is decreased back
    #: and the original sale still have the products.
    STATUS_CANCELLED = 'cancelled'

    statuses = collections.OrderedDict([
        (STATUS_PENDING, _(u'Pending')),
        (STATUS_CONFIRMED, _(u'Confirmed')),
        (STATUS_CANCELLED, _(u'Cancelled')),
    ])

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

    #: Status of the returned sale
    status = EnumCol(default=STATUS_PENDING)

    #: the date this return was done
    return_date = DateTimeCol(default_factory=localnow)

    #: the date that the |returned sale| with the status pending was received
    confirm_date = DateTimeCol(default=None)

    # When this returned sale was undone
    undo_date = DateTimeCol(default=None)

    # FIXME: Duplicated from Invoice. Remove it
    #: the invoice number for this returning
    invoice_number = IntCol(default=None)

    #: the reason why this return was made
    reason = UnicodeCol(default=u'')

    #: The reason this returned sale was undone
    undo_reason = UnicodeCol(default=u'')

    sale_id = IdCol(default=None)

    #: the |sale| we're returning
    sale = Reference(sale_id, 'Sale.id')

    new_sale_id = IdCol(default=None)

    #: if not ``None``, :obj:`.sale` was traded for this |sale|
    new_sale = Reference(new_sale_id, 'Sale.id')

    responsible_id = IdCol()

    #: the |loginuser| responsible for doing this return
    responsible = Reference(responsible_id, 'LoginUser.id')

    confirm_responsible_id = IdCol()

    #: the |loginuser| responsible for receiving the pending return
    confirm_responsible = Reference(confirm_responsible_id, 'LoginUser.id')

    undo_responsible_id = IdCol()
    #: the |loginuser| responsible for undoing this returned sale.
    undo_responsible = Reference(undo_responsible_id, 'LoginUser.id')

    branch_id = IdCol()

    #: the |branch| in which this return happened
    branch = Reference(branch_id, 'Branch.id')

    #: a list of all items returned in this return
    returned_items = ReferenceSet('id', 'ReturnedSaleItem.returned_sale_id')

    #: |payments| generated by this returned sale
    payments = None

    #: |transporter| used in returned sale
    transporter = None

    invoice_id = IdCol()

    #: The |invoice| generated by the returned sale
    invoice = Reference(invoice_id, 'Invoice.id')

    def __init__(self, store=None, **kwargs):
        kwargs['invoice'] = Invoice(store=store, invoice_type=Invoice.TYPE_IN)
        super(ReturnedSale, self).__init__(store=store, **kwargs)

    @property
    def group(self):
        """|paymentgroup| for this return sale.

        Can return:
          * For a *trade*, use the |paymentgroup| from
            the replacement |sale|.
          * For a *devolution*, use the |paymentgroup| from
            the returned |sale|.
        """
        if self.new_sale:
            return self.new_sale.group
        if self.sale:
            return self.sale.group
        return None

    @property
    def client(self):
        """The |client| of this return

        Note that this is the same as :obj:`.sale.client`
        """
        return self.sale and self.sale.client

    @property
    def sale_total(self):
        """The current total amount of the |sale|.

        This is calculated by getting the
        :attr:`total amount <stoqlib.domain.sale.Sale.total_amount>` of the
        returned sale and subtracting the sum of :obj:`.returned_total` of
        all existing returns for the same sale.
        """
        if not self.sale:
            return currency(0)

        # TODO: Filter by status
        returned = self.store.find(ReturnedSale, sale=self.sale)
        # This will sum the total already returned for this sale,
        # excluiding *self* within the same store
        returned_total = sum([
            returned_sale.returned_total for returned_sale in returned
            if returned_sale != self
        ])

        return currency(self.sale.total_amount - returned_total)

    @property
    def paid_total(self):
        """The total paid for this sale

        Note that this is the same as
        :meth:`stoqlib.domain.sale.Sale.get_total_paid`
        """
        if not self.sale:
            return currency(0)

        return self.sale.get_total_paid()

    @property
    def returned_total(self):
        """The total being returned on this return

        This is done by summing the :attr:`ReturnedSaleItem.total` of
        all of this :obj:`returned items <.returned_items>`
        """
        return currency(sum([item.total for item in self.returned_items]))

    @property
    def total_amount(self):
        """The total amount for this return

        See :meth:`.return_` for details of how this is used.
        """
        return currency(self.sale_total - self.paid_total -
                        self.returned_total)

    @property
    def total_amount_abs(self):
        """The absolute total amount for this return

        This is the same as abs(:attr:`.total_amount`). Useful for
        displaying it on a gui, just changing it's label to show if
        it's 'overpaid' or 'missing'.
        """
        return currency(abs(self.total_amount))

    #
    #  IContainer implementation
    #

    def add_item(self, returned_item):
        assert not returned_item.returned_sale
        returned_item.returned_sale = self

    def get_items(self):
        return self.returned_items

    def remove_item(self, item):
        item.returned_sale = None
        self.store.maybe_remove(item)

    #
    # IInvoice implementation
    #

    @property
    def comments(self):
        return self.reason

    @property
    def discount_value(self):
        return currency(0)

    @property
    def invoice_subtotal(self):
        return self.returned_total

    @property
    def invoice_total(self):
        return self.returned_total

    @property
    def recipient(self):
        if self.sale.client:
            return self.sale.client.person
        return None

    @property
    def operation_nature(self):
        # TODO: Save the operation nature in new returned_sale table field.
        return _(u"Sale Return")

    #
    #  Public API
    #

    @classmethod
    def get_pending_returned_sales(cls, store, branch):
        """Returns a list of pending |returned_sale|

        :param store: a store
        :param branch: the |branch| where the sale was made
        """
        from stoqlib.domain.sale import Sale

        tables = [cls, Join(Sale, cls.sale_id == Sale.id)]
        # We want the returned_sale which sale was made on the branch
        # So we are comparing Sale.branch with |branch| to build the query
        return store.using(*tables).find(
            cls, And(cls.status == cls.STATUS_PENDING, Sale.branch == branch))

    def is_pending(self):
        return self.status == ReturnedSale.STATUS_PENDING

    def is_undone(self):
        return self.status == ReturnedSale.STATUS_CANCELLED

    def can_undo(self):
        return self.status == ReturnedSale.STATUS_CONFIRMED

    def return_(self, method_name=u'money', login_user=None):
        """Do the return of this returned sale.

        :param unicode method_name: The name of the payment method that will be
          used to create this payment.

        If :attr:`.total_amount` is:
          * > 0, the client is returning more than it paid, we will create
            a |payment| with that value so the |client| can be reversed.
          * == 0, the |client| is returning the same amount that needs to be paid,
            so existing payments will be cancelled and the |client| doesn't
            owe anything to us.
          * < 0, than the payments need to be readjusted before calling this.

        .. seealso: :meth:`stoqlib.domain.sale.Sale.return_` as that will be
           called after that payment logic is done.
        """
        assert self.sale and self.sale.can_return()
        self._clean_not_used_items()

        payment = None
        if self.total_amount == 0:
            # The client does not owe anything to us
            self.group.cancel()
        elif self.total_amount < 0:
            # The user has paid more than it's returning
            for payment in self.group.get_pending_payments():
                if payment.is_inpayment():
                    # We are returning money to client, that means he doesn't owe
                    # us anything, we do now. Cancel pending payments
                    payment.cancel()

            method = PaymentMethod.get_by_name(self.store, method_name)
            description = _(u'%s returned for sale %s') % (
                method.description, self.sale.identifier)
            payment = method.create_payment(Payment.TYPE_OUT,
                                            payment_group=self.group,
                                            branch=self.branch,
                                            value=self.total_amount_abs,
                                            description=description)
            payment.set_pending()
            if method_name == u'credit':
                payment.pay()

        # FIXME: For now, we are not reverting the comission as there is a
        # lot of things to consider. See bug 5215 for information about it.
        self._revert_fiscal_entry()

        self.sale.return_(self)

        # Save invoice number, operation_nature and branch in Invoice table.
        self.invoice.invoice_number = self.invoice_number
        self.invoice.operation_nature = self.operation_nature
        self.invoice.branch = self.branch

        if self.sale.branch == self.branch:
            self.confirm(login_user)

    def trade(self):
        """Do a trade for this return

        Almost the same as :meth:`.return_`, but unlike it, this won't
        generate reversed payments to the client. Instead, it'll
        generate an inpayment using :obj:`.returned_total` value,
        so it can be used as an "already paid quantity" on :obj:`.new_sale`.
        """
        assert self.new_sale
        if self.sale:
            assert self.sale.can_return()
        self._clean_not_used_items()

        store = self.store
        group = self.group
        method = PaymentMethod.get_by_name(store, u'trade')
        description = _(u'Traded items for sale %s') % (
            self.new_sale.identifier, )
        value = self.returned_total

        self._return_items()

        value_as_discount = sysparam.get_bool('USE_TRADE_AS_DISCOUNT')
        if value_as_discount:
            self.new_sale.discount_value = self.returned_total
        else:
            payment = method.create_payment(Payment.TYPE_IN,
                                            group,
                                            self.branch,
                                            value,
                                            description=description)
            payment.set_pending()
            payment.pay()
            self._revert_fiscal_entry()

        if self.sale:
            self.sale.return_(self)

    def remove(self):
        """Remove this return and it's items from the database"""
        # XXX: Why do we remove this object from the database
        # We must remove children_items before we remove its parent_item
        for item in self.returned_items.find(
                Eq(ReturnedSaleItem.parent_item_id, None)):
            [
                self.remove_item(child)
                for child in getattr(item, 'children_items')
            ]
            self.remove_item(item)
        self.store.remove(self)

    def confirm(self, login_user):
        """Receive the returned_sale_items from a pending |returned_sale|

        :param user: the |login_user| that received the pending returned sale
        """
        assert self.status == self.STATUS_PENDING
        self._return_items()
        self.status = self.STATUS_CONFIRMED
        self.confirm_responsible = login_user
        self.confirm_date = localnow()

    def undo(self, reason):
        """Undo this returned sale.

        This includes removing the returned items from stock again (updating the
        quantity decreased on the sale).

        :param reason: The reason for this operation.
        """
        assert self.can_undo()
        for item in self.get_items():
            item.undo()

        # We now need to create a new in payment for the total amount of this
        # returned sale.
        method_name = self._guess_payment_method()
        method = PaymentMethod.get_by_name(self.store, method_name)
        description = _(u'%s return undone for sale %s') % (
            method.description, self.sale.identifier)
        payment = method.create_payment(Payment.TYPE_IN,
                                        payment_group=self.group,
                                        branch=self.branch,
                                        value=self.returned_total,
                                        description=description)
        payment.set_pending()
        payment.pay()

        self.status = self.STATUS_CANCELLED
        self.cancel_date = localnow()
        self.undo_reason = reason

        # if the sale status is returned, we must reset it to confirmed (only
        # confirmed sales can be returned)
        if self.sale.is_returned():
            self.sale.set_not_returned()

    #
    #  Private
    #

    def _guess_payment_method(self):
        """Guesses the payment method used in this returned sale.
        """
        value = self.returned_total
        # Now look for the out payment, ie, the payment that we possibly created
        # for the returned value.
        payments = list(
            self.sale.payments.find(payment_type=Payment.TYPE_OUT,
                                    value=value))
        if len(payments) == 1:
            # There is only one payment that matches our criteria, we can trust it
            # is the one we are looking for.
            method = payments[0].method.method_name
        elif len(payments) == 0:
            # This means that the returned sale didn't endup creating any return
            # payment for the client. Let's just create a money payment then
            method = u'money'
        else:
            # This means that we found more than one return payment for this
            # value. This probably means that the user has returned multiple
            # items in different returns.
            methods = set(payment.method.method_name for payment in payments)
            if len(methods) == 1:
                # All returns were using the same method. Lets use that one them
                method = methods.pop()
            else:
                # The previous returns used different methods, let's pick money
                method = u'money'

        return method

    def _return_items(self):
        # We must have at least one item to return
        assert self.returned_items.count()

        # FIXME
        branch = get_current_branch(self.store)
        for item in self.returned_items:
            item.return_(branch)

    def _get_returned_percentage(self):
        return Decimal(self.returned_total / self.sale.total_amount)

    def _clean_not_used_items(self):
        store = self.store
        for item in self.returned_items:
            if not item.quantity:
                # Removed items not marked for return
                item.delete(item.id, store=store)

    def _revert_fiscal_entry(self):
        entry = self.store.find(FiscalBookEntry,
                                payment_group=self.group,
                                is_reversal=False).one()
        if not entry:
            return

        # FIXME: Instead of doing a partial reversion of fiscal entries,
        # we should be reverting the exact tax for each returned item.
        returned_percentage = self._get_returned_percentage()
        entry.reverse_entry(self.invoice_number,
                            icms_value=entry.icms_value * returned_percentage,
                            iss_value=entry.iss_value * returned_percentage,
                            ipi_value=entry.ipi_value * returned_percentage)
コード例 #23
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 = collections.OrderedDict([
        (STATUS_AVAILABLE, _(u'Available')),
        (STATUS_CLOSED, _(u'Closed')),
    ])

    #: a code used internally by the shop to reference this sellable.
    #: It is usually not printed and displayed to |clients|, barcode is for that.
    #: It may be used as an shorter alternative to the barcode.
    code = UnicodeCol(default=u'', validator=_validate_code)

    #: barcode, mostly for products, usually printed and attached to the
    #: package.
    barcode = UnicodeCol(default=u'', validator=_validate_barcode)

    #: status the sellable is in
    status = EnumCol(allow_none=False, default=STATUS_AVAILABLE)

    #: cost of the sellable, this is not tied to a specific |supplier|,
    #: which may have a different cost. This can also be the production cost of
    #: manufactured item by the company.
    cost = PriceCol(default=0)

    #: price of sellable, how much the |client| paid.
    base_price = PriceCol(default=0)

    #: the last time the cost was updated
    cost_last_updated = DateTimeCol(default_factory=localnow)

    #: the last time the price was updated
    price_last_updated = DateTimeCol(default_factory=localnow)

    #: full description of sellable
    description = UnicodeCol(default=u'')

    #: maximum discount allowed
    max_discount = PercentCol(default=0)

    #: commission to pay after selling this sellable
    commission = PercentCol(default=0)

    #: A sort order to override default alphabetic order in lists.
    sort_order = IntCol()

    #: If this is a favorite sellable
    favorite = BoolCol()

    #: Some keywords for this sellable.
    keywords = UnicodeCol()

    #: notes for the sellable
    notes = UnicodeCol(default=u'')

    unit_id = IdCol(default=None)

    #: the |sellableunit|, quantities of this sellable are in this unit.
    unit = Reference(unit_id, 'SellableUnit.id')

    category_id = IdCol(default=None)

    #: a reference to category table
    category = Reference(category_id, 'SellableCategory.id')

    tax_constant_id = IdCol(default=None)

    #: the |sellabletaxconstant|, this controls how this sellable is taxed
    tax_constant = Reference(tax_constant_id, 'SellableTaxConstant.id')

    #: the |product| for this sellable or ``None``
    product = Reference('id', 'Product.id', on_remote=True)

    #: the |service| for this sellable or ``None``
    service = Reference('id', 'Service.id', on_remote=True)

    #: the |storable| for this |product|'s sellable
    product_storable = Reference('id', 'Storable.id', on_remote=True)

    default_sale_cfop_id = IdCol(default=None)

    #: the default |cfop| that will be used when selling this sellable
    default_sale_cfop = Reference(default_sale_cfop_id, 'CfopData.id')

    #: A special price used when we have a "on sale" state, this
    #: can be used for promotions
    on_sale_price = PriceCol(default=0)

    #: When the promotional/special price starts to apply
    on_sale_start_date = DateTimeCol(default=None)

    #: When the promotional/special price ends
    on_sale_end_date = DateTimeCol(default=None)

    #: This sellable's images
    images = ReferenceSet('id', 'Image.sellable_id')

    #: specifies whether the product requires kitchen production
    requires_kitchen_production = BoolCol(default=False)

    def __init__(self,
                 store=None,
                 category=None,
                 cost=None,
                 commission=None,
                 description=None,
                 price=None):
        """Creates a new sellable
        :param store: a store
        :param category: category of this sellable
        :param cost: the cost, defaults to 0
        :param commission: commission for this sellable
        :param description: readable description of the sellable
        :param price: the price, defaults to 0
        """

        Domain.__init__(self, store=store)

        if category:
            if commission is None:
                commission = category.get_commission()
            if price is None and cost is not None:
                markup = category.get_markup()
                price = self._get_price_by_markup(markup, cost=cost)

        self.category = category
        self.commission = commission or currency(0)
        self.cost = cost or currency(0)
        self.description = description
        self.price = price or currency(0)

    #
    # Helper methods
    #

    def _get_price_by_markup(self, markup, cost=None):
        if cost is None:
            cost = self.cost
        return currency(quantize(cost + (cost * (markup / currency(100)))))

    #
    # Properties
    #

    @property
    def status_str(self):
        """The sellable status as a string"""
        return self.statuses[self.status]

    @property
    def unit_description(self):
        """Returns the description of the |sellableunit| of this sellable

        :returns: the unit description or an empty string if no
          |sellableunit| was set.
        :rtype: unicode
        """
        return self.unit and self.unit.description or u""

    @property
    def image(self):
        """This sellable's main image."""
        # FIXME: Should we use .first() here? What will happen if there are
        # more than one image with "is_main" flag set to True? There's no way
        # to prevent that in the database
        return self.images.find(is_main=True).one()

    @property
    def markup(self):
        """Markup, the opposite of discount, a value added
        on top of the sale. It's calculated as::
          ((cost/price)-1)*100
        """
        if self.cost == 0:
            return Decimal(0)
        return ((self.price / self.cost) - 1) * 100

    @markup.setter
    def markup(self, markup):
        self.price = self._get_price_by_markup(markup)

    @property
    def price(self):
        if self.is_on_sale():
            return self.on_sale_price
        else:
            category = sysparam.get_object(self.store, 'DEFAULT_TABLE_PRICE')
            if category:
                info = self.get_category_price_info(category)
                if info:
                    return info.price
            return self.base_price

    @price.setter
    def price(self, price):
        if price < 0:
            # Just a precaution for gui validation fails.
            price = 0

        if self.is_on_sale():
            self.on_sale_price = price
        else:
            self.base_price = price

    #
    #  Accessors
    #

    def is_available(self):
        """Whether the sellable is available and can be sold.

        :returns: ``True`` if the item can be sold, ``False`` otherwise.
        """
        # FIXME: Perhaps this should be done elsewhere. Johan 2008-09-26
        if sysparam.compare_object('DELIVERY_SERVICE', self.service):
            return True
        return self.status == self.STATUS_AVAILABLE

    def set_available(self):
        """Mark the sellable as available

        Being available means that it can be ordered or sold.

        :raises: :exc:`ValueError`: if the sellable is already available
        """
        if self.is_available():
            raise ValueError('This sellable is already available')
        self.status = self.STATUS_AVAILABLE

    def is_closed(self):
        """Whether the sellable is closed or not.

        :returns: ``True`` if closed, ``False`` otherwise.
        """
        return self.status == Sellable.STATUS_CLOSED

    def close(self):
        """Mark the sellable as closed.

        After the sellable is closed, this will call the close method of the
        service or product related to this sellable.

        :raises: :exc:`ValueError`: if the sellable is already closed
        """
        if self.is_closed():
            raise ValueError('This sellable is already closed')

        assert self.can_close()
        self.status = Sellable.STATUS_CLOSED

        obj = self.service or self.product
        obj.close()

    def can_remove(self):
        """Whether we can delete this sellable from the database.

        ``False`` if the product/service was used in some cases below::

          - Sold or received
          - The |product| is in a |purchase|
        """
        if self.product and not self.product.can_remove():
            return False

        if self.service and not self.service.can_remove():
            return False

        return super(Sellable, self).can_remove(
            skip=[('product', 'id'), ('service', 'id'), (
                'image',
                'sellable_id'), ('client_category_price', 'sellable_id')])

    def can_close(self):
        """Whether we can close this sellable.

        :returns: ``True`` if the product has no stock left or the service
            is not required by the system (i.e. Delivery service is
            required). ``False`` otherwise.
        """
        obj = self.service or self.product
        return obj.can_close()

    def get_commission(self):
        return self.commission

    def get_suggested_markup(self):
        """Returns the suggested markup for the sellable

        :returns: suggested markup
        :rtype: decimal
        """
        return self.category and self.category.get_markup()

    def get_category_description(self):
        """Returns the description of this sellables category
        If it's unset, return the constant from the category, if any

        :returns: sellable category description or an empty string if no
          |sellablecategory| was set.
        :rtype: unicode
        """
        category = self.category
        return category and category.description or u""

    def get_tax_constant(self):
        """Returns the |sellabletaxconstant| for this sellable.
        If it's unset, return the constant from the category, if any

        :returns: the |sellabletaxconstant| or ``None`` if unset
        """
        if self.tax_constant:
            return self.tax_constant

        if self.category:
            return self.category.get_tax_constant()

    def get_category_prices(self):
        """Returns all client category prices associated with this sellable.

        :returns: the client category prices
        """
        return self.store.find(ClientCategoryPrice, sellable=self)

    def get_category_price_info(self, category):
        """Returns the :class:`ClientCategoryPrice` information for the given
        :class:`ClientCategory` and this |sellable|.

        :returns: the :class:`ClientCategoryPrice` or ``None``
        """
        info = self.store.find(ClientCategoryPrice,
                               sellable=self,
                               category=category).one()
        return info

    def get_price_for_category(self, category):
        """Given the |clientcategory|, returns the price for that category
        or the default sellable price.

        :param category: a |clientcategory|
        :returns: The value that should be used as a price for this sellable.
        """
        info = self.get_category_price_info(category)
        if info:
            return info.price
        return self.price

    def get_maximum_discount(self, category=None, user=None):
        user_discount = user.profile.max_discount if user else 0
        if category is not None:
            info = self.get_category_price_info(category) or self
        else:
            info = self

        return Decimal(max(user_discount, info.max_discount))

    def get_requires_kitchen_production(self, branch):
        """Check if a sellable requires kitchen production

        :param branch: branch for checking if there is a sellable_branch_override
        :returns: Whether the sellable requires kitchen production for a given branch
        """
        # Check for overrides before checking the actual sellable
        sellable_override = SellableBranchOverride.find_by_sellable(
            sellable=self, branch=branch)

        if sellable_override and sellable_override.requires_kitchen_production is not None:
            return sellable_override.requires_kitchen_production
        return self.requires_kitchen_production

    def check_code_exists(self, code):
        """Check if there is another sellable with the same code.

        :returns: ``True`` if we already have a sellable with the given code
          ``False`` otherwise.
        """
        return self.check_unique_value_exists(Sellable.code, code)

    def check_barcode_exists(self, barcode):
        """Check if there is another sellable with the same barcode.

        :returns: ``True`` if we already have a sellable with the given barcode
          ``False`` otherwise.
        """
        return self.check_unique_value_exists(Sellable.barcode, barcode)

    def check_taxes_validity(self, branch):
        """Check if icms taxes are valid.

        This check is done because some icms taxes (such as CSOSN 101) have
        a 'valid until' field on it. If these taxes has expired, we cannot sell
        the sellable.
        Check this method using assert inside a try clause.

        :raises: :exc:`TaxError` if there are any issues with the sellable taxes.
        """
        icms_template = self.product and self.product.get_icms_template(branch)
        SellableCheckTaxesEvent.emit(self, branch)
        if not icms_template:
            return
        elif not icms_template.p_cred_sn:
            return
        elif not icms_template.is_p_cred_sn_valid():
            # Translators: ICMS tax rate credit = Alíquota de crédito do ICMS
            raise TaxError(
                _("You cannot sell this item before updating "
                  "the 'ICMS tax rate credit' field on '%s' "
                  "Tax Class.\n"
                  "If you don't know what this means, contact "
                  "the system administrator.") %
                icms_template.product_tax_template.name)

    def is_on_sale(self):
        """Check if the price is currently on sale.

        :return: ``True`` if it is on sale, ``False`` otherwise
        """
        if not self.on_sale_price:
            return False

        return is_date_in_interval(localnow(), self.on_sale_start_date,
                                   self.on_sale_end_date)

    def is_valid_quantity(self, new_quantity):
        """Whether the new quantity is valid for this sellable or not.

        If the new quantity is fractioned, check on this sellable unit if it
        allows fractioned quantities. If not, this new quantity cannot be used.

        Note that, if the sellable lacks a unit, we will not allow
        fractions either.

        :returns: ``True`` if new quantity is Ok, ``False`` otherwise.
        """
        if self.unit and not self.unit.allow_fraction:
            return not bool(new_quantity % 1)

        return True

    def is_valid_price(self,
                       newprice,
                       category=None,
                       user=None,
                       extra_discount=None):
        """Checks if *newprice* is valid for this sellable

        Returns a dict indicating whether the new price is a valid price as
        allowed by the discount by the user, by the category or by the sellable
        maximum discount

        :param newprice: The new price that we are trying to sell this
            sellable for
        :param category: Optionally define a |clientcategory| that we will get
            the price info from
        :param user: The user role may allow a different discount percentage.
        :param extra_discount: some extra discount for the sellable
            to be considered for the min_price
        :returns: A dict with the following keys:
            * is_valid: ``True`` if the price is valid, else ``False``
            * min_price: The minimum price for this sellable.
            * max_discount: The maximum discount for this sellable.
        """
        if category is not None:
            info = self.get_category_price_info(category) or self
        else:
            info = self

        max_discount = self.get_maximum_discount(category=category, user=user)
        min_price = info.price * (1 - max_discount / 100)

        if extra_discount is not None:
            # The extra discount can be greater than the min_price, and
            # a negative min_price doesn't make sense
            min_price = max(currency(0), min_price - extra_discount)

        return {
            'is_valid': newprice >= min_price,
            'min_price': min_price,
            'max_discount': max_discount,
        }

    def copy_sellable(self, target=None):
        """This method copies self to another sellable

        If the |sellable| target is None, a new sellable is created.

        :param target: The |sellable| target for the copy
        returns: a |sellable| identical to self
        """
        if target is None:
            target = Sellable(store=self.store)

        props = [
            'base_price', 'category_id', 'cost', 'max_discount', 'commission',
            'notes', 'unit_id', 'tax_constant_id', 'default_sale_cfop_id',
            'on_sale_price', 'on_sale_start_date', 'on_sale_end_date'
        ]

        for prop in props:
            value = getattr(self, prop)
            setattr(target, prop, value)

        return target

    #
    # IDescribable implementation
    #

    def get_description(self, full_description=False):
        desc = self.description
        if full_description and self.get_category_description():
            desc = u"[%s] %s" % (self.get_category_description(), desc)

        return desc

    #
    # Domain hooks
    #

    def on_update(self):
        obj = self.product or self.service
        obj.on_update()

    def on_object_changed(self, attr, old_value, value):
        if attr == 'cost':
            self.cost_last_updated = localnow()
            if self.product:
                self.product.update_product_cost(value)
        elif attr == 'base_price':
            self.price_last_updated = localnow()

    #
    # Classmethods
    #

    def remove(self):
        """
        Remove this sellable. This will also remove the |product| or
        |sellable| and |categoryprice|
        """
        assert self.can_remove()

        # Remove category price before delete the sellable.
        category_prices = self.get_category_prices()
        for category_price in category_prices:
            category_price.remove()

        for image in self.images:
            self.store.remove(image)

        if self.product:
            self.product.remove()
        elif self.service:
            self.service.remove()

        self.store.remove(self)

    @classmethod
    def get_available_sellables_query(cls, store):
        """Get the sellables that are available and can be sold.

        For instance, this will filter out the internal sellable used
        by a |delivery|.

        This is similar to `.get_available_sellables`, but it returns
        a query instead of the actual results.

        :param store: a store
        :returns: a query expression
        """

        delivery = sysparam.get_object(store, 'DELIVERY_SERVICE')
        return And(cls.id != delivery.sellable.id,
                   cls.status == cls.STATUS_AVAILABLE)

    @classmethod
    def get_available_sellables(cls, store):
        """Get the sellables that are available and can be sold.

        For instance, this will filter out the internal sellable used
        by a |delivery|.

        :param store: a store
        :returns: a resultset with the available sellables
        """
        query = cls.get_available_sellables_query(store)
        return store.find(cls, query)

    @classmethod
    def get_unblocked_sellables_query(cls,
                                      store,
                                      storable=False,
                                      supplier=None,
                                      consigned=False):
        """Helper method for get_unblocked_sellables

        When supplier is not ```None``, you should use this query only with
        Viewables that join with supplier, like ProductFullStockSupplierView.

        :param store: a store
        :param storable: if ``True``, we should filter only the sellables that
          are also a |storable|.
        :param supplier: |supplier| to filter on or ``None``
        :param consigned: if the sellables are consigned

        :returns: a query expression
        """
        from stoqlib.domain.product import Product, ProductSupplierInfo
        query = And(cls.get_available_sellables_query(store),
                    cls.id == Product.id, Product.consignment == consigned)
        if storable:
            from stoqlib.domain.product import Storable
            query = And(query, Sellable.id == Product.id,
                        Storable.id == Product.id)

        if supplier:
            query = And(query, Sellable.id == Product.id,
                        Product.id == ProductSupplierInfo.product_id,
                        ProductSupplierInfo.supplier_id == supplier.id)

        return query

    @classmethod
    def get_unblocked_sellables(cls,
                                store,
                                storable=False,
                                supplier=None,
                                consigned=False):
        """
        Returns unblocked sellable objects, which means the
        available sellables plus the sold ones.

        :param store: a store
        :param storable: if `True`, only return sellables that also are
          |storable|
        :param supplier: a |supplier| or ``None``, if set limit the returned
          object to this |supplier|

        :rtype: queryset of sellables
        """
        query = cls.get_unblocked_sellables_query(store, storable, supplier,
                                                  consigned)
        return store.find(cls, query)

    @classmethod
    def get_unblocked_by_categories_query(cls,
                                          store,
                                          categories,
                                          include_uncategorized=True):
        """Returns the available sellables by a list of categories.

        :param store: a store
        :param categories: a list of SellableCategory instances
        :param include_uncategorized: whether or not include the sellables
            without a category

        :rtype: generator of sellables
        """
        queries = []
        if len(categories):
            queries.append(In(Sellable.category_id,
                              [c.id for c in categories]))
        if include_uncategorized:
            queries.append(Eq(Sellable.category_id, None))

        query = cls.get_unblocked_sellables_query(store)
        return And(query, Or(*queries))
コード例 #24
0
class Product(Domain):
    """A Product is a thing that can be:

      * ordered (via |purchase|)
      * stored (via |storable|)
      * sold (via |sellable|)
      * manufactured (via |production|)

    A manufactured product can have several |components|, which are parts
    that when combined create the product.

    A consigned product is borrowed from a |supplier|. You can also loan out
    your own products via |loan|.

    If the product does not use stock managment, it will be possible to sell
    items, even if it was never purchased.

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

    __storm_table__ = 'product'

    sellable_id = IdCol()

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

    suppliers = ReferenceSet('id', 'ProductSupplierInfo.product_id')

    #: if this product is loaned from the |supplier|
    consignment = BoolCol(default=False)

    #: ``True`` if this product has |components|.
    #: This is stored on Product to avoid a join to find out if there is any
    #: components or not.
    is_composed = BoolCol(default=False)

    #: If this product will use stock management.
    #: When this is set to ``True``, a corresponding |storable| should be created.
    #: For ``False`` a storable will not be created and the quantity currently
    #: in stock will not be known, e.g. |purchases| will not increase the stock
    # quantity, and the operations that decrease stock (like a |sale| or a
    # |loan|, will be allowed at any time.
    manage_stock = BoolCol(default=True)

    #: physical location of this product, like a drawer or shelf number
    location = UnicodeCol(default=u'')

    manufacturer_id = IdCol(default=None)

    #: name of the manufacturer for this product, eg "General Motors"
    manufacturer = Reference(manufacturer_id, 'ProductManufacturer.id')

    #: name of the brand, eg "Chevrolet" or "Opel"
    brand = UnicodeCol(default=u'')

    #: name of the family, eg "Cobalt" or "Astra"
    family = UnicodeCol(default=u'')

    #: name of the model, eg "2.2 L Ecotec L61 I4" or "2.0 8V/ CD 2.0 Hatchback 5p Aut"
    model = UnicodeCol(default=u'')

    #: a number representing this part
    part_number = UnicodeCol(default=u'')

    #: physical width of this product, unit not enforced
    width = DecimalCol(default=0)

    #: physical height of this product, unit not enforced
    height = DecimalCol(default=0)

    #: depth in this product, unit not enforced
    depth = DecimalCol(default=0)

    #: physical weight of this product, unit not enforced
    weight = DecimalCol(default=0)

    #: The time in days it takes to manufacter this product
    production_time = IntCol(default=1)

    #: Brazil specific: NFE: nomenclature comon do mercuosol
    ncm = UnicodeCol(default=None)

    #: NFE: see ncm
    ex_tipi = UnicodeCol(default=None)

    #: NFE: see ncm
    genero = UnicodeCol(default=None)

    icms_template_id = IdCol(default=None)

    icms_template = Reference(icms_template_id, 'ProductIcmsTemplate.id')

    ipi_template_id = IdCol(default=None)

    ipi_template = Reference(ipi_template_id, 'ProductIpiTemplate.id')

    #: Used for composed products only
    quality_tests = ReferenceSet('id', 'ProductQualityTest.product_id')

    #: list of |suppliers| that sells this product
    suppliers = ReferenceSet('id', 'ProductSupplierInfo.product_id')

    @property
    def description(self):
        return self.sellable.description

    @property
    def storable(self):
        return self.store.find(Storable, product=self).one()

    #
    #  Public API
    #

    def has_quality_tests(self):
        return not self.quality_tests.find().is_empty()

    def remove(self):
        """Deletes this product from the database.
        """
        storable = self.storable
        if storable:
            self.store.remove(storable)
        for i in self.get_suppliers_info():
            self.store.remove(i)
        for i in self.get_components():
            self.store.remove(i)

        self.store.remove(self)

    def can_remove(self):
        """Whether we can delete this product and it's |sellable| from the
        database.

        ``False`` if the product was sold, received or used in a
        production. ``True`` otherwise.
        """
        from stoqlib.domain.production import ProductionItem
        if self.get_history().count():
            return False
        storable = self.storable
        if storable and storable.get_stock_items().count():
            return False
        # Return False if the product is component of other.
        elif self.store.find(ProductComponent,
                             component=self).count():
            return False
        # Return False if the component(product) is used in a production.
        elif self.store.find(ProductionItem,
                             product=self).count():
            return False
        return True

    def can_close(self):
        """Checks if this product can be closed

        Called by |sellable| to check if it can be closed or not.
        A product can be closed if it doesn't have any stock left
        """
        if self.manage_stock:
            return self.storable.get_total_balance() == 0
        return True

    def get_manufacture_time(self, quantity, branch):
        """Returns the estimated time in days to manufacture a product

        If the |components| don't have enough stock, the estimated time to
        obtain missing |components| will also be considered (using the max
        lead time from the |suppliers|)

        :param quantity:
        :param branch: the |branch|
        """
        assert self.is_composed

        # Components maximum lead time
        comp_max_time = 0
        for i in self.get_components():
            storable = i.component.storable
            needed = quantity * i.quantity
            stock = storable.get_balance_for_branch(branch)
            # We have enought of this component items to produce.
            if stock >= needed:
                continue
            comp_max_time = max(comp_max_time,
                                i.component.get_max_lead_time(needed, branch))
        return self.production_time + comp_max_time

    def get_max_lead_time(self, quantity, branch):
        """Returns the longest lead time for this product.

        If this is a composed product, the lead time will be the time to
        manufacture the product plus the time to obtain all the missing
        components

        If its a regular product this will be the longest lead time for a
        supplier to deliver the product (considering the worst case).

        quantity and |branch| are used only when the product is composed
        """
        if self.is_composed:
            return self.get_manufacture_time(quantity, branch)
        else:
            return self.suppliers.find().max(ProductSupplierInfo.lead_time) or 0

    def get_history(self):
        """Returns the list of :class:`ProductHistory` for this product.
        """
        return self.store.find(ProductHistory, sellable=self.sellable)

    def get_main_supplier_name(self):
        supplier_info = self.get_main_supplier_info()
        return supplier_info.get_name()

    def get_main_supplier_info(self):
        """Gets a list of main suppliers for a Product, the main supplier
        is the most recently selected supplier.

        :returns: main supplier info
        :rtype: ProductSupplierInfo or None if a product lacks
           a main suppliers
        """
        store = self.store
        return store.find(ProductSupplierInfo,
                          product=self, is_main_supplier=True).one()

    def get_suppliers_info(self):
        """Returns a list of suppliers for this product

        :returns: a list of suppliers
        :rtype: list of ProductSupplierInfo
        """
        return self.store.find(ProductSupplierInfo,
                               product=self)

    def get_components(self):
        """Returns the products which are our |components|.

        :returns: a sequence of |components|
        """
        return self.store.find(ProductComponent, product=self)

    def has_components(self):
        """Returns if this product has a |component| or not.

        :returns: ``True`` if this product has |components|, ``False`` otherwise.
        """
        return self.get_components().count() > 0

    def get_production_cost(self):
        """ Return the production cost of one unit of the product.

        :returns: the production cost
        """
        return self.sellable.cost

    def is_supplied_by(self, supplier):
        """If this product is supplied by the given |supplier|, returns the
        object with the supplier information. Returns ``None`` otherwise
        """
        store = self.store
        return store.find(ProductSupplierInfo, product=self,
                          supplier=supplier).one() is not None

    def is_composed_by(self, product):
        """Returns if we are composed by a given product or not.

        :param product: a possible component of this product
        :returns: ``True`` if the given product is one of our component or a
          component of our components, otherwise ``False``.
        """
        for component in self.get_components():
            if product is component.component:
                return True
            if component.component.is_composed_by(product):
                return True
        return False

    def is_being_produced(self):
        from stoqlib.domain.production import ProductionOrderProducingView
        return ProductionOrderProducingView.is_product_being_produced(self)

    #
    # Domain
    #

    def on_create(self):
        ProductCreateEvent.emit(self)

    def on_delete(self):
        ProductRemoveEvent.emit(self)

    def on_update(self):
        store = self.store
        emitted_store_list = getattr(self, '_emitted_store_list', set())

        # Since other classes can propagate this event (like Sellable),
        # emit the event only once for each store.
        if not store in emitted_store_list:
            ProductEditEvent.emit(self)
            emitted_store_list.add(store)

        self._emitted_store_list = emitted_store_list
コード例 #25
0
ファイル: payment.py プロジェクト: 5l1v3r1/stoq-1
class Payment(IdentifiableDomain):
    """Payment, a transfer of money between a |branch| and |client| or a
    |supplier|.

    Payments between:

    * a client and a branch are :obj:`.TYPE_IN`, has a |sale| associated.
    * branch and a supplier are :obj:`.TYPE_OUT`, has a |purchase| associated.

    Payments are sometimes referred to as *installments*.

    Sales and purchase orders can be accessed via the
    :obj:`payment group <.group>`

    +-------------------------+-------------------------+
    | **Status**              | **Can be set to**       |
    +-------------------------+-------------------------+
    | :obj:`STATUS_PREVIEW`   | :obj:`STATUS_PENDING`   |
    +-------------------------+-------------------------+
    | :obj:`STATUS_PENDING`   | :obj:`STATUS_PAID`,     |
    |                         | :obj:`STATUS_CANCELLED` |
    +-------------------------+-------------------------+
    | :obj:`STATUS_PAID`      | :obj:`STATUS_PENDING`,  |
    |                         | :obj:`STATUS_CANCELLED` |
    +-------------------------+-------------------------+
    | :obj:`STATUS_CANCELLED` | None                    |
    +-------------------------+-------------------------+

    .. graphviz::

       digraph status {
         STATUS_PREVIEW -> STATUS_PENDING;
         STATUS_PENDING -> STATUS_PAID;
         STATUS_PENDING -> STATUS_CANCELLED;
         STATUS_PAID -> STATUS_PENDING;
         STATUS_PAID -> STATUS_CANCELLED;
       }

    Simple sale workflow:

    * Creating a sale, status is set to :obj:`STATUS_PREVIEW`
    * Confirming the sale, status is set to :obj:`STATUS_PENDING`
    * Paying the installment, status is set to :obj:`STATUS_PAID`
    * Cancelling the payment, status is set to :obj:`STATUS_CANCELLED`

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

    __storm_table__ = 'payment'

    #: incoming to the company, accounts receivable, payment from
    #: a |client| to a |branch|
    TYPE_IN = u'in'

    #: outgoing from the company, accounts payable, a payment from
    #: |branch| to a |supplier|
    TYPE_OUT = u'out'

    #: payment group this payment belongs to hasn't been confirmed,
    # should normally be filtered when showing a payment list
    STATUS_PREVIEW = u'preview'

    #: payment group has been confirmed and the payment has not been received
    STATUS_PENDING = u'pending'

    #: the payment has been received
    STATUS_PAID = u'paid'

    # FIXME: Remove these two
    #: Unused.
    STATUS_REVIEWING = u'reviewing'

    #: Unused.
    STATUS_CONFIRMED = u'confirmed'

    #: payment was cancelled, for instance the payments of the group was changed, or
    #: the group was cancelled.
    STATUS_CANCELLED = u'cancelled'

    statuses = collections.OrderedDict([
        (STATUS_PREVIEW, _(u'Preview')),
        (STATUS_PENDING, _(u'To Pay')),
        (STATUS_PAID, _(u'Paid')),
        (STATUS_REVIEWING, _(u'Reviewing')),
        (STATUS_CONFIRMED, _(u'Confirmed')),
        (STATUS_CANCELLED, _(u'Cancelled')),
    ])

    #: type of payment :obj:`.TYPE_IN` or :obj:`.TYPE_OUT`
    payment_type = EnumCol(allow_none=False, default=TYPE_IN)

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

    #: status, see |payment| for more information.
    status = EnumCol(allow_none=False, default=STATUS_PREVIEW)

    #: description payment, usually something like "1/3 Money for Sale 1234"
    description = UnicodeCol(default=None)

    # FIXME: use TransactionTimestamp() instead to avoid server/client date
    #        inconsistencies

    #: when this payment was opened
    open_date = DateTimeCol(default_factory=localnow)

    #: when this payment is due
    due_date = DateTimeCol()

    #: when this payment was paid
    paid_date = DateTimeCol(default=None)

    #: when this payment was cancelled
    cancel_date = DateTimeCol(default=None)

    # FIXME: Figure out when and why this differs from value
    #: base value
    base_value = PriceCol(default=None)

    #: value of the payment
    value = PriceCol()

    #: the actual amount that was paid, including penalties, interest, discount etc.
    paid_value = PriceCol(default=None)

    #: interest of this payment
    interest = PriceCol(default=0)

    #: discount, an absolute value with the difference between the
    #: sales price and :obj:`.value`
    discount = PriceCol(default=0)

    #: penalty of the payment
    penalty = PriceCol(default=0)

    # FIXME: Figure out what this is used for
    #: number of the payment
    payment_number = UnicodeCol(default=None)

    branch_id = IdCol(allow_none=False)

    #: |branch| associated with this payment.
    #: For a :obj:`.TYPE_IN` payment, this is the branch that will receive
    #: the money. For a :obj:`.TYPE_IN` payment, this is the branch that
    #: will make the payment
    branch = Reference(branch_id, 'Branch.id')

    station_id = IdCol(allow_none=False)
    #: The station this object was created at
    station = Reference(station_id, 'BranchStation.id')

    method_id = IdCol()

    #: |paymentmethod| for this payment
    #: payment
    method = Reference(method_id, 'PaymentMethod.id')

    group_id = IdCol()

    #: |paymentgroup| for this payment
    group = Reference(group_id, 'PaymentGroup.id')

    category_id = IdCol()

    #: |paymentcategory| this payment belongs to, can be None
    category = Reference(category_id, 'PaymentCategory.id')

    #: list of :class:`comments <stoqlib.domain.payment.comments.PaymentComment>` for
    #: this payment
    comments = ReferenceSet('id', 'PaymentComment.payment_id')

    #: :class:`check data <stoqlib.domain.payment.method.CheckData>` for
    #: this payment
    check_data = Reference('id', 'CheckData.payment_id', on_remote=True)

    #: |accounttransaction| for this payment
    transaction = Reference('id',
                            'AccountTransaction.payment_id',
                            on_remote=True)

    card_data = Reference('id', 'CreditCardData.payment_id', on_remote=True)

    #: indicates if a bill has been received. They are usually delivered by
    #: mail before the due date. This is not indicating whether the payment has
    #: been paid, just that the receiver has notified the payer somehow.
    bill_received = BoolCol(default=False)

    attachment_id = IdCol()

    #: |attachment| for this payment
    attachment = Reference(attachment_id, 'Attachment.id')

    def __init__(self, store, branch, **kw):
        from stoqlib.domain.person import Branch
        assert isinstance(branch, Branch)
        if not 'value' in kw:
            raise TypeError('You must provide a value argument')
        if not 'base_value' in kw or not kw['base_value']:
            kw['base_value'] = kw['value']
        super(Payment, self).__init__(store=store, branch=branch, **kw)

    def _check_status(self, status, operation_name):
        fmt = 'Invalid status for %s operation: %s'
        assert self.status == status, (
            fmt % (operation_name, self.statuses[self.status]))

    #
    # ORMObject hooks
    #

    def delete(self):
        # First call hooks, do this first so the hook
        # have access to everything it needs
        self.method.operation.payment_delete(self)
        # FIXME: BUG 5581 check if it is really safe to remove the payment
        # when using with synced databases
        self.store.remove(self)

    @classmethod
    def create_repeated(cls,
                        store,
                        payment,
                        repeat_type,
                        start_date,
                        end_date,
                        temporary_identifiers=False):
        """Create a set of repeated payments.
        Given a type of interval (*repeat_type*), a start date and an end_date,
        this creates a list of payments for that interval.

        Note, this will also update the description of the payment that's passed
        in.
        :param store: a store
        :param payment: the payment to repeat
        :param repeat_type: the kind of repetition (weekly, monthly etc)
        :param start_date: the date to start this repetition
        :param end_date: the date to end this repetition
        :param temporary_identifiers: If the payments should be created with temporary
          identifiers
        :returns: a list of repeated payments
        """
        dates = create_date_interval(interval_type=repeat_type,
                                     start_date=start_date,
                                     end_date=end_date)
        n_dates = dates.count()
        if n_dates == 1:
            raise AssertionError
        description = payment.description
        payment.description = u'1/%d %s' % (n_dates, description)
        payment.due_date = dates[0]

        payments = []
        for i, date in enumerate(dates[1:]):
            temporary_identifier = None
            if temporary_identifiers:
                temporary_identifier = Payment.get_temporary_identifier(store)
            p = Payment(open_date=payment.open_date,
                        identifier=temporary_identifier,
                        branch=payment.branch,
                        station=payment.station,
                        payment_type=payment.payment_type,
                        status=payment.status,
                        description=u'%d/%d %s' %
                        (i + 2, n_dates, description),
                        value=payment.value,
                        base_value=payment.base_value,
                        due_date=date,
                        method=payment.method,
                        group=payment.group,
                        category=payment.category,
                        store=store)
            payments.append(p)
        return payments

    #
    # Properties
    #

    @property
    def comments_number(self):
        """The number of |paymentcomments| for this payment"""
        return self.comments.count()

    @property
    def bank_account_number(self):
        """For check payments, the :class:`bank account <BankAccount>` number"""
        # This is used by test_payment_method, and is a convenience
        # property, ideally we should move it to payment operation
        # somehow
        if self.method.method_name == u'check':
            data = self.method.operation.get_check_data_by_payment(self)
            bank_account = data.bank_account
            if bank_account:
                return bank_account.bank_number

    @property
    def installment_number(self):
        payments = self.group.get_valid_payments().order_by(Payment.identifier)
        for i, payment in enumerate(payments):
            if self == payment:
                return i + 1

    @property
    def status_str(self):
        """The :obj:`Payment.status` as a translated string"""
        if not self.status in self.statuses:
            raise DatabaseInconsistency('Invalid status for Payment '
                                        'instance, got %d' % self.status)
        return self.statuses[self.status]

    def get_days_late(self):
        """For due payments, the number of days late this payment is

        :returns: the number of days late
        """
        if self.status == Payment.STATUS_PAID:
            return 0

        days_late = localtoday().date() - self.due_date.date()
        if days_late.days < 0:
            return 0

        return days_late.days

    def set_pending(self):
        """Set a :obj:`.STATUS_PREVIEW` payment as :obj:`.STATUS_PENDING`.
        This also means that this is valid payment and its owner
        actually can charge it
        """
        self._check_status(self.STATUS_PREVIEW, u'set_pending')
        self.status = self.STATUS_PENDING

    def set_not_paid(self, change_entry):
        """Set a :obj:`.STATUS_PAID` payment as :obj:`.STATUS_PENDING`.
        This requires clearing paid_date and paid_value

        :param change_entry: a :class:`PaymentChangeHistory` object,
          that will hold the changes information
        """
        self._check_status(self.STATUS_PAID, u'set_not_paid')

        if self.transaction:
            self.transaction.create_reverse()

        change_entry.last_status = self.STATUS_PAID
        change_entry.new_status = self.STATUS_PENDING

        sale = self.group and self.group.sale

        if sale and sale.can_set_not_paid():
            sale.set_not_paid()
        self.status = self.STATUS_PENDING
        self.paid_date = None
        self.paid_value = None

    def pay(self,
            paid_date=None,
            paid_value=None,
            source_account=None,
            destination_account=None,
            account_transaction_number=None):
        """Pay the current payment set its status as :obj:`.STATUS_PAID`

        If this payment belongs to a sale, and all other payments from the sale
        are paid then the sale will be set as paid.
        """
        if self.status != Payment.STATUS_PENDING:
            raise ValueError(_(u"This payment is already paid."))
        self._check_status(self.STATUS_PENDING, u'pay')

        paid_value = paid_value or (self.value - self.discount + self.interest)
        self.paid_value = paid_value
        self.paid_date = paid_date or TransactionTimestamp()
        self.status = self.STATUS_PAID

        if (self.is_separate_payment()
                or self.method.operation.create_transaction()):
            AccountTransaction.create_from_payment(
                self,
                code=account_transaction_number,
                source_account=source_account,
                destination_account=destination_account)

        sale = self.group and self.group.sale
        if sale:
            sale.create_commission(self)

            # When paying payments of a sale, check if the other payments are
            # paid. If they are, this means you can change the sale status to
            # paid as well.
            if sale.can_set_paid():
                sale.set_paid()

        if self.value == self.paid_value:
            msg = _(
                u"{method} payment with value {value:.2f} was paid").format(
                    method=self.method.method_name, value=self.value)
        else:
            msg = _(u"{method} payment with value original value "
                    u"{original_value:.2f} was paid with value "
                    u"{value:.2f}").format(method=self.method.method_name,
                                           original_value=self.value,
                                           value=self.paid_value)
        Event.log(self.store, Event.TYPE_PAYMENT, msg.capitalize())

    def cancel(self, change_entry=None):
        """Cancel the payment, set it's status to :obj:`.STATUS_CANCELLED`
        """
        # TODO Check for till entries here and call cancel_till_entry if
        # it's possible. Bug 2598
        if not self.can_cancel():
            raise StoqlibError(
                _(u"Invalid status for cancel operation, "
                  u"got %s") % self.status_str)

        if self.transaction:
            self.transaction.create_reverse()

        old_status = self.status
        self.status = self.STATUS_CANCELLED
        self.cancel_date = TransactionTimestamp()

        if change_entry is not None:
            change_entry.last_status = old_status
            change_entry.new_status = self.status

        msg = _(
            u"{method} payment with value {value:.2f} was cancelled").format(
                method=self.method.method_name, value=self.value)
        Event.log(self.store, Event.TYPE_PAYMENT, msg.capitalize())

    def change_due_date(self, new_due_date):
        """Changes the payment due date.
        :param new_due_date: The new due date for the payment.
        :rtype: datetime.date
        """
        if self.status in [Payment.STATUS_PAID, Payment.STATUS_CANCELLED]:
            raise StoqlibError(
                _(u"Invalid status for change_due_date operation, "
                  u"got %s") % self.status_str)
        self.due_date = new_due_date

    def update_value(self, new_value):
        """Update the payment value.

        """
        self.value = new_value

    def can_cancel(self):
        return self.status in (Payment.STATUS_PREVIEW, Payment.STATUS_PENDING,
                               Payment.STATUS_PAID)

    def get_payable_value(self):
        """Returns the calculated payment value with the daily interest.

        Note that the payment group daily_interest must be between 0 and 100.

        :returns: the payable value
        """
        if self.status in [self.STATUS_PREVIEW, self.STATUS_CANCELLED]:
            return self.value
        if self.status in [
                self.STATUS_PAID, self.STATUS_REVIEWING, self.STATUS_CONFIRMED
        ]:
            return self.paid_value

        return self.value + self.get_interest()

    def get_penalty(self, date=None):
        """Calculate the penalty in an absolute value

        :param date: date of payment
        :returns: penalty
        :rtype: :class:`kiwi.currency.currency`
        """
        if date is None:
            date = localtoday().date()
        elif date < self.open_date.date():
            raise ValueError(_(u"Date can not be less then open date"))
        elif date > localtoday().date():
            raise ValueError(_(u"Date can not be greather then future date"))
        if not self.method.penalty:
            return currency(0)

        # Don't add penalty if we pay in time!
        if self.due_date.date() >= date:
            return currency(0)

        return currency(self.method.penalty / 100 * self.value)

    def get_interest(self, date=None, pay_penalty=True):
        """Calculate the interest in an absolute value

        :param date: date of payment
        :returns: interest
        :rtype: :class:`kiwi.currency.currency`
        """
        if date is None:
            date = localtoday().date()
        elif date < self.open_date.date():
            raise ValueError(_(u"Date can not be less then open date"))
        elif date > localtoday().date():
            raise ValueError(_(u"Date can not be greather then future date"))

        if not self.method.daily_interest:
            return currency(0)

        days = (date - self.due_date.date()).days
        if days <= 0:
            return currency(0)

        base_value = self.value + (pay_penalty and self.get_penalty(date=date))

        return currency(days * self.method.daily_interest / 100 * base_value)

    def has_commission(self):
        """Check if this |payment| already has a |commission|"""
        from stoqlib.domain.commission import Commission
        return self.store.find(Commission, payment=self).any()

    def is_paid(self):
        """Check if the payment is paid.

        :returns: ``True`` if the payment is paid
        """
        return self.status == Payment.STATUS_PAID

    def is_pending(self):
        """Check if the payment is pending.

        :returns: ``True`` if the payment is pending
        """
        return self.status == Payment.STATUS_PENDING

    def is_preview(self):
        """Check if the payment is in preview state

        :returns: ``True`` if the payment is paid
        """
        return self.status == Payment.STATUS_PREVIEW

    def is_cancelled(self):
        """Check if the payment was cancelled.

        :returns: ``True`` if the payment was cancelled
        """
        return self.status == Payment.STATUS_CANCELLED

    def get_paid_date_string(self):
        """Get a paid date string

        :returns: the paid date string or PAID DATE if the payment isn't paid
        """
        if self.paid_date:
            return self.paid_date.date().strftime('%x')
        return _(u'NOT PAID')

    def get_open_date_string(self):
        """Get a open date string

        :returns: the open date string or empty string
        """
        if self.open_date:
            return self.open_date.date().strftime('%x')
        return u""

    def is_inpayment(self):
        """Find out if a payment is :obj:`incoming <.TYPE_IN>`

        :returns: ``True`` if it's incoming
        """
        return self.payment_type == self.TYPE_IN

    def is_outpayment(self):
        """Find out if a payment is :obj:`outgoing <.TYPE_OUT>`

        :returns: ``True`` if it's outgoing
        """
        return self.payment_type == self.TYPE_OUT

    def is_separate_payment(self):
        """Find out if this payment is created separately from a
        sale, purchase or renegotiation
        :returns: ``True`` if it's separate.
        """

        # FIXME: This is a hack, we should rather store a flag
        #        in the database that tells us how the payment was
        #        created.
        group = self.group
        if not group:
            # Should never happen
            return False

        if group.sale:
            return False
        elif group.purchase:
            return False
        elif group._renegotiation:
            return False

        return True

    def is_of_method(self, method_name):
        """Find out if the payment was made with a certain method

        :returns: ``True`` if it's a payment of that method
        """
        return self.method.method_name == method_name
コード例 #26
0
ファイル: purchase.py プロジェクト: pjamil/stoq
class PurchaseItem(Domain):
    """This class stores information of the purchased items.
    """

    __storm_table__ = 'purchase_item'

    quantity = QuantityCol(default=1)
    quantity_received = QuantityCol(default=0)
    quantity_sold = QuantityCol(default=0)
    quantity_returned = QuantityCol(default=0)

    #: the cost which helps the purchaser to define the
    #: main cost of a certain product.
    base_cost = PriceCol()

    cost = PriceCol()

    #: The ICMS ST value for the product purchased
    icms_st_value = PriceCol(default=0)

    #: The IPI value for the product purchased
    ipi_value = PriceCol(default=0)

    expected_receival_date = DateTimeCol(default=None)

    sellable_id = IdCol()

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

    order_id = IdCol()

    #: the |purchase|
    order = Reference(order_id, 'PurchaseOrder.id')

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

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

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

        # FIXME: Avoding shadowing sellable.cost
        kw['base_cost'] = kw['sellable'].cost

        if not 'cost' in kw:
            kw['cost'] = kw['sellable'].cost

        Domain.__init__(self, store=store, **kw)

    #
    # Accessors
    #

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

    def get_total_sold(self):
        return currency(self.quantity_sold * self.cost)

    def get_received_total(self):
        return currency(self.quantity_received * self.cost)

    def has_been_received(self):
        return self.quantity_received >= self.quantity

    def has_partial_received(self):
        return self.quantity_received > 0

    def get_pending_quantity(self):
        return self.quantity - self.quantity_received

    def get_quantity_as_string(self):
        unit = self.sellable.unit
        return u"%s %s" % (format_quantity(self.quantity),
                           unit and unit.description or u"")

    def get_quantity_received_as_string(self):
        unit = self.sellable.unit
        return u"%s %s" % (format_quantity(self.quantity_received),
                           unit and unit.description or u"")

    @classmethod
    def get_ordered_quantity(cls, store, sellable):
        """Returns the quantity already ordered of a given sellable.

        :param store: a store
        :param sellable: the sellable we want to know the quantity ordered.
        :returns: the quantity already ordered of a given sellable or zero if
          no quantity have been ordered.
        """
        query = And(PurchaseItem.sellable_id == sellable.id,
                    PurchaseOrder.id == PurchaseItem.order_id,
                    PurchaseOrder.status == PurchaseOrder.ORDER_CONFIRMED)
        ordered_items = store.find(PurchaseItem, query)
        return ordered_items.sum(PurchaseItem.quantity) or Decimal(0)

    def return_consignment(self, quantity):
        """
        Return this as a consignment item

        :param quantity: the quantity to return
        """
        storable = self.sellable.product_storable
        assert storable
        storable.decrease_stock(quantity=quantity,
                                branch=self.order.branch,
                                type=StockTransactionHistory.TYPE_CONSIGNMENT_RETURNED,
                                object_id=self.id)

    def get_component_quantity(self, parent):
        """Get the quantity of a component.

        :param parent: the |purchase_item| parent_item of self
        :returns: the quantity of the component
        """
        for component in parent.sellable.product.get_components():
            if self.sellable.product == component.component:
                return component.quantity
コード例 #27
0
ファイル: receiving.py プロジェクト: n3zsistemas-bkp/stoq
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)
コード例 #28
0
ファイル: db.py プロジェクト: ricky/supysonic
        track_with_cover = self.tracks.find(Track.folder_id == Folder.id, Folder.has_cover_art).any()
        if track_with_cover:
            info['coverArt'] = str(track_with_cover.folder_id)

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

        return info

    def sort_key(self):
        year = min(map(lambda t: t.year if t.year else 9999, self.tracks))
        return '%i%s' % (year, self.name.lower())

Artist.albums = ReferenceSet(Artist.id, Album.artist_id)

class Track(object):
    __storm_table__ = 'track'

    id = UUID(primary = True, default_factory = uuid.uuid4)
    disc = Int()
    number = Int()
    title = Unicode()
    year = Int() # nullable
    genre = Unicode() # nullable
    duration = Int()
    album_id = UUID()
    album = Reference(album_id, Album.id)
    artist_id = UUID()
    artist = Reference(artist_id, Artist.id)
コード例 #29
0
class Till(IdentifiableDomain):
    """The Till describes the financial operations of a specific day.

    The operations that are recorded in a Till:

      * Sales
      * Adding cash
      * Removing cash
      * Giving out an early salary

    Each operation is associated with a |tillentry|.

    You can only open a Till once per day, and you cannot open a new
    till before you closed the previously opened one.
    """

    __storm_table__ = 'till'

    #: this till is created, but not yet opened
    STATUS_PENDING = u'pending'

    #: this till is opened and we can make sales for it.
    STATUS_OPEN = u'open'

    #: end of the day, the till is closed and no more
    #: financial operations can be done in this store.
    STATUS_CLOSED = u'closed'

    #: after the till is closed, it can optionally be verified by a different user
    #: (usually a manager or supervisor)
    STATUS_VERIFIED = u'verified'

    statuses = collections.OrderedDict([
        (STATUS_PENDING, _(u'Pending')),
        (STATUS_OPEN, _(u'Opened')),
        (STATUS_CLOSED, _(u'Closed')),
        (STATUS_VERIFIED, _(u'Verified')),
    ])

    #: A sequencial number that identifies this till.
    identifier = IdentifierCol()

    status = EnumCol(default=STATUS_PENDING)

    #: The total amount we had the moment the till was opened.
    initial_cash_amount = PriceCol(default=0, allow_none=False)

    #: The total amount we have the moment the till is closed.
    final_cash_amount = PriceCol(default=0, allow_none=False)

    #: When the till was opened or None if it has not yet been opened.
    opening_date = DateTimeCol(default=None)

    #: When the till was closed or None if it has not yet been closed
    closing_date = DateTimeCol(default=None)

    #: When the till was verifyed or None if it's not yet verified
    verify_date = DateTimeCol(default=None)

    station_id = IdCol()
    #: the |branchstation| associated with the till, eg the computer
    #: which opened it.
    station = Reference(station_id, 'BranchStation.id')

    branch_id = IdCol()
    #: the branch this till is from
    branch = Reference(branch_id, 'Branch.id')

    observations = UnicodeCol(default=u"")

    responsible_open_id = IdCol()
    #: The responsible for opening the till
    responsible_open = Reference(responsible_open_id, "LoginUser.id")

    responsible_close_id = IdCol()
    #: The responsible for closing the till
    responsible_close = Reference(responsible_close_id, "LoginUser.id")

    responsible_verify_id = IdCol()
    #: The responsible for verifying the till
    responsible_verify = Reference(responsible_verify_id, "LoginUser.id")

    summary = ReferenceSet('id', 'TillSummary.till_id')

    #
    # Classmethods
    #

    @classmethod
    def get_current(cls, store, station: BranchStation):
        """Fetches the Till for the current station.

        :param store: a store
        :returns: a Till instance or None
        """
        assert station is not None

        till = store.find(cls, status=Till.STATUS_OPEN, station=station).one()
        if till and till.needs_closing():
            fmt = _("You need to close the till opened at %s before "
                    "doing any fiscal operations")
            raise TillError(fmt % (till.opening_date.date(), ))

        return till

    @classmethod
    def get_last_opened(cls, store, station: BranchStation):
        """Fetches the last Till which was opened.
        If in doubt, use Till.get_current instead. This method is a special case
        which is used to be able to close a till without calling get_current()

        :param store: a store
        """
        result = store.find(Till, status=Till.STATUS_OPEN, station=station)
        result = result.order_by(Till.opening_date)
        if not result.is_empty():
            return result[0]

    @classmethod
    def get_last(cls, store, station: BranchStation):
        result = store.find(Till, station=station).order_by(Till.opening_date)
        return result.last()

    @classmethod
    def get_last_closed(cls, store, station: BranchStation):
        result = store.find(Till, station=station,
                            status=Till.STATUS_CLOSED).order_by(Till.opening_date)
        return result.last()

    #
    # Till methods
    #

    def open_till(self, user: LoginUser):
        """Open the till.

        It can only be done once per day.
        The final cash amount of the previous till will be used
        as the initial value in this one after opening it.
        """
        if self.status == Till.STATUS_OPEN:
            raise TillError(_('Till is already open'))

        manager = get_plugin_manager()
        # The restriction to only allow opening the till only once per day comes from
        # the (soon to be obsolete) ECF devices.
        if manager.is_active('ecf'):
            # Make sure that the till has not been opened today
            today = localtoday().date()
            if not self.store.find(Till,
                                   And(Date(Till.opening_date) >= today,
                                       Till.station_id == self.station.id)).is_empty():
                raise TillError(_("A till has already been opened today"))

        last_till = self._get_last_closed_till()
        if last_till:
            if not last_till.closing_date:
                raise TillError(_("Previous till was not closed"))

            initial_cash_amount = last_till.final_cash_amount
        else:
            initial_cash_amount = 0

        self.initial_cash_amount = initial_cash_amount

        self.opening_date = TransactionTimestamp()
        self.status = Till.STATUS_OPEN
        self.responsible_open = user
        assert self.responsible_open is not None
        TillOpenedEvent.emit(self)

    def close_till(self, user: LoginUser, observations=""):
        """This method close the current till operation with the confirmed
        sales associated. If there is a sale with a differente status than
        SALE_CONFIRMED, a new 'pending' till operation is created and
        these sales are associated with the current one.
        """

        if self.status == Till.STATUS_CLOSED:
            raise TillError(_("Till is already closed"))

        if self.get_cash_amount() < 0:
            raise ValueError(_("Till balance is negative, but this should not "
                               "happen. Contact Stoq Team if you need "
                               "assistance"))

        self.final_cash_amount = self.get_cash_amount()
        self.closing_date = TransactionTimestamp()
        self.status = Till.STATUS_CLOSED
        self.observations = observations
        self.responsible_close = user
        assert self.responsible_open is not None
        TillClosedEvent.emit(self)

    def add_entry(self, payment):
        """
        Adds an entry to the till.

        :param payment: a |payment|
        :returns: |tillentry| representing the added debit
        """
        if payment.is_inpayment():
            value = payment.value
        elif payment.is_outpayment():
            value = -payment.value
        else:  # pragma nocoverage
            raise AssertionError(payment)

        return self._add_till_entry(value, payment.description, payment)

    def add_debit_entry(self, value, reason=u""):
        """Add debit to the till

        :param value: amount to add
        :param reason: description of payment
        :returns: |tillentry| representing the added debit
        """
        assert value >= 0

        return self._add_till_entry(-value, reason)

    def add_credit_entry(self, value, reason=u""):
        """Add credit to the till

        :param value: amount to add
        :param reason: description of entry
        :returns: |tillentry| representing the added credit
        """
        assert value >= 0

        return self._add_till_entry(value, reason)

    def needs_closing(self):
        """Checks if there's an open till that needs to be closed before
        we can do any further fiscal operations.
        :returns: True if it needs to be closed, otherwise false
        """
        if self.status != Till.STATUS_OPEN:
            return False

        # Verify that the till wasn't opened today
        if self.opening_date.date() == localtoday().date():
            return False

        if localnow().hour < sysparam.get_int('TILL_TOLERANCE_FOR_CLOSING'):
            return False

        return True

    def get_balance(self):
        """Returns the balance of all till operations plus the initial amount
        cash amount.
        :returns: the balance
        :rtype: currency
        """
        total = self.get_entries().sum(TillEntry.value) or 0
        return currency(self.initial_cash_amount + total)

    def get_cash_amount(self):
        """Returns the total cash amount on the till. That includes "extra"
        payments (like cash advance, till complement and so on), the money
        payments and the initial cash amount.
        :returns: the cash amount on the till
        :rtype: currency
        """
        store = self.store
        money = PaymentMethod.get_by_name(store, u'money')

        clause = And(Or(Eq(TillEntry.payment_id, None),
                        Payment.method_id == money.id),
                     TillEntry.till_id == self.id)

        join = LeftJoin(Payment, Payment.id == TillEntry.payment_id)
        results = store.using(TillEntry, join).find(TillEntry, clause)

        return currency(self.initial_cash_amount +
                        (results.sum(TillEntry.value) or 0))

    def get_entries(self):
        """Fetches all the entries related to this till
        :returns: all entries
        :rtype: sequence of |tillentry|
        """
        return self.store.find(TillEntry, till=self)

    def get_credits_total(self):
        """Calculates the total credit for all entries in this till
        :returns: total credit
        :rtype: currency
        """
        results = self.store.find(
            TillEntry, And(TillEntry.value > 0,
                           TillEntry.till_id == self.id))
        return currency(results.sum(TillEntry.value) or 0)

    def get_debits_total(self):
        """Calculates the total debit for all entries in this till
        :returns: total debit
        :rtype: currency
        """
        results = self.store.find(
            TillEntry, And(TillEntry.value < 0,
                           TillEntry.till_id == self.id))
        return currency(results.sum(TillEntry.value) or 0)

    def get_day_summary_data(self) -> Dict[Tuple[PaymentMethod,
                                                 Optional['stoqlib.domain.payment.card.CreditProvider'],
                                                 Optional[str]], currency]:
        """Get the summary of this till.
        """
        money_method = PaymentMethod.get_by_name(self.store, u'money')
        day_history = {}
        # Keys are (method, provider, card_type), provider and card_type may be None if
        # payment was not with card
        day_history[(money_method, None, None)] = currency(0)

        for entry in self.get_entries():
            provider = card_type = None
            payment = entry.payment
            method = payment.method if payment else money_method
            if payment and payment.card_data:
                provider = payment.card_data.provider
                card_type = payment.card_data.card_type

            key = (method, provider, card_type)
            day_history.setdefault(key, currency(0))
            day_history[key] += entry.value

        return day_history

    @deprecated(new='create_day_summary')
    def get_day_summary(self):
        return self.create_day_summary()  # pragma nocover

    def create_day_summary(self) -> List['TillSummary']:
        """Get the summary of this till for closing.

        When using a blind closing process, this will create TillSummary entries that
        will save the values all payment methods used.
        """
        summary = []
        for (method, provider, card_type), value in self.get_day_summary_data().items():
            summary.append(TillSummary(till=self, method=method, provider=provider,
                                       card_type=card_type, system_value=value))
        return summary

    #
    # Private
    #

    def _get_last_closed_till(self):
        results = self.store.find(Till, status=Till.STATUS_CLOSED,
                                  station=self.station).order_by(Till.opening_date)
        return results.last()

    def _add_till_entry(self, value, description, payment=None):
        assert value != 0
        return TillEntry(value=value,
                         description=description,
                         payment=payment,
                         till=self,
                         station=self.station,
                         branch=self.station.branch,
                         store=self.store)
コード例 #30
0
ファイル: loan.py プロジェクト: sarkis89/stoq
class Loan(Domain):
    """
    A loan is a collection of |sellable| that is being loaned
    to a |client|, the items are expected to be either be
    returned to stock or sold via a |sale|.

    A loan that can hold a set of :class:`loan items <LoanItem>`

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

    __storm_table__ = 'loan'

    #: The request for a loan has been added to the system,
    #: we know which of the items the client wishes to loan,
    #: it's not defined if the client has actually picked up
    #: the items.
    STATUS_OPEN = u'open'

    #: All the products or other sellable items have been
    #: returned and are available in stock.
    STATUS_CLOSED = u'closed'

    #: The loan is cancelled and all the products or other sellable items have
    #: been returned and are available in stock.
    STATUS_CANCELLED = u'cancelled'

    # FIXME: This is missing a few states,
    #        STATUS_LOANED: stock is completely synchronized
    statuses = {
        STATUS_OPEN: _(u'Opened'),
        STATUS_CLOSED: _(u'Closed'),
        STATUS_CANCELLED: _(u'Cancelled')
    }

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

    #: status of the loan
    status = EnumCol(allow_none=False, default=STATUS_OPEN)

    #: notes related to this loan.
    notes = UnicodeCol(default=u'')

    #: date loan was opened
    open_date = DateTimeCol(default_factory=localnow)

    #: date loan was closed
    close_date = DateTimeCol(default=None)

    #: loan expires on this date, we expect the items to
    #: to be returned by this date
    expire_date = DateTimeCol(default=None)

    #: the date the loan was cancelled
    cancel_date = DateTimeCol(default=None)

    removed_by = UnicodeCol(default=u'')

    #: the reason the loan was cancelled
    cancel_reason = UnicodeCol()

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

    #: :class:`user <stoqlib.domain.person.LoginUser>` of the system
    #: that made the loan
    # FIXME: Should probably be a SalesPerson, we can find the
    #        LoginUser via te.user_id
    responsible_id = IdCol()
    responsible = Reference(responsible_id, 'LoginUser.id')

    #: client that loaned the items
    client_id = IdCol(default=None)
    client = Reference(client_id, 'Client.id')

    client_category_id = IdCol(default=None)

    #: the |clientcategory| used for price determination.
    client_category = Reference(client_category_id, 'ClientCategory.id')

    #: a list of all items loaned in this loan
    loaned_items = ReferenceSet('id', 'LoanItem.loan_id')

    #: |payments| generated by this loan
    payments = None

    #: |transporter| used in loan
    transporter = None

    invoice_id = IdCol()

    #: The |invoice| generated by the loan
    invoice = Reference(invoice_id, 'Invoice.id')

    #: The responsible for cancelling the loan. At the moment, the
    #: |loginuser| that cancelled the loan
    cancel_responsible_id = IdCol()
    cancel_responsible = Reference(cancel_responsible_id, 'LoginUser.id')

    def __init__(self, store=None, **kwargs):
        kwargs['invoice'] = Invoice(store=store, invoice_type=Invoice.TYPE_OUT)
        super(Loan, self).__init__(store=store, **kwargs)

    #
    # Classmethods
    #

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

    #
    # IContainer implementation
    #

    def add_item(self, loan_item):
        assert not loan_item.loan
        loan_item.loan = self

    def get_items(self):
        return self.store.find(LoanItem, loan=self)

    def remove_item(self, loan_item):
        loan_item.loan = None
        self.store.maybe_remove(loan_item)

    #
    # IInvoice implementation
    #

    @property
    def comments(self):
        return [Settable(comment=self.notes)]

    @property
    def discount_value(self):
        discount = currency(0)
        for item in self.get_items():
            if item.price > item.sellable.base_price:
                continue
            discount += item.sellable.base_price - item.price
        return discount

    @property
    def invoice_subtotal(self):
        return self.get_sale_base_subtotal()

    @property
    def invoice_total(self):
        return self.get_total_amount()

    @property
    def recipient(self):
        return self.client.person

    @property
    def operation_nature(self):
        # TODO: Save the operation nature in new loan table field.
        return _(u"Loan")

    #
    # Public API
    #

    def add_sellable(self, sellable, quantity=1, price=None, batch=None):
        """Adds a new sellable item to a loan

        :param sellable: the |sellable|
        :param quantity: quantity to add, defaults to 1
        :param price: optional, the price, it not set the price
          from the sellable will be used
        :param batch: the |batch| this sellable comes from if the sellable is a
          storable. Should be ``None`` if it is not a storable or if the storable
          does not have batches.
        """
        self.validate_batch(batch, sellable=sellable)
        price = price or sellable.price
        base_price = sellable.price
        return LoanItem(store=self.store,
                        quantity=quantity,
                        loan=self,
                        sellable=sellable,
                        batch=batch,
                        price=price,
                        base_price=base_price)

    def get_available_discount_for_items(self, user=None, exclude_item=None):
        """Get available discount for items in this loan

        The available items discount is the total discount not used
        by items in this sale. For instance, if we have 2 products
        with a price of 100 and they can have 10% of discount, we have
        20 of discount available. If one of those products price
        is set to 98, that is, using 2 of it's discount, the available
        discount is now 18.

        :param user: passed to
            :meth:`stoqlib.domain.sellable.Sellable.get_maximum_discount`
            together with :obj:`.client_category` to check for the max
            discount for sellables on this sale
        :param exclude_item: a |saleitem| to exclude from the calculations.
            Useful if you are trying to get some extra discount for that
            item and you don't want it's discount to be considered here
        :returns: the available discount
        """
        available_discount = currency(0)
        used_discount = currency(0)

        for item in self.get_items():
            if item == exclude_item:
                continue
            # Don't put surcharges on the discount, or it can end up negative
            if item.price > item.sellable.base_price:
                continue

            used_discount += item.sellable.base_price - item.price
            max_discount = item.sellable.get_maximum_discount(
                category=self.client_category, user=user) / 100
            available_discount += item.base_price * max_discount

        return available_discount - used_discount

    def set_items_discount(self, discount):
        """Apply discount on this sale's items

        :param decimal.Decimal discount: the discount to be applied
            as a percentage, e.g. 10.0, 22.5
        """
        new_total = currency(0)

        item = None
        candidate = None
        for item in self.get_items():
            item.set_discount(discount)
            new_total += item.price * item.quantity
            if item.quantity == 1:
                candidate = item

        # Since we apply the discount percentage above, items can generate a
        # 3rd decimal place, that will be rounded to the 2nd, making the value
        # differ. Find that difference and apply it to a sale item, preferable
        # to one with a quantity of 1 since, for instance, applying +0,1 to an
        # item with a quantity of 4 would make it's total +0,4 (+0,3 extra than
        # we are trying to adjust here).
        discount_value = (self.get_sale_base_subtotal() * discount) / 100
        diff = new_total - self.get_sale_base_subtotal() + discount_value
        if diff:
            item = candidate or item
            item.price -= diff

    #
    # Accessors
    #

    def get_total_amount(self):
        """
        Fetches the total value of the loan, that is to be paid by
        the client.

        It can be calculated as::

            Sale total = Sum(product and service prices) + surcharge +
                             interest - discount

        :returns: the total value
        """
        return currency(self.get_items().sum(
            Round(LoanItem.price * LoanItem.quantity, DECIMAL_PRECISION)) or 0)

    def get_client_name(self):
        if self.client:
            return self.client.person.name
        return u''

    def get_branch_name(self):
        if self.branch:
            return self.branch.get_description()
        return u''

    def get_responsible_name(self):
        return self.responsible.person.name

    #
    # Public API
    #

    def sync_stock(self):
        """Synchronizes the stock of *self*'s :class:`loan items <LoanItem>`

        Just a shortcut to call :meth:`LoanItem.sync_stock` of all of
        *self*'s :class:`loan items <LoanItem>` instead of having
        to do that one by one.
        """
        for loan_item in self.get_items():
            # No need to sync stock for products that dont need.
            if not loan_item.sellable.product.manage_stock:
                continue
            loan_item.sync_stock()

    def can_close(self):
        """Checks if the loan can be closed. A loan can be closed if it is
        opened and all the items have been returned or sold.
        :returns: True if the loan can be closed, False otherwise.
        """
        if self.status != Loan.STATUS_OPEN:
            return False
        for item in self.get_items():
            if item.sale_quantity + item.return_quantity != item.quantity:
                return False
        return True

    def get_sale_base_subtotal(self):
        """Get the base subtotal of items

        Just a helper that, unlike :meth:`.get_sale_subtotal`, will
        return the total based on item's base price.

        :returns: the base subtotal
        """
        subtotal = self.get_items().sum(LoanItem.quantity *
                                        LoanItem.base_price)
        return currency(subtotal)

    def close(self):
        """Closes the loan. At this point, all the loan items have been
        returned to stock or sold."""
        assert self.can_close()
        self.close_date = localnow()
        self.status = Loan.STATUS_CLOSED

    def confirm(self):
        # Save the operation nature and branch in Invoice table.
        self.invoice.operation_nature = self.operation_nature
        self.invoice.branch = self.branch
        # Since there is no status change here and the event requires
        # the parameter, we use None
        old_status = None
        StockOperationConfirmedEvent.emit(self, old_status)
コード例 #31
0
ファイル: sellable.py プロジェクト: lucaslamounier/stoq
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)
コード例 #32
0
class Address(Domain):
    __storm_table__ = 'address'

    street = UnicodeCol(default=u'')
    streetnumber = IntCol(default=None)
    district = UnicodeCol(default=u'')
    postal_code = UnicodeCol(default=u'')
    complement = UnicodeCol(default=u'')
    is_main_address = BoolCol(default=False)
    person_id = IntCol()
    person = Reference(person_id, Person.id)
    city_location_id = IntCol()
    city_location = Reference(city_location_id, CityLocation.id)

Person.addresses = ReferenceSet(Person.id, Address.person_id)


class Delivery(Domain):
    __storm_table__ = 'delivery'

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

    status = IntCol(default=STATUS_INITIAL)
    open_date = DateTimeCol(default=None)
    deliver_date = DateTimeCol(default=None)
    receive_date = DateTimeCol(default=None)
    tracking_code = UnicodeCol(default=u'')
コード例 #33
0
ファイル: receiving.py プロジェクト: n3zsistemas-bkp/stoq
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
コード例 #34
0
ファイル: sqlobject.py プロジェクト: marcosstevens2012/GPP
 def _get_bound_reference_set(self, obj):
     assert obj is not None
     return ReferenceSet.__get__(self, obj)