Example #1
0
    def open_till(self):
        """Open the till.

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

        # 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
Example #2
0
def _ensure_card_providers():
    """ Creates a list of default card providers """
    log.info("Creating Card Providers")
    from stoqlib.domain.payment.card import CreditProvider, CardPaymentDevice

    providers = {
        u'VISA': u'VISA',
        u'MASTER': u'MASTERCARD',
        u'AMEX': u'AMERICAN EXPRESS'
    }
    store = new_store()
    for short_name, provider_id in providers.items():
        provider = CreditProvider.get_provider_by_provider_id(
            provider_id, store)
        if not provider.is_empty():
            continue

        CreditProvider(short_name=short_name,
                       provider_id=providers[short_name],
                       open_contract_date=TransactionTimestamp(),
                       store=store)
    devices = store.find(CardPaymentDevice)
    if devices.is_empty():
        CardPaymentDevice(store=store, description=_(u'Default'))
    store.commit(close=True)
Example #3
0
    def _update_te(self):
        user = get_current_user(self.store)
        station = get_current_station(self.store)

        self.te_modified.te_time = TransactionTimestamp()
        self.te_modified.user_id = user and user.id
        self.te_modified.station_id = station and station.id
Example #4
0
    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())
Example #5
0
    def set_renegotiated(self):
        """Set the sale as renegotiated. The sale payments have been
        renegotiated and the operations will be done in other payment group."""
        assert self.can_set_renegotiated()

        self.close_date = TransactionTimestamp()
        self.status = PaymentRenegotiation.STATUS_RENEGOTIATED
Example #6
0
    def confirm(self, confirm_date=None):
        """Confirms the purchase order

        :param confirm_data: optional, datetime
        """
        if confirm_date is None:
            confirm_date = TransactionTimestamp()

        if self.status not in [PurchaseOrder.ORDER_PENDING,
                               PurchaseOrder.ORDER_CONSIGNED]:
            fmt = _(u'Invalid order status, it should be '
                    u'ORDER_PENDING or ORDER_CONSIGNED, got %s')
            raise ValueError(fmt % (self.status_str, ))

        # In consigned purchases there is no payments at this point.
        if self.status != PurchaseOrder.ORDER_CONSIGNED and self.group:
            for payment in self.payments:
                payment.set_pending()

        if self.supplier and self.group:
            self.group.recipient = self.supplier.person

        self.responsible = get_current_user(self.store)
        self.status = PurchaseOrder.ORDER_CONFIRMED
        self.confirm_date = confirm_date

        Event.log(self.store, Event.TYPE_ORDER,
                  _(u"Order %s, total value %2.2f, supplier '%s' "
                    u"is now confirmed") % (self.identifier,
                                            self.purchase_total,
                                            self.supplier.person.name))
Example #7
0
def _create_transaction(store, till_entry):
    # Dont create till entries for sangrias/suprimentos as those are really tied to the
    # old ECF behaivour (where *all* the sales values are added to the till). If we
    # create transactions for those operations, the value would be duplicated when the
    # payment is finally payed.
    if not till_entry.payment:
        return

    if till_entry.value > 0:
        operation_type = AccountTransaction.TYPE_IN
        source_account = sysparam.get_object_id('IMBALANCE_ACCOUNT')
        dest_account = sysparam.get_object_id('TILLS_ACCOUNT')
    else:
        operation_type = AccountTransaction.TYPE_OUT
        source_account = sysparam.get_object_id('TILLS_ACCOUNT')
        dest_account = sysparam.get_object_id('IMBALANCE_ACCOUNT')

    AccountTransaction(description=till_entry.description,
                       source_account_id=source_account,
                       account_id=dest_account,
                       value=abs(till_entry.value),
                       code=str(till_entry.identifier),
                       date=TransactionTimestamp(),
                       store=store,
                       payment=till_entry.payment,
                       operation_type=operation_type)
Example #8
0
    def confirm(self, confirm_date=None):
        """Confirms the purchase order

        :param confirm_data: optional, datetime
        """
        if confirm_date is None:
            confirm_date = TransactionTimestamp()

        if self.status not in [PurchaseOrder.ORDER_PENDING,
                               PurchaseOrder.ORDER_CONSIGNED]:
            raise ValueError(
                _(u'Invalid order status, it should be '
                  u'ORDER_PENDING or ORDER_CONSIGNED, got %s') % (
                      self.get_status_str(), ))

        transaction = IPaymentTransaction(self)
        transaction.confirm()

        if self.supplier:
            self.group.recipient = self.supplier.person

        self.responsible = get_current_user(self.store)
        self.status = PurchaseOrder.ORDER_CONFIRMED
        self.confirm_date = confirm_date

        Event.log(Event.TYPE_ORDER,
                  _(u"Order %s, total value %2.2f, supplier '%s' "
                    u"is now confirmed") % (self.identifier,
                                            self.get_purchase_total(),
                                            self.supplier.person.name))
Example #9
0
File: till.py Project: romaia/stoq
    def open_till(self):
        """Open the till.

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

        # Make sure that the till has not been opened today
        today = datetime.date.today()
        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
Example #10
0
    def create_reverse(self):
        """Reverse this transaction, this happens when a payment
        is set as not paid.

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

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

        self.payment = None
        new_type = self.get_inverted_operation_type(self.operation_type)
        return AccountTransaction(source_account=self.account,
                                  account=self.source_account,
                                  value=self.value,
                                  description=_(u"Reverted: %s") %
                                  (self.description),
                                  code=self.code,
                                  date=TransactionTimestamp(),
                                  store=self.store,
                                  payment=None,
                                  operation_type=new_type)
Example #11
0
def _create_transaction(store, till_entry):
    AccountTransaction(description=till_entry.description,
                       source_account=sysparam(store).IMBALANCE_ACCOUNT,
                       account=sysparam(store).TILLS_ACCOUNT,
                       value=till_entry.value,
                       code=unicode(till_entry.id),
                       date=TransactionTimestamp(),
                       store=store,
                       payment=till_entry.payment)
Example #12
0
 def _on_object_added(self, obj_info):
     store = obj_info.get("store")
     for attr, entry_type in [('te_created', TransactionEntry.CREATED),
                              ('te_modified', TransactionEntry.MODIFIED)]:
         entry = TransactionEntry(te_time=TransactionTimestamp(),
                                  user_id=None,
                                  station_id=None,
                                  type=entry_type,
                                  store=store)
         setattr(self, attr, entry)
Example #13
0
    def open_till(self):
        """Open the till.

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

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

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

            initial_cash_amount = last_till.final_cash_amount
        else:
            initial_cash_amount = 0

        self.initial_cash_amount = initial_cash_amount

        self.opening_date = TransactionTimestamp()
        self.status = Till.STATUS_OPEN
        self.responsible_open = get_current_user(self.store)
        assert self.responsible_open is not None
        TillOpenedEvent.emit(self)
Example #14
0
    def add_sold_item(cls, store, branch, product_sellable_item):
        """Adds a |saleitem| to the history. *product_sale_item* is an item
        that was created during a |sale|.

        :param store: a store
        :param branch: the |branch|
        :param product_sellable_item: the |saleitem| for the sold |product|
        """
        cls(branch=branch,
            sellable=product_sellable_item.sellable,
            quantity_sold=product_sellable_item.quantity,
            sold_date=TransactionTimestamp(),
            store=store)
Example #15
0
 def _create_fiscal_entry(cls, store, entry_type, group, cfop, invoice_number,
                          iss_value=0, icms_value=0, ipi_value=0):
     return FiscalBookEntry(
         entry_type=entry_type,
         iss_value=iss_value,
         ipi_value=ipi_value,
         icms_value=icms_value,
         invoice_number=invoice_number,
         cfop=cfop,
         drawee=group.recipient,
         branch=get_current_branch(store),
         date=TransactionTimestamp(),
         payment_group=group,
         store=store)
Example #16
0
    def _update_sintegra_data(self):
        data = self._driver.get_sintegra()
        if data is None:
            return

        store = new_store()
        # coupon_start and coupon_end are actually, start coo, and current coo.
        coupon_start = data.coupon_start
        coupon_end = data.coupon_end
        # 0 means that the start coo isn't known, fetch
        # the current coo from the the database and add 1
        # TODO: try to avoid this hack
        if coupon_start == 0:
            results = store.find(FiscalDayHistory,
                                 station=self._printer.station).order_by(
                                     Desc(FiscalDayHistory.emission_date))
            if results.count():
                coupon_start = results[0].coupon_end + 1
            else:
                coupon_start = 1

        # Something went wrong or no coupons opened during the day
        if coupon_end <= coupon_start:
            store.commit(close=True)
            return

        station = store.fetch(self._printer.station)
        day = FiscalDayHistory(
            store=store,
            emission_date=data.opening_date,
            station=station,
            serial=unicode(data.serial),
            # 1 -> 001, FIXME: should fix stoqdrivers
            serial_id=int(data.serial_id),
            coupon_start=coupon_start,
            coupon_end=coupon_end,
            crz=data.crz,
            cro=data.cro,
            reduction_date=TransactionTimestamp(),
            period_total=data.period_total,
            total=data.total)

        for code, value, type in data.taxes:
            FiscalDayTax(fiscal_day_history=day,
                         code=unicode(code),
                         value=value,
                         type=unicode(type),
                         store=store)
        store.commit(close=True)
Example #17
0
    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())
Example #18
0
    def close(self, close_date=None):
        """Closes the inventory process

        :param close_date: the closing date or None for right now.
        :type: datetime.datetime
        """
        if not close_date:
            close_date = TransactionTimestamp()

        if not self.is_open():
            raise AssertionError("You can not close an inventory which is "
                                 "already closed!")

        self.close_date = close_date
        self.status = Inventory.STATUS_CLOSED
Example #19
0
def _create_transaction(store, till_entry):
    if till_entry.value > 0:
        operation_type = AccountTransaction.TYPE_IN
        source_account = sysparam.get_object_id('IMBALANCE_ACCOUNT')
        dest_account = sysparam.get_object_id('TILLS_ACCOUNT')
    else:
        operation_type = AccountTransaction.TYPE_OUT
        source_account = sysparam.get_object_id('TILLS_ACCOUNT')
        dest_account = sysparam.get_object_id('IMBALANCE_ACCOUNT')

    AccountTransaction(description=till_entry.description,
                       source_account_id=source_account,
                       account_id=dest_account,
                       value=abs(till_entry.value),
                       code=str(till_entry.identifier),
                       date=TransactionTimestamp(),
                       store=store,
                       payment=till_entry.payment,
                       operation_type=operation_type)
Example #20
0
    def close_till(self):
        """This method close the current till operation with the confirmed
        sales associated. If there is a sale with a differente status than
        SALE_CONFIRMED, a new 'pending' till operation is created and
        these sales are associated with the current one.
        """

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

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

        self.final_cash_amount = self.get_balance()
        self.closing_date = TransactionTimestamp()
        self.status = Till.STATUS_CLOSED
Example #21
0
def _ensure_card_providers():
    """ Creates a list of default card providers """
    log.info("Creating Card Providers")
    from stoqlib.domain.payment.card import CreditProvider

    providers = [
        u'VISANET', u'REDECARD', u'AMEX', u'HIPERCARD', u'BANRISUL', u'PAGGO',
        u'CREDISHOP', u'CERTIF'
    ]

    store = new_store()
    for name in providers:
        provider = CreditProvider.get_provider_by_provider_id(name, store)
        if not provider.is_empty():
            continue

        CreditProvider(short_name=name,
                       provider_id=name,
                       open_contract_date=TransactionTimestamp(),
                       store=store)
    store.commit(close=True)
Example #22
0
    def open_till(self):
        """Open the till.

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

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

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

            initial_cash_amount = last_till.final_cash_amount
        else:
            initial_cash_amount = 0

        self.initial_cash_amount = initial_cash_amount

        self.opening_date = TransactionTimestamp()
        self.status = Till.STATUS_OPEN
        self.responsible_open = get_current_user(self.store)
        assert self.responsible_open is not None
        TillOpenedEvent.emit(self)
Example #23
0
def apply_patch(store):
    store.execute("""
        CREATE TABLE stock_transaction_history(
            id serial NOT NULL PRIMARY KEY,
            te_id bigint UNIQUE REFERENCES transaction_entry(id),
            date timestamp,
            stock_cost numeric(20, 8) CONSTRAINT positive_cost
                CHECK (stock_cost >= 0),
            quantity numeric(20, 3),
            type int CONSTRAINT type_range CHECK (type >= 0 and type <= 15),
            object_id bigint,
            responsible_id bigint NOT NULL REFERENCES login_user(id)
                ON UPDATE CASCADE,
            product_stock_item_id bigint NOT NULL REFERENCES product_stock_item(id)
                ON UPDATE CASCADE ON DELETE CASCADE
        );""")

    res = store.execute("""SELECT id FROM login_user WHERE
                           username='******'""").get_one()
    if not res:
        res = store.execute("""SELECT MIN(id) FROM login_user""").get_one()
    if res:
        user_id = res[0]

    # If the database is being created, there is no user and no stock items,
    # so this for will not be executed.
    for (item, sellable) in store.find((ProductStockItem, Sellable),
                                       And(ProductStockItem.storable_id == Storable.id,
                                           Storable.product_id == Product.id,
                                           Product.sellable_id == Sellable.id)):
        StockTransactionHistory(product_stock_item_id=item.id,
                                date=TransactionTimestamp(),
                                stock_cost=item.stock_cost,
                                quantity=item.quantity,
                                responsible_id=user_id,
                                type=StockTransactionHistory.TYPE_IMPORTED,
                                store=store)
Example #24
0
    def create_sale(self, id_=None, branch=None, client=None):
        from stoqlib.domain.sale import Sale
        from stoqlib.domain.till import Till
        till = Till.get_current(self.store)
        if till is None:
            till = self.create_till()
            till.open_till()
        salesperson = self.create_sales_person()
        group = self.create_payment_group()
        if client:
            group.payer = client.person

        sale = Sale(coupon_id=0,
                    open_date=TransactionTimestamp(),
                    salesperson=salesperson,
                    branch=branch or get_current_branch(self.store),
                    cfop=sysparam(self.store).DEFAULT_SALES_CFOP,
                    group=group,
                    client=client,
                    store=self.store)
        if id_:
            sale.id = id_
            sale.identifier = id_
        return sale
Example #25
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)
Example #26
0
    def create_payment(self,
                       payment_type,
                       payment_group,
                       branch,
                       value,
                       due_date=None,
                       description=None,
                       base_value=None,
                       till=ValueUnset,
                       payment_number=None):
        """Creates a new payment according to a payment method interface

        :param payment_type: the kind of payment, in or out
        :param payment_group: a :class:`PaymentGroup` subclass
        :param branch: the :class:`branch <stoqlib.domain.person.Branch>`
          associated with the payment, for incoming payments this is the
          branch receiving the payment and for outgoing payments this is the
          branch sending the payment.
        :param value: value of payment
        :param due_date: optional, due date of payment
        :param details: optional
        :param description: optional, description of the payment
        :param base_value: optional
        :param till: optional
        :param payment_number: optional
        :returns: a :class:`payment <stoqlib.domain.payment.Payment>`
        """
        store = self.store

        if due_date is None:
            due_date = TransactionTimestamp()

        if payment_type == Payment.TYPE_IN:
            query = And(Payment.group_id == payment_group.id,
                        Payment.method_id == self.id,
                        Payment.payment_type == Payment.TYPE_IN,
                        Payment.status != Payment.STATUS_CANCELLED)
            payment_count = store.find(Payment, query).count()
            if payment_count == self.max_installments:
                raise PaymentMethodError(
                    _('You can not create more inpayments for this payment '
                      'group since the maximum allowed for this payment '
                      'method is %d') % self.max_installments)
            elif payment_count > self.max_installments:
                raise DatabaseInconsistency(
                    _('You have more inpayments in database than the maximum '
                      'allowed for this payment method'))

        if not description:
            description = self.describe_payment(payment_group)

        # If till is unset, do some clever guessing
        if till is ValueUnset:
            # We only need a till for inpayments
            if payment_type == Payment.TYPE_IN:
                till = Till.get_current(store)
            elif payment_type == Payment.TYPE_OUT:
                till = None
            else:
                raise AssertionError(payment_type)

        payment = Payment(store=store,
                          branch=branch,
                          payment_type=payment_type,
                          due_date=due_date,
                          value=value,
                          base_value=base_value,
                          group=payment_group,
                          method=self,
                          category=None,
                          till=till,
                          description=description,
                          payment_number=payment_number)
        self.operation.payment_create(payment)
        return payment
Example #27
0
class Till(Domain):
    """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 = 0

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

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

    statuses = {
        STATUS_PENDING: _(u'Pending'),
        STATUS_OPEN: _(u'Opened'),
        STATUS_CLOSED: _(u'Closed')
    }

    status = IntCol(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)

    station_id = IntCol()

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

    #
    # Classmethods
    #

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

        :param store: a store
        :returns: a Till instance or None
        """
        station = get_current_station(store)
        assert station is not None

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

        return till

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

        :param store: a store
        """

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

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

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

    #
    # Till methods
    #

    def open_till(self):
        """Open the till.

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

        # 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

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

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

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

        self.final_cash_amount = self.get_balance()
        self.closing_date = TransactionTimestamp()
        self.status = Till.STATUS_CLOSED

    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:
            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

        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
        """
        from stoqlib.domain.payment.method import PaymentMethod
        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)

    #
    # Private
    #

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

    def _add_till_entry(self, value, description, payment=None):
        assert value != 0
        return TillEntry(value=value,
                         description=description,
                         payment=payment,
                         till=self,
                         branch=self.station.branch,
                         store=self.store)
Example #28
0
 def process_one(self, data, fields, store):
     CreditProvider(open_contract_date=TransactionTimestamp(),
                    short_name=data.provider_name,
                    store=store)
Example #29
0
    def create_payment(self,
                       branch,
                       station: BranchStation,
                       payment_type,
                       payment_group,
                       value,
                       due_date=None,
                       description=None,
                       base_value=None,
                       payment_number=None,
                       identifier=None,
                       ignore_max_installments=False):
        """Creates a new payment according to a payment method interface

        :param payment_type: the kind of payment, in or out
        :param payment_group: a :class:`PaymentGroup` subclass
        :param branch: the :class:`branch <stoqlib.domain.person.Branch>`
          associated with the payment, for incoming payments this is the
          branch receiving the payment and for outgoing payments this is the
          branch sending the payment.
        :param value: value of payment
        :param due_date: optional, due date of payment
        :param details: optional
        :param description: optional, description of the payment
        :param base_value: optional
        :param payment_number: optional
        :param ignore_max_installments: optional, defines whether max_installments should be
          ignored.
        :returns: a :class:`payment <stoqlib.domain.payment.Payment>`
        """
        store = self.store

        if due_date is None:
            due_date = TransactionTimestamp()

        if not ignore_max_installments and payment_type == Payment.TYPE_IN:
            query = And(Payment.group_id == payment_group.id,
                        Payment.method_id == self.id,
                        Payment.payment_type == Payment.TYPE_IN,
                        Payment.status != Payment.STATUS_CANCELLED)
            payment_count = store.find(Payment, query).count()
            if payment_count == self.max_installments:
                raise PaymentMethodError(
                    _('You can not create more inpayments for this payment '
                      'group since the maximum allowed for this payment '
                      'method is %d') % self.max_installments)
            elif payment_count > self.max_installments:
                raise DatabaseInconsistency(
                    _('You have more inpayments in database than the maximum '
                      'allowed for this payment method'))

        if not description:
            description = self.describe_payment(payment_group)

        payment = Payment(store=store,
                          branch=branch,
                          station=station,
                          identifier=identifier,
                          payment_type=payment_type,
                          due_date=due_date,
                          value=value,
                          base_value=base_value,
                          group=payment_group,
                          method=self,
                          category=None,
                          description=description,
                          payment_number=payment_number)
        self.operation.payment_create(payment)
        return payment
Example #30
0
File: till.py Project: romaia/stoq
class Till(Domain):
    """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 = 0

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

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

    statuses = {STATUS_PENDING: _(u'Pending'),
                STATUS_OPEN: _(u'Opened'),
                STATUS_CLOSED: _(u'Closed')}

    status = IntCol(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)

    station_id = IntCol()

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

    #
    # Classmethods
    #

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

        :param store: a store
        :returns: a Till instance or None
        """
        station = get_current_station(store)
        assert station is not None

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

        return till

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

        :param store: a store
        """

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

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

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

    #
    # Till methods
    #

    def open_till(self):
        """Open the till.

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

        # Make sure that the till has not been opened today
        today = datetime.date.today()
        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

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

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

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

        self.final_cash_amount = self.get_balance()
        self.closing_date = TransactionTimestamp()
        self.status = Till.STATUS_CLOSED

    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:
            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() == datetime.date.today():
            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
        """
        from stoqlib.domain.payment.method import PaymentMethod
        store = self.store
        money = PaymentMethod.get_by_name(store, u'money')

        clause = And(Or(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)

    #
    # Private
    #

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

    def _add_till_entry(self, value, description, payment=None):
        assert value != 0
        return TillEntry(value=value,
                         description=description,
                         payment=payment,
                         till=self,
                         branch=self.station.branch,
                         store=self.store)
Example #31
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):
        """Fetches the Till for the current station.

        :param store: a store
        :returns: a Till instance or None
        """
        station = get_current_station(store)
        assert station is not None

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

        return till

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

        :param store: a store
        """

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

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

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

    #
    # Till methods
    #

    def open_till(self):
        """Open the till.

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

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

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

            initial_cash_amount = last_till.final_cash_amount
        else:
            initial_cash_amount = 0

        self.initial_cash_amount = initial_cash_amount

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

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

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

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

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

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

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

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

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

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

        return self._add_till_entry(-value, reason)

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

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

        return self._add_till_entry(value, reason)

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

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

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

        return True

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

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

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

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

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

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

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

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

    # FIXME: Rename to create_day_summary
    def get_day_summary(self):
        """Get the summary of this till for closing.

        When using a blind closing process, this will create TillSummary entries that
        will save the values all payment methods used.
        """
        money_method = PaymentMethod.get_by_name(self.store, u'money')
        day_history = {}
        # Keys are (method, provider, card_type), provider and card_type may be None if
        # payment was not with card
        day_history[(money_method, None, None)] = 0

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

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

        summary = []
        for (method, provider, card_type), value in day_history.items():
            summary.append(TillSummary(till=self, method=method, provider=provider,
                                       card_type=card_type, system_value=value))
        return summary

    #
    # Private
    #

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

    def _add_till_entry(self, value, description, payment=None):
        assert value != 0
        return TillEntry(value=value,
                         description=description,
                         payment=payment,
                         till=self,
                         branch=self.station.branch,
                         store=self.store)