コード例 #1
0
class QuoteGroup(IdentifiableDomain):

    __storm_table__ = 'quote_group'

    #: 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()

    branch_id = IdCol()
    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')

    #
    # IContainer
    #

    def get_items(self):
        return self.store.find(Quotation, group=self)

    def remove_item(self, item):
        if item.group is not self:
            raise ValueError(_(u'You can not remove an item which does not '
                               u'belong to this group.'))

        order = item.purchase
        # FIXME: Bug 5581 Removing objects with synced databases is dangerous.
        # Investigate this usage
        self.store.remove(item)
        for order_item in order.get_items():
            order.remove_item(order_item)
        self.store.remove(order)

    def add_item(self, item):
        store = self.store
        return Quotation(store=store, purchase=item, group=self, branch=self.branch,
                         station=self.station)

    #
    # IDescribable
    #

    def get_description(self):
        return _(u"quote number %s") % self.identifier

    #
    # Public API
    #

    def cancel(self):
        """Cancel a quote group."""
        store = self.store
        for quote in self.get_items():
            quote.close()
            # FIXME: Bug 5581 Removing objects with synced databases is
            # dangerous. Investigate this usage
            store.remove(quote)
コード例 #2
0
class QuoteGroup(Domain):

    __storm_table__ = 'quote_group'

    #: 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()

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

    #
    # IContainer
    #

    def get_items(self):
        return self.store.find(Quotation, group=self)

    def remove_item(self, item):
        if item.group is not self:
            raise ValueError(
                _(u'You can not remove an item which does not '
                  u'belong to this group.'))

        order = item.purchase
        self.store.remove(item)
        for order_item in order.get_items():
            order.remove_item(order_item)
        self.store.remove(order)

    def add_item(self, item):
        store = self.store
        return Quotation(purchase=item,
                         group=self,
                         branch=self.branch,
                         store=store)

    #
    # IDescribable
    #

    def get_description(self):
        return _(u"quote number %s") % self.identifier

    #
    # Public API
    #

    def cancel(self):
        """Cancel a quote group."""
        store = self.store
        for quote in self.get_items():
            quote.close()
            store.remove(quote)
コード例 #3
0
class TillEntry(IdentifiableDomain):
    """A TillEntry is a representing cash added or removed in a |till|.
     * A positive value represents addition.
     * A negative value represents removal.
    """
    __storm_table__ = 'till_entry'

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

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

    #: A small string describing what was done
    description = UnicodeCol()

    #: value of transaction
    value = PriceCol()

    till_id = IdCol(allow_none=False)

    #: the |till| the entry takes part of
    till = Reference(till_id, 'Till.id')

    payment_id = IdCol(default=None)

    #: |payment| of this entry, if any
    payment = Reference(payment_id, 'Payment.id')

    branch_id = IdCol()

    #: |branch| that received or gave money
    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')

    @property
    def time(self):
        """The time of the entry

        Note that this is the same as :obj:`.date.time()`, but with
        microseconds replaced to *0*.
        """
        time = self.date.time()
        return time.replace(microsecond=0)

    @property
    def branch_name(self):
        return self.branch.get_description()
コード例 #4
0
class Quotation(IdentifiableDomain):
    __storm_table__ = 'quotation'

    #: 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()

    group_id = IdCol()
    group = Reference(group_id, 'QuoteGroup.id')
    purchase_id = IdCol()
    purchase = Reference(purchase_id, 'PurchaseOrder.id')
    branch_id = IdCol()
    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')

    def get_description(self):
        supplier = self.purchase.supplier.person.name
        return u"Group %s - %s" % (self.group.identifier, supplier)

    #
    # Public API
    #

    def close(self):
        """Closes the quotation"""
        # we don't have a specific status for closed quotes, so we just
        # cancel it
        if not self.is_closed():
            self.purchase.cancel()

    def is_closed(self):
        """Returns if the quotation is closed or not.

        :returns: True if the quotation is closed, False otherwise.
        """
        return self.purchase.status == PurchaseOrder.ORDER_CANCELLED
コード例 #5
0
ファイル: renegotiation.py プロジェクト: victornovy/stoq
class PaymentRenegotiation(Domain):
    """Class for payments renegotiations
    """

    __storm_table__ = 'payment_renegotiation'

    (STATUS_CONFIRMED, STATUS_PAID, STATUS_RENEGOTIATED) = range(3)

    statuses = collections.OrderedDict([
        (STATUS_CONFIRMED, _(u'Confirmed')),
        (STATUS_PAID, _(u'Paid')),
        (STATUS_RENEGOTIATED, _(u'Renegotiated')),
    ])

    #: 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 = IntCol(default=STATUS_CONFIRMED)
    notes = UnicodeCol(default=None)
    open_date = DateTimeCol(default_factory=localnow)
    close_date = DateTimeCol(default=None)
    discount_value = PriceCol(default=0)
    surcharge_value = PriceCol(default=0)
    total = PriceCol(default=0)
    responsible_id = IdCol()
    responsible = Reference(responsible_id, 'LoginUser.id')
    client_id = IdCol(default=None)
    client = Reference(client_id, 'Client.id')
    branch_id = IdCol(default=None)
    branch = Reference(branch_id, 'Branch.id')
    group_id = IdCol()
    group = Reference(group_id, 'PaymentGroup.id')

    #
    # Public API
    #

    def can_set_renegotiated(self):
        """Only sales with status confirmed can be renegotiated.
        :returns: True if the sale can be renegotiated, False otherwise.
        """
        # This should be as simple as:
        # return self.status == Sale.STATUS_CONFIRMED
        # But due to bug 3890 we have to check every payment.
        return any([
            payment.status == Payment.STATUS_PENDING
            for payment in self.payments
        ])

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

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

    def get_status_name(self):
        return self.statuses[self.status]

    def get_subtotal(self):
        return currency(self.total + self.discount_value -
                        self.surcharge_value)

    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

    @property
    def payments(self):
        return self.group.get_valid_payments()

    #
    #   IContainer Implementation
    #

    def add_item(self, payment):
        # TODO:
        pass

    def remove_item(self, payment):
        # TODO:
        pass

    def get_items(self):
        return self.store.find(PaymentGroup, renegotiation=self)
コード例 #6
0
ファイル: inventory.py プロジェクト: tmaxter/stoq
class Inventory(Domain):
    """ The Inventory handles the logic related to creating inventories
    for the available |product| (or a group of) in a certain |branch|.

    It has the following states:

    - STATUS_OPEN: an inventory is opened, at this point the products which
      are going to be counted (and eventually adjusted) are
      selected.
      And then, the inventory items are available for counting and
      adjustment.

    - STATUS_CLOSED: all the inventory items have been counted (and
      eventually) adjusted.

    - STATUS_CANCELLED: the process was cancelled before being finished,
      this can only happen before any items are adjusted.

    .. graphviz::

       digraph inventory_status {
         STATUS_OPEN -> STATUS_CLOSED;
         STATUS_OPEN -> STATUS_CANCELLED;
       }
    """

    __storm_table__ = 'inventory'

    #: The inventory process is open
    STATUS_OPEN = 0

    #: The inventory process is closed
    STATUS_CLOSED = 1

    #: The inventory process was cancelled, eg never finished
    STATUS_CANCELLED = 2

    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 inventory, either STATUS_OPEN, STATUS_CLOSED or
    #: STATUS_CANCELLED
    status = IntCol(default=STATUS_OPEN)

    #: number of the invoice if this inventory generated an adjustment
    invoice_number = IntCol(default=None)

    #: the date inventory process was started
    open_date = DateTimeCol(default_factory=localnow)

    #: the date inventory process was closed
    close_date = DateTimeCol(default=None)

    branch_id = IntCol()

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

    #
    # Public API
    #

    def is_open(self):
        """Returns True if the inventory process is open, False
        otherwise.
        """
        return self.status == self.STATUS_OPEN

    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

    def all_items_counted(self):
        """Returns True if all inventory items are counted, False
        otherwise.
        """
        if self.status == self.STATUS_CLOSED:
            return False

        store = self.store
        not_counted = store.find(InventoryItem, inventory=self,
                                 actual_quantity=None)
        return not_counted.count() == 0

    def get_items(self):
        """Returns all the inventory items related to this inventory

        :returns: items
        :rtype: a sequence of :class:`InventoryItem`
        """
        store = self.store
        return store.find(InventoryItem, inventory=self)

    @classmethod
    def get_open_branches(cls, store):
        """Retuns all the branches available to open the inventory
        process.

        :returns: branches
        :rtype: a sequence of :class:`Branch`
        """
        for branch in store.find(Branch):
            if not store.find(cls, branch=branch,
                              status=cls.STATUS_OPEN).one():
                yield branch

    @classmethod
    def has_open(cls, store, branch):
        """Returns if there is an inventory opened at the moment or not.

        :returns: The open inventory, if there is one. None otherwise.
        """
        return store.find(cls, status=Inventory.STATUS_OPEN,
                          branch=branch).one()

    def get_items_for_adjustment(self):
        """Returns all the inventory items that needs adjustment, that is
        the recorded quantity is different from the actual quantity.

        :returns: items
        :rtype: a sequence of :class:`InventoryItem`
        """
        query = And(InventoryItem.inventory_id == self.id,
                    InventoryItem.recorded_quantity !=
                    InventoryItem.actual_quantity,
                    Eq(InventoryItem.cfop_data_id, None),
                    InventoryItem.reason == u"")
        return self.store.find(InventoryItem, query)

    def has_adjusted_items(self):
        """Returns if we already have an item adjusted or not.

        :returns: ``True`` if there is one or more items adjusted, False
          otherwise.
        """
        query = And(InventoryItem.inventory_id == self.id,
                    Ne(InventoryItem.cfop_data_id, None),
                    InventoryItem.reason != u"")
        return not self.store.find(InventoryItem, query).is_empty()

    def cancel(self):
        """Cancel this inventory. Notice that, to cancel an inventory no
        products should have been adjusted.
        """
        if not self.is_open():
            raise AssertionError(
                "You can't cancel an inventory that is not opened!")

        if self.has_adjusted_items():
            raise AssertionError(
                "You can't cancel an inventory that has adjusted items!")

        self.status = Inventory.STATUS_CANCELLED

    def get_status_str(self):
        return self.statuses[self.status]
コード例 #7
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
コード例 #8
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)
コード例 #9
0
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>`__
    """

    implements(IContainer)

    __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 = 0

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

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

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

    removed_by = UnicodeCol(default=u'')

    #: branch where the loan was done
    branch_id = IntCol()
    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 = IntCol()
    responsible = Reference(responsible_id, 'LoginUser.id')

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

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

    #
    # 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):
        LoanItem.delete(loan_item.id, store=self.store)

    #
    # 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
        return LoanItem(store=self.store,
                        quantity=quantity,
                        loan=self,
                        sellable=sellable,
                        batch=batch,
                        price=price)

    #
    # 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.person.name
        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():
            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 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
コード例 #10
0
ファイル: stockdecrease.py プロジェクト: rosalin/stoq
class StockDecrease(Domain):
    """Stock Decrease object implementation.

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

    __storm_table__ = 'stock_decrease'

    #: Stock Decrease is still being edited
    STATUS_INITIAL = 0

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

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

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

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

    reason = UnicodeCol(default=u'')

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

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

    responsible_id = IdCol()

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

    removed_by_id = IdCol()

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

    branch_id = IdCol()

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

    cfop_id = IdCol()

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

    group_id = IdCol()

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

    cost_center_id = IdCol()

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

    #
    # Classmethods
    #

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

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

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

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

    # Status

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

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

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

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

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

        self.status = StockDecrease.STATUS_CONFIRMED

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

    #
    # Accessors
    #

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

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

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

        return self.removed_by.get_description()

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

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

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

    # Other methods

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

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

        return StockDecreaseItem(store=self.store,
                                 quantity=quantity,
                                 stock_decrease=self,
                                 sellable=sellable,
                                 batch=batch,
                                 cost=cost)
コード例 #11
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)
コード例 #12
0
ファイル: stockdecrease.py プロジェクト: lucaslamounier/stoq
class StockDecrease(Domain):
    """Stock Decrease object implementation.

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

    __storm_table__ = 'stock_decrease'

    #: Stock Decrease is still being edited
    STATUS_INITIAL = u'initial'

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

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

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

    #: status of the sale
    status = EnumCol(allow_none=False, default=STATUS_INITIAL)

    reason = UnicodeCol(default=u'')

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

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

    responsible_id = IdCol()

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

    removed_by_id = IdCol()

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

    branch_id = IdCol()

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

    #: person who is receiving
    person_id = IdCol()

    person = Reference(person_id, 'Person.id')

    #: the choosen CFOP
    cfop_id = IdCol()

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

    #: the payment group related to this stock decrease
    group_id = IdCol()

    group = Reference(group_id, 'PaymentGroup.id')

    cost_center_id = IdCol()

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

    #: |transporter| used in stock decrease
    transporter = None

    invoice_id = IdCol()

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

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

    #
    # IInvoice implementation
    #

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

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

    @property
    def invoice_subtotal(self):
        return currency(self.get_total_cost())

    @property
    def invoice_total(self):
        return currency(self.get_total_cost())

    @property
    def payments(self):
        if self.group:
            return self.group.get_valid_payments().order_by(Payment.open_date)
        return None

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

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

    #
    # Classmethods
    #

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

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

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

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

    # Status

    def can_confirm(self):
        """Only stock decreases with status equal to INITIAL can be confirmed

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

    def confirm(self):
        """Confirms the stock decrease

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

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

        old_status = self.status
        self.status = StockDecrease.STATUS_CONFIRMED

        # Save the operation_nature and branch in Invoice Table
        self.invoice.operation_nature = self.operation_nature
        self.invoice.branch = branch

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

        StockOperationConfirmedEvent.emit(self, old_status)

    #
    # Accessors
    #

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

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

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

        return self.removed_by.get_description()

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

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

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

    # Other methods

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

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

        return StockDecreaseItem(store=self.store,
                                 quantity=quantity,
                                 stock_decrease=self,
                                 sellable=sellable,
                                 batch=batch,
                                 cost=cost)
コード例 #13
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
コード例 #14
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)
コード例 #15
0
class ProductionOrder(Domain):
    """Production Order object implementation.

    :cvar ORDER_OPENED: The production order is opened, production items might
                        have been added.
    :cvar ORDER_WAITING: The production order is waiting some conditions to
                         start the manufacturing process.
    :cvar ORDER_PRODUCING: The production order have already started.
    :cvar ORDER_CLOSED: The production have finished.

    :attribute status: the production order status
    :attribute open_date: the date when the production order was created
    :attribute close_date: the date when the production order have been closed
    :attribute description: the production order description
    :attribute responsible: the person responsible for the production order
    """

    __storm_table__ = 'production_order'

    (ORDER_OPENED, ORDER_WAITING, ORDER_PRODUCING, ORDER_CLOSED,
     ORDER_QA) = range(5)

    statuses = {
        ORDER_OPENED: _(u'Opened'),
        ORDER_WAITING: _(u'Waiting'),
        ORDER_PRODUCING: _(u'Producing'),
        ORDER_CLOSED: _(u'Closed'),
        ORDER_QA: _(u'Quality Assurance'),
    }

    #: 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 = IntCol(default=ORDER_OPENED)
    open_date = DateTimeCol(default_factory=localnow)
    expected_start_date = DateTimeCol(default=None)
    start_date = DateTimeCol(default=None)
    close_date = DateTimeCol(default=None)
    description = UnicodeCol(default=u'')
    responsible_id = IdCol(default=None)
    responsible = Reference(responsible_id, 'Employee.id')
    branch_id = IdCol()
    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.'))
        self.store.remove(item)

    #
    # Public API
    #

    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.'))
        self.store.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

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

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

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

    # FIXME: Test
    def try_finalize_production(self):
        """When all items are completely produced, change the status of the
        production to CLOSED.
        """
        assert (self.status == ProductionOrder.ORDER_PRODUCING
                or self.status == ProductionOrder.ORDER_QA), self.status

        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.person.name

    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
コード例 #16
0
class ReceivingInvoice(IdentifiableDomain):

    __storm_table__ = 'receiving_invoice'

    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()

    #: 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)
    icms_st_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=0)

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

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

    branch_id = IdCol()
    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')

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

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

    group_id = IdCol()
    group = Reference(group_id, 'PaymentGroup.id')

    receiving_orders = ReferenceSet('id',
                                    'ReceivingOrder.receiving_invoice_id')

    @classmethod
    def check_unique_invoice_number(cls, store, invoice_number, supplier):
        count = store.find(
            cls,
            And(cls.invoice_number == invoice_number,
                ReceivingInvoice.supplier == supplier)).count()
        return count == 0

    @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
        if self.ipi_total:
            total_surcharge += self.ipi_total
        if self.icms_st_total:
            total_surcharge += self.icms_st_total

        for receiving in self.receiving_orders:
            total_surcharge += receiving.total_surcharges

        # 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_discounts(self):
        """Returns the sum of all discounts (purchase & receiving)"""
        total_discount = 0
        if self.discount_value:
            total_discount += self.discount_value

        for receiving in self.receiving_orders:
            total_discount += receiving.total_discounts

        return currency(total_discount)

    @property
    def products_total(self):
        return currency(
            sum((r.products_total for r in self.receiving_orders), 0))

    @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)

    @property
    def total_for_payment(self):
        """Fetch the total for the invoice payment. Exclude the freight value if
        it will be in a diferent pament
        """
        total = self.total
        if self.freight_type == self.FREIGHT_FOB_PAYMENT:
            total -= self.freight_total
        return currency(total)

    @property
    def payments(self):
        """Returns all valid payments for this invoice

        This will return a list of valid payments for this invoice, that
        is, all payments on the payment group that were not cancelled.
        If you need to get the cancelled too, use self.group.payments.

        :returns: a list of |payment|
        """
        return self.group.get_valid_payments()

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

    @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 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)

    def create_freight_payment(self, group=None):
        store = self.store
        money_method = PaymentMethod.get_by_name(store, u'money')
        # If we have a transporter, the freight payment will be for him
        if not group:
            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(self.branch,
                                              self.station,
                                              Payment.TYPE_OUT,
                                              group,
                                              self.freight_total,
                                              due_date=localnow(),
                                              description=description)
        payment.set_pending()
        return payment

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

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

        return freight_type

    def confirm(self, user: LoginUser):
        self.invoice_total = self.total
        if self.group:
            self.group.confirm()
        for receiving in self.receiving_orders:
            receiving.invoice_number = self.invoice_number

        # 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 = self.group or self.get_purchase_orders().pop().group
        FiscalBookEntry.create_product_entry(self.store, self.branch, user,
                                             group, receiving.cfop,
                                             self.invoice_number,
                                             self.icms_total, self.ipi_total)

    def add_receiving(self, receiving):
        receiving.receiving_invoice = self

    def get_purchase_orders(self):
        purchases = set()
        for receiving in self.receiving_orders:
            purchases.update(set(receiving.purchase_orders))
        return purchases

    def _get_percentage_value(self, percentage):
        if not percentage:
            return currency(0)
        subtotal = self.products_total
        percentage = Decimal(percentage)
        return subtotal * (percentage / 100)
コード例 #17
0
class ReceivingOrder(IdentifiableDomain):
    """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'

    #: 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'')

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

    # Número do Romaneio. The number used by the transporter to identify the packing
    packing_number = UnicodeCol()

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

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

    branch_id = IdCol()
    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')

    receiving_invoice_id = IdCol(default=None)
    receiving_invoice = Reference(receiving_invoice_id, 'ReceivingInvoice.id')

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

    def __init__(self, store=None, **kw):
        super(ReceivingOrder, self).__init__(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)
        if not 'cfop' in kw:
            self.cfop = sysparam.get_object(store, 'DEFAULT_RECEIVING_CFOP')

    #
    #  Public API
    #

    def confirm(self, user: LoginUser):
        if self.receiving_invoice:
            self.receiving_invoice.confirm(user)

        for item in self.get_items():
            item.add_stock_items(user)

        purchases = list(self.purchase_orders)
        for purchase in purchases:
            if purchase.can_close():
                purchase.close()

        # XXX: Will the packing number aways be the same as the suppliert order?
        if purchase.work_order:
            self.packing_number = purchase.work_order.supplier_order

    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,
                          ipi_value=0,
                          icms_st_value=0):
        """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,
                                  ipi_value=ipi_value,
                                  icms_st_value=icms_st_value,
                                  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.
        """
        # If the invoice has more than one receiving, the values could be inconsistent
        assert self.receiving_invoice.receiving_orders.count() == 1
        difference = self.receiving_invoice.total - self.receiving_invoice.products_total
        if create_freight_payment:
            difference -= self.receiving_invoice.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.receiving_invoice.freight_total and create_freight_payment:
            purchases = list(self.purchase_orders)
            if len(purchases
                   ) == 1 and self.receiving_invoice.transporter is None:
                group = purchases[0].group
            else:
                group = None
            self.receiving_invoice.create_freight_payment(group=group)

    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):
        if self.receiving_invoice and self.receiving_invoice.group:
            return self.receiving_invoice.payments

        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)

    #
    # Accessors
    #

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

    @property
    def freight_type(self):
        if self.receiving_invoice:
            return self.receiving_invoice.freight_type
        return None

    @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_received_total() for item in self.get_items()),
                    currency(0))
        return currency(total)

    @property
    def product_total_with_ipi(self):
        total = sum((item.get_received_total(with_ipi=True)
                     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
        for purchase in self.purchase_orders:
            total_surcharge += purchase.surcharge_value
        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
        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 purchase order
        """
        total = self.product_total_with_ipi
        total -= self.total_discounts
        total += self.total_surcharges

        return currency(total)
コード例 #18
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.
    """

    implements(IContainer)

    __storm_table__ = 'returned_sale'

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

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

    #: the invoice number for this returning
    invoice_number = IntCol(default=None)

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

    sale_id = IntCol(default=None)

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

    new_sale_id = IntCol(default=None)

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

    responsible_id = IntCol()

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

    branch_id = IntCol()

    #: 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')

    @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)

        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.delete(item.id, store=self.store)

    #
    #  Public API
    #

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

        :param str 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
            store = self.store
            group = self.group
            for payment in [
                    p for p in group.get_pending_payments()
                    if p.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(store, method_name)
            description = _(u'%s returned for sale %s') % (
                method.operation.description, self.sale.identifier)
            value = self.total_amount_abs
            payment = method.create_payment(Payment.TYPE_OUT,
                                            group,
                                            self.branch,
                                            value,
                                            description=description)
            payment.set_pending()
            if method_name == u'credit':
                payment.pay()

        self._return_sale(payment)

    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
        payment = method.create_payment(Payment.TYPE_IN,
                                        group,
                                        self.branch,
                                        value,
                                        description=description)
        payment.set_pending()
        payment.pay()

        self._return_sale(payment)

    def remove(self):
        """Remove this return and it's items from the database"""
        for item in self.get_items():
            self.remove_item(item)
        self.delete(self.id, store=self.store)

    #
    #  Private
    #

    def _get_returned_percentage(self):
        return decimal.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 _return_sale(self, payment):
        # We must have at least one item to return
        assert self.returned_items.count()

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

        if self.sale:
            # 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_commission(payment)
            self._revert_fiscal_entry()
            self.sale.return_(self)

    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)

    def _revert_commission(self, payment):
        from stoqlib.domain.commission import Commission
        store = self.store
        old_commissions = store.find(Commission, sale=self.sale)
        old_commissions_total = old_commissions.sum(Commission.value)
        if old_commissions_total <= 0:
            # Comission total should not be negative
            return

        # old_commissions_paid, unlike old_commissions_total, contains the
        # total positive generated commission, so we can revert it partially
        old_commissions_paid = old_commissions.find(Commission.value >= 0).sum(
            Commission.value)
        value = old_commissions_paid * self._get_returned_percentage()
        assert old_commissions_total - value >= 0

        Commission(
            store=store,
            commission_type=old_commissions[0].commission_type,
            sale=self.sale,
            payment=payment,
            salesperson=self.sale.salesperson,
            # Generate a negative commission to compensate the returned items
            value=-value,
        )
コード例 #19
0
class PurchaseOrder(Domain, Adaptable):
    """Purchase and order definition."""

    __storm_table__ = 'purchase_order'

    (ORDER_CANCELLED, ORDER_QUOTING, ORDER_PENDING, ORDER_CONFIRMED,
     ORDER_CLOSED, ORDER_CONSIGNED) = range(6)

    statuses = {
        ORDER_CANCELLED: _(u'Cancelled'),
        ORDER_QUOTING: _(u'Quoting'),
        ORDER_PENDING: _(u'Pending'),
        ORDER_CONFIRMED: _(u'Confirmed'),
        ORDER_CLOSED: _(u'Closed'),
        ORDER_CONSIGNED: _(u'Consigned')
    }

    (FREIGHT_FOB, FREIGHT_CIF) = range(2)

    freight_types = {FREIGHT_FOB: _(u'FOB'), FREIGHT_CIF: _(u'CIF')}

    #: 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 = IntCol(default=ORDER_QUOTING)
    open_date = DateTimeCol(default_factory=localnow)
    quote_deadline = DateTimeCol(default=None)
    expected_receival_date = DateTimeCol(default_factory=localnow)
    expected_pay_date = DateTimeCol(default_factory=localnow)
    receival_date = DateTimeCol(default=None)
    confirm_date = DateTimeCol(default=None)
    notes = UnicodeCol(default=u'')
    salesperson_name = UnicodeCol(default=u'')
    freight_type = IntCol(default=FREIGHT_FOB)
    expected_freight = PriceCol(default=0)
    surcharge_value = PriceCol(default=0)
    discount_value = PriceCol(default=0)
    consigned = BoolCol(default=False)
    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')
    responsible_id = IdCol()
    responsible = Reference(responsible_id, 'LoginUser.id')
    group_id = IdCol()
    group = Reference(group_id, 'PaymentGroup.id')

    def __init__(self, **kwargs):
        super(PurchaseOrder, self).__init__(**kwargs)
        self.addFacet(IPaymentTransaction)

    def __storm_loaded__(self):
        super(PurchaseOrder, self).__storm_loaded__()
        self.addFacet(IPaymentTransaction)

    #
    # IContainer Implementation
    #

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

    def remove_item(self, item):
        if item.order is not self:
            raise ValueError(
                _(u'Argument item must have an order attribute '
                  'associated with the current purchase instance'))
        self.store.remove(item)

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

    #
    # Properties
    #

    def _set_discount_by_percentage(self, value):
        """Sets a 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)

    def _get_discount_by_percentage(self):
        discount_value = self.discount_value
        if not discount_value:
            return currency(0)
        subtotal = self.get_purchase_subtotal()
        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 = property(_get_discount_by_percentage,
                                   _set_discount_by_percentage)

    def _set_surcharge_by_percentage(self, value):
        """Sets a 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 %"""
        self.surcharge_value = self._get_percentage_value(value)

    def _get_surcharge_by_percentage(self):
        surcharge_value = self.surcharge_value
        if not surcharge_value:
            return currency(0)
        subtotal = self.get_purchase_subtotal()
        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 = property(_get_surcharge_by_percentage,
                                    _set_surcharge_by_percentage)

    @property
    def payments(self):
        """Returns all valid payments for this purchase

        This will return a list of valid payments for this purchase, that
        is, all payments on the payment group that were not cancelled.
        If you need to get the cancelled too, use self.group.payments.

        :returns: a list of |payment|
        """
        return self.group.get_valid_payments()

    #
    # Private
    #

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

    #
    # Public API
    #

    def is_paid(self):
        for payment in self.payments:
            if not payment.is_paid():
                return False
        return True

    def can_cancel(self):
        """Find out if it's possible to cancel the order

        :returns: True if it's possible to cancel the order, otherwise False
        """
        # FIXME: Canceling partial orders disabled until we fix bug 3282
        for item in self.get_items():
            if item.has_partial_received():
                return False
        return self.status in [
            self.ORDER_QUOTING, self.ORDER_PENDING, self.ORDER_CONFIRMED
        ]

    def can_close(self):
        """Find out if it's possible to close the order

        :returns: True if it's possible to close the order, otherwise False
        """

        # Consigned orders can be closed only after being confirmed
        if self.status == self.ORDER_CONSIGNED:
            return False

        for item in self.get_items():
            if not item.has_been_received():
                return False
        return True

    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.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(
            self.store, 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))

    def set_consigned(self):
        if self.status != PurchaseOrder.ORDER_PENDING:
            raise ValueError(
                _(u'Invalid order status, it should be '
                  u'ORDER_PENDING, got %s') % (self.get_status_str(), ))

        self.responsible = get_current_user(self.store)
        self.status = PurchaseOrder.ORDER_CONSIGNED

    def close(self):
        """Closes the purchase order
        """
        if self.status != PurchaseOrder.ORDER_CONFIRMED:
            raise ValueError(
                _(u'Invalid status, it should be confirmed '
                  u'got %s instead') % self.get_status_str())
        self.status = self.ORDER_CLOSED

        Event.log(
            self.store, Event.TYPE_ORDER,
            _(u"Order %s, total value %2.2f, supplier '%s' "
              u"is now closed") % (self.identifier, self.get_purchase_total(),
                                   self.supplier.person.name))

    def cancel(self):
        """Cancels the purchase order
        """
        assert self.can_cancel()

        # we have to cancel the payments too
        transaction = IPaymentTransaction(self)
        transaction.cancel()

        self.status = self.ORDER_CANCELLED

    def receive_item(self, item, quantity_to_receive):
        if not item in self.get_pending_items():
            raise StoqlibError(
                _(u'This item is not pending, hence '
                  u'cannot be received'))
        quantity = item.quantity - item.quantity_received
        if quantity < quantity_to_receive:
            raise StoqlibError(
                _(u'The quantity that you want to receive '
                  u'is greater than the total quantity of '
                  u'this item %r') % item)
        self.increase_quantity_received(item, quantity_to_receive)

    def increase_quantity_received(self, purchase_item, quantity_received):
        sellable = purchase_item.sellable
        items = [
            item for item in self.get_items()
            if item.sellable.id == sellable.id
        ]
        qty = len(items)
        if not qty:
            raise ValueError(
                _(u'There is no purchase item for '
                  u'sellable %r') % sellable)

        purchase_item.quantity_received += quantity_received

    def get_status_str(self):
        return PurchaseOrder.translate_status(self.status)

    def get_freight_type_name(self):
        if not self.freight_type in self.freight_types.keys():
            raise DatabaseInconsistency(
                _(u'Invalid freight_type, got %d') % self.freight_type)
        return self.freight_types[self.freight_type]

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

    def get_supplier_name(self):
        return self.supplier.get_description()

    def get_transporter_name(self):
        if not self.transporter:
            return u""
        return self.transporter.get_description()

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

    def get_purchase_subtotal(self):
        """Get the subtotal of the purchase.
        The sum of all the items cost * items quantity
        """
        return currency(
            self.get_items().sum(PurchaseItem.cost * PurchaseItem.quantity)
            or 0)

    def get_purchase_total(self):
        subtotal = self.get_purchase_subtotal()
        total = subtotal - self.discount_value + self.surcharge_value
        if total < 0:
            raise ValueError(_(u'Purchase total can not be lesser than zero'))
        # XXX: Since the purchase_total value must have two digits
        # (at the moment) we need to format the value to a 2-digit number and
        # then convert it to currency data type, because the subtotal value
        # may return a 3-or-more-digit value, depending on COST_PRECISION_DIGITS
        # parameters.
        return currency(get_formatted_price(total))

    def get_received_total(self):
        """Like {get_purchase_subtotal} but only takes into account the
        received items
        """
        return currency(self.get_items().sum(
            PurchaseItem.cost * PurchaseItem.quantity_received) or 0)

    def get_remaining_total(self):
        """The total value to be paid for the items not received yet
        """
        return self.get_purchase_total() - self.get_received_total()

    def get_pending_items(self):
        """
        Returns a sequence of all items which we haven't received yet.
        """
        return self.get_items().find(
            PurchaseItem.quantity_received < PurchaseItem.quantity)

    def get_partially_received_items(self):
        """
        Returns a sequence of all items which are partially received.
        """
        return self.get_items().find(PurchaseItem.quantity_received > 0)

    def get_open_date_as_string(self):
        return self.open_date and self.open_date.strftime("%x") or u""

    def get_quote_deadline_as_string(self):
        return self.quote_deadline and self.quote_deadline.strftime(
            "%x") or u""

    def get_receiving_orders(self):
        """Returns all ReceivingOrder related to this purchase order
        """
        from stoqlib.domain.receiving import ReceivingOrder
        return self.store.find(ReceivingOrder, purchase=self)

    def get_data_for_labels(self):
        """ This function returns some necessary data to print the purchase's
        items labels
        """
        for purchase_item in self.get_items():
            sellable = purchase_item.sellable
            label_data = Settable(barcode=sellable.barcode,
                                  code=sellable.code,
                                  description=sellable.description,
                                  price=sellable.price,
                                  quantity=purchase_item.quantity)
            yield label_data

    def has_batch_item(self):
        """Fetch the storables from this purchase order and returns ``True`` if
        any of them is a batch storable.

        :returns: ``True`` if this purchase order has batch items, ``False`` if
        it doesn't.
        """
        return not self.store.find(
            Storable,
            And(self.id == PurchaseOrder.id, PurchaseOrder.id
                == PurchaseItem.order_id, PurchaseItem.sellable_id
                == Sellable.id, Sellable.id
                == Product.sellable_id, Product.id == Storable.product_id,
                Eq(Storable.is_batch, True))).is_empty()

    #
    # Classmethods
    #

    @classmethod
    def translate_status(cls, status):
        if not status in cls.statuses:
            raise DatabaseInconsistency(
                _(u'Got an unexpected status value: '
                  u'%s') % status)
        return cls.statuses[status]
コード例 #20
0
ファイル: transfer.py プロジェクト: esosaja/stoq
class TransferOrder(Domain):
    """ Transfer Order class
    """
    __storm_table__ = 'transfer_order'

    STATUS_PENDING = u'pending'
    STATUS_SENT = u'sent'
    STATUS_RECEIVED = u'received'

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

    status = EnumCol(default=STATUS_PENDING)

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

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

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

    #: The invoice number of the transfer
    invoice_number = IntCol()

    #: Comments of a transfer
    comments = UnicodeCol()

    source_branch_id = IdCol()

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

    destination_branch_id = IdCol()

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

    source_responsible_id = IdCol()

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

    destination_responsible_id = IdCol()

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

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

    #: |transporter| used in transfer
    transporter = None

    invoice_id = IdCol()

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

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

    #
    # IContainer implementation
    #

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

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

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

    #
    # IInvoice implementation
    #

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

    @property
    def invoice_subtotal(self):
        subtotal = self.get_items().sum(TransferOrderItem.quantity *
                                        TransferOrderItem.stock_cost)
        return currency(subtotal)

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

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

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

    #
    # Public API
    #

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

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

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

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

        self.validate_batch(batch, sellable=sellable)

        product = sellable.product
        if product.manage_stock:
            stock_item = product.storable.get_stock_item(
                self.source_branch, batch)
            stock_cost = stock_item.stock_cost
        else:
            stock_cost = sellable.cost

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

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

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

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

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

        # 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

        self.status = self.STATUS_SENT

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

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

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

    @classmethod
    def get_pending_transfers(cls, store, branch):
        """Get all the transfers that need to be recieved

        Get all transfers that have STATUS_SENT and the current branch as the destination
        This is useful if you want to list all the items that need to be
        recieved in a certain branch
        """
        return store.find(
            cls,
            And(cls.status == cls.STATUS_SENT,
                cls.destination_branch == branch))

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

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

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

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

        return self.destination_responsible.person.name

    def get_total_items_transfer(self):
        """Retuns the |transferitems| quantity
        """
        return sum([item.quantity for item in self.get_items()], 0)
コード例 #21
0
ファイル: receiving.py プロジェクト: rosalin/stoq
class ReceivingOrder(Domain):
    """Receiving order definition.
    """

    __storm_table__ = 'receiving_order'

    #: Products in the order was not received or received partially.
    STATUS_PENDING = 0

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

    (FREIGHT_FOB_PAYMENT, FREIGHT_FOB_INSTALLMENTS, FREIGHT_CIF_UNKNOWN,
     FREIGHT_CIF_INVOICE) = range(4)

    freight_types = {
        FREIGHT_FOB_PAYMENT: _(u"FOB - Freight value "
                               u"on a new payment"),
        FREIGHT_FOB_INSTALLMENTS: _(u"FOB - Freight value "
                                    u"on installments"),
        FREIGHT_CIF_UNKNOWN: _(u"CIF - Freight value is unknown"),
        FREIGHT_CIF_INVOICE: _(u"CIF - Freight value highlighted "
                               u"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 = IntCol(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 = IntCol(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 number of the order that has been received.
    invoice_number = IntCol()
    invoice_total = PriceCol(default=None)
    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')
    purchase_id = IdCol()
    purchase = Reference(purchase_id, 'PurchaseOrder.id')
    transporter_id = IdCol(default=None)
    transporter = Reference(transporter_id, 'Transporter.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.purchase = kw.pop('purchase', None)
        self.supplier = kw.pop('supplier', None)
        if not 'cfop' in kw:
            self.cfop = sysparam(store).DEFAULT_RECEIVING_CFOP

    #
    #  Public API
    #

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

        FiscalBookEntry.create_product_entry(self.store, self.purchase.group,
                                             self.cfop, self.invoice_number,
                                             self.icms_total, self.ipi_total)
        self.invoice_total = self.get_total()
        if self.purchase.can_close():
            self.purchase.close()

    def add_purchase_item(self, item, quantity=None, batch_number=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 item.order != self.purchase:
            raise ValueError("The purchase item must be on the same purchase "
                             "of this receiving")
        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)

    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.
        """
        group = self.purchase.group
        difference = self.get_total() - self.get_products_total()
        if create_freight_payment:
            difference -= self.freight_total

        if difference != 0:
            payments = group.get_pending_payments()
            payments_number = payments.count()
            if payments_number > 0:
                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).
        if self.transporter is not None:
            group = PaymentGroup(store=store)
            group.recipient = self.transporter.person
        else:
            group = self.purchase.group

        description = _(u'Freight for purchase %s') % (
            self.purchase.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):
        store = self.store
        return store.find(ReceivingOrderItem, receiving_order=self)

    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)

    #
    # Properties
    #

    @property
    def group(self):
        return self.purchase.group

    @property
    def payments(self):
        return self.group.payments

    #
    # Accessors
    #

    def get_cfop_code(self):
        return self.cfop.code.encode()

    def get_transporter_name(self):
        if not self.transporter:
            return u""
        return self.transporter.get_description()

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

    def get_supplier_name(self):
        if not self.supplier:
            return u""
        return self.supplier.get_description()

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

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

    def get_receival_date_str(self):
        return self.receival_date.strftime("%x")

    def _get_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

        if self.purchase.surcharge_value:
            total_surcharge += self.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)

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

        if self.purchase.discount_value:
            total_discount += self.purchase.discount_value

        return currency(total_discount)

    def get_total(self):
        """Fetch the total, including discount and surcharge for both the
        purchase order and the receiving order.
        """

        total = self.get_products_total()
        total -= self._get_total_discounts()
        total += self._get_total_surcharges()

        return currency(total)

    def guess_freight_type(self):
        """Returns a freight_type based on the purchase's freight_type"""
        if self.purchase.freight_type == PurchaseOrder.FREIGHT_FOB:
            if self.purchase.is_paid():
                freight_type = ReceivingOrder.FREIGHT_FOB_PAYMENT
            else:
                freight_type = ReceivingOrder.FREIGHT_FOB_INSTALLMENTS
        elif self.purchase.freight_type == PurchaseOrder.FREIGHT_CIF:
            if not self.purchase.expected_freight:
                freight_type = ReceivingOrder.FREIGHT_CIF_UNKNOWN
            else:
                freight_type = ReceivingOrder.FREIGHT_CIF_INVOICE

        return freight_type

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

    def _set_discount_by_percentage(self, value):
        """Sets a 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)

    def _get_discount_by_percentage(self):
        discount_value = self.discount_value
        if not discount_value:
            return currency(0)
        subtotal = self.get_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 = property(_get_discount_by_percentage,
                                   _set_discount_by_percentage)

    def _set_surcharge_by_percentage(self, value):
        """Sets a 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 %
        """
        self.surcharge_value = self._get_percentage_value(value)

    def _get_surcharge_by_percentage(self):
        surcharge_value = self.surcharge_value
        if not surcharge_value:
            return currency(0)
        subtotal = self.get_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 = property(_get_surcharge_by_percentage,
                                    _set_surcharge_by_percentage)
コード例 #22
0
ファイル: purchase.py プロジェクト: pjamil/stoq
class PurchaseOrder(Domain):
    """Purchase and order definition."""

    __storm_table__ = 'purchase_order'

    ORDER_QUOTING = u'quoting'
    ORDER_PENDING = u'pending'
    ORDER_CONFIRMED = u'confirmed'
    ORDER_CONSIGNED = u'consigned'
    ORDER_CANCELLED = u'cancelled'
    ORDER_CLOSED = u'closed'

    statuses = collections.OrderedDict([
        (ORDER_QUOTING, _(u'Quoting')),
        (ORDER_PENDING, _(u'Pending')),
        (ORDER_CONFIRMED, _(u'Confirmed')),
        (ORDER_CONSIGNED, _(u'Consigned')),
        (ORDER_CANCELLED, _(u'Cancelled')),
        (ORDER_CLOSED, _(u'Closed')),
    ])

    FREIGHT_FOB = u'fob'
    FREIGHT_CIF = u'cif'

    freight_types = {FREIGHT_FOB: _(u'FOB'),
                     FREIGHT_CIF: _(u'CIF')}

    #: 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 = EnumCol(allow_none=False, default=ORDER_QUOTING)
    open_date = DateTimeCol(default_factory=localnow, allow_none=False)
    quote_deadline = DateTimeCol(default=None)
    expected_receival_date = DateTimeCol(default_factory=localnow)
    expected_pay_date = DateTimeCol(default_factory=localnow)
    # XXX This column is not being used anywhere
    receival_date = DateTimeCol(default=None)
    confirm_date = DateTimeCol(default=None)
    notes = UnicodeCol(default=u'')
    salesperson_name = UnicodeCol(default=u'')
    freight_type = EnumCol(allow_none=False, default=FREIGHT_FOB)
    expected_freight = PriceCol(default=0)

    surcharge_value = PriceCol(default=0)
    discount_value = PriceCol(default=0)

    consigned = BoolCol(default=False)
    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')
    responsible_id = IdCol()
    responsible = Reference(responsible_id, 'LoginUser.id')
    group_id = IdCol()
    group = Reference(group_id, 'PaymentGroup.id')

    #: Indicates if the order is from a work order
    work_order_id = IdCol()
    work_order = Reference(work_order_id, 'WorkOrder.id')

    #
    # IContainer Implementation
    #

    def get_items(self, with_children=True):
        """Get the items of the purchase order

        :param with_children: indicate if we should fetch children_items or not
        """
        query = PurchaseItem.order == self
        if not with_children:
            query = And(query, Eq(PurchaseItem.parent_item_id, None))
        return self.store.find(PurchaseItem, query)

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

    def add_item(self, sellable, quantity=Decimal(1), parent=None, cost=None,
                 icms_st_value=0, ipi_value=0):
        """Add a sellable to this purchase.

        If the sellable is part of a package (parent is not None), then the actual cost
        and quantity will be calculated based on how many items of this component is on
        the package.

        :param sellable: the sellable being added
        :param quantity: How many units of this sellable we are adding
        :param cost: The price being paid for this sellable
        :param parent: The parent of this sellable, incase of a package
        """
        if cost is None:
            cost = sellable.cost

        if parent:
            component = parent.sellable.product.get_component(sellable)
            cost = cost / component.quantity
            quantity = quantity * component.quantity
        else:
            if sellable.product.is_package:
                # If this is a package, the cost will be calculated and updated by the
                # compoents of the package
                cost = Decimal('0')

        store = self.store
        return PurchaseItem(store=store, order=self,
                            sellable=sellable, quantity=quantity, cost=cost,
                            parent_item=parent, icms_st_value=icms_st_value,
                            ipi_value=ipi_value)

    #
    # Properties
    #

    @property
    def discount_percentage(self):
        """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 %"""
        discount_value = self.discount_value
        if not discount_value:
            return currency(0)
        subtotal = self.purchase_subtotal
        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):
        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.purchase_subtotal
        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)

    @property
    def payments(self):
        """Returns all valid payments for this purchase

        This will return a list of valid payments for this purchase, that
        is, all payments on the payment group that were not cancelled.
        If you need to get the cancelled too, use self.group.payments. If this
        purchase does not have a payment group, return a empty list.

        :returns: a list of |payment|
        """
        return self.group.get_valid_payments() if self.group else []

    #
    # Private
    #

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

    def _payback_paid_payments(self):
        paid_value = self.group.get_total_paid()

        # If we didn't pay anything yet, there is no need to create a payback.
        if not paid_value:
            return

        money = PaymentMethod.get_by_name(self.store, u'money')
        payment = money.create_payment(
            Payment.TYPE_IN, self.group, self.branch,
            paid_value, description=_(u'%s Money Returned for Purchase %s') % (
                u'1/1', self.identifier))
        payment.set_pending()
        payment.pay()

    #
    # Public API
    #

    def is_paid(self):
        if not self.group:
            return False

        for payment in self.payments:
            if not payment.is_paid():
                return False
        return True

    def can_cancel(self):
        """Find out if it's possible to cancel the order

        :returns: True if it's possible to cancel the order, otherwise False
        """
        # FIXME: Canceling partial orders disabled until we fix bug 3282
        for item in self.get_items():
            if item.has_partial_received():
                return False
        return self.status in [self.ORDER_QUOTING,
                               self.ORDER_PENDING,
                               self.ORDER_CONFIRMED]

    def can_close(self):
        """Find out if it's possible to close the order

        :returns: True if it's possible to close the order, otherwise False
        """

        # Consigned orders can be closed only after being confirmed
        if self.status == self.ORDER_CONSIGNED:
            return False

        for item in self.get_items():
            if not item.has_been_received():
                return False
        return True

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

    def set_consigned(self):
        if self.status != PurchaseOrder.ORDER_PENDING:
            raise ValueError(
                _(u'Invalid order status, it should be '
                  u'ORDER_PENDING, got %s') % (self.status_str, ))

        self.responsible = get_current_user(self.store)
        self.status = PurchaseOrder.ORDER_CONSIGNED

    def close(self):
        """Closes the purchase order
        """
        if self.status != PurchaseOrder.ORDER_CONFIRMED:
            raise ValueError(_(u'Invalid status, it should be confirmed '
                               u'got %s instead') % self.status_str)
        self.status = self.ORDER_CLOSED

        Event.log(self.store, Event.TYPE_ORDER,
                  _(u"Order %s, total value %2.2f, supplier '%s' "
                    u"is now closed") % (self.identifier,
                                         self.purchase_total,
                                         self.supplier.person.name))

    def cancel(self):
        """Cancels the purchase order
        """
        assert self.can_cancel()

        # we have to cancel the payments too
        self._payback_paid_payments()
        self.group.cancel()

        self.status = self.ORDER_CANCELLED

    def receive_item(self, item, quantity_to_receive):
        if not item in self.get_pending_items():
            raise StoqlibError(_(u'This item is not pending, hence '
                                 u'cannot be received'))
        quantity = item.quantity - item.quantity_received
        if quantity < quantity_to_receive:
            raise StoqlibError(_(u'The quantity that you want to receive '
                                 u'is greater than the total quantity of '
                                 u'this item %r') % item)
        self.increase_quantity_received(item, quantity_to_receive)

    def increase_quantity_received(self, purchase_item, quantity_received):
        sellable = purchase_item.sellable
        items = [item for item in self.get_items()
                 if item.sellable.id == sellable.id]
        qty = len(items)
        if not qty:
            raise ValueError(_(u'There is no purchase item for '
                               u'sellable %r') % sellable)

        purchase_item.quantity_received += quantity_received

    def update_products_cost(self):
        """Update purchase's items cost

        Update the costs of all products on this purchase
        to the costs specified in the order.
        """
        for item in self.get_items():
            item.sellable.cost = item.cost
            product = item.sellable.product
            product_supplier = product.get_product_supplier_info(self.supplier)
            product_supplier.base_cost = item.cost

    @property
    def status_str(self):
        return PurchaseOrder.translate_status(self.status)

    @property
    def freight_type_name(self):
        if not self.freight_type in self.freight_types.keys():
            raise DatabaseInconsistency(_(u'Invalid freight_type, got %d')
                                        % self.freight_type)
        return self.freight_types[self.freight_type]

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

    @property
    def supplier_name(self):
        return self.supplier.get_description()

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

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

    @property
    def purchase_subtotal(self):
        """Get the subtotal of the purchase.
        The sum of all the items cost * items quantity
        """
        return currency(self.get_items().sum(
            PurchaseItem.cost * PurchaseItem.quantity) or 0)

    @property
    def purchase_total(self):
        subtotal = self.purchase_subtotal
        total = subtotal - self.discount_value + self.surcharge_value
        if total < 0:
            raise ValueError(_(u'Purchase total can not be lesser than zero'))
        # XXX: Since the purchase_total value must have two digits
        # (at the moment) we need to format the value to a 2-digit number and
        # then convert it to currency data type, because the subtotal value
        # may return a 3-or-more-digit value, depending on COST_PRECISION_DIGITS
        # parameters.
        return currency(get_formatted_price(total))

    @property
    def received_total(self):
        """Like {purchase_subtotal} but only takes into account the
        received items
        """
        return currency(self.get_items().sum(
            PurchaseItem.cost *
            PurchaseItem.quantity_received) or 0)

    def get_remaining_total(self):
        """The total value to be paid for the items not received yet
        """
        return self.purchase_total - self.received_total

    def get_pending_items(self, with_children=True):
        """
        Returns a sequence of all items which we haven't received yet.
        """
        return self.get_items(with_children=with_children).find(
            PurchaseItem.quantity_received < PurchaseItem.quantity)

    def get_partially_received_items(self):
        """
        Returns a sequence of all items which are partially received.
        """
        return self.get_items().find(
            PurchaseItem.quantity_received > 0)

    def get_open_date_as_string(self):
        return self.open_date and self.open_date.strftime("%x") or u""

    def get_quote_deadline_as_string(self):
        return self.quote_deadline and self.quote_deadline.strftime("%x") or u""

    def get_receiving_orders(self):
        """Returns all ReceivingOrder related to this purchase order
        """
        from stoqlib.domain.receiving import PurchaseReceivingMap, ReceivingOrder
        tables = [PurchaseReceivingMap, ReceivingOrder]
        query = And(PurchaseReceivingMap.purchase_id == self.id,
                    PurchaseReceivingMap.receiving_id == ReceivingOrder.id)
        return self.store.using(*tables).find(ReceivingOrder, query)

    def get_data_for_labels(self):
        """ This function returns some necessary data to print the purchase's
        items labels
        """
        for purchase_item in self.get_items():
            sellable = purchase_item.sellable
            label_data = Settable(barcode=sellable.barcode, code=sellable.code,
                                  description=sellable.description,
                                  price=sellable.price, sellable=sellable,
                                  quantity=purchase_item.quantity)
            yield label_data

    def has_batch_item(self):
        """Fetch the storables from this purchase order and returns ``True`` if
        any of them is a batch storable.

        :returns: ``True`` if this purchase order has batch items, ``False`` if
        it doesn't.
        """
        return not self.store.find(Storable,
                                   And(self.id == PurchaseOrder.id,
                                       PurchaseOrder.id == PurchaseItem.order_id,
                                       PurchaseItem.sellable_id == Sellable.id,
                                       Sellable.id == Storable.id,
                                       Eq(Storable.is_batch, True))).is_empty()

    def create_receiving_order(self):
        from stoqlib.domain.receiving import ReceivingOrder
        receiving = ReceivingOrder(self.store, branch=self.branch)
        receiving.add_purchase(self)
        for item in self.get_items():
            receiving.add_purchase_item(item, quantity=item.quantity)

        return receiving

    #
    # Classmethods
    #

    @classmethod
    def translate_status(cls, status):
        if not status in cls.statuses:
            raise DatabaseInconsistency(_(u'Got an unexpected status value: '
                                          u'%s') % status)
        return cls.statuses[status]

    @classmethod
    def find_by_work_order(cls, store, work_order):
        return store.find(PurchaseOrder, work_order=work_order)
コード例 #23
0
ファイル: inventory.py プロジェクト: rosalin/stoq
class Inventory(Domain):
    """ The Inventory handles the logic related to creating inventories
    for the available |product| (or a group of) in a certain |branch|.

    It has the following states:

    - STATUS_OPEN: an inventory is opened, at this point the products which
      are going to be counted (and eventually adjusted) are
      selected.
      And then, the inventory items are available for counting and
      adjustment.

    - STATUS_CLOSED: all the inventory items have been counted (and
      eventually) adjusted.

    - STATUS_CANCELLED: the process was cancelled before being finished,
      this can only happen before any items are adjusted.

    .. graphviz::

       digraph inventory_status {
         STATUS_OPEN -> STATUS_CLOSED;
         STATUS_OPEN -> STATUS_CANCELLED;
       }
    """

    __storm_table__ = 'inventory'

    #: The inventory process is open
    STATUS_OPEN = 0

    #: The inventory process is closed
    STATUS_CLOSED = 1

    #: The inventory process was cancelled, eg never finished
    STATUS_CANCELLED = 2

    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 inventory, either STATUS_OPEN, STATUS_CLOSED or
    #: STATUS_CANCELLED
    status = IntCol(default=STATUS_OPEN)

    #: number of the invoice if this inventory generated an adjustment
    invoice_number = IntCol(default=None)

    #: the date inventory process was started
    open_date = DateTimeCol(default_factory=localnow)

    #: the date inventory process was closed
    close_date = DateTimeCol(default=None)

    responsible_id = IdCol(allow_none=False)
    #: the responsible for this inventory. At the moment, the
    #: |loginuser| that opened the inventory
    responsible = Reference(responsible_id, 'LoginUser.id')

    branch_id = IdCol(allow_none=False)
    #: branch where the inventory process was done
    branch = Reference(branch_id, 'Branch.id')

    #: the |inventoryitems| of this inventory
    inventory_items = ReferenceSet('id', 'InventoryItem.inventory_id')

    #
    # Public API
    #

    def add_sellable(self, sellable, batch_number=None):
        """Add a sellable in this inventory

        Note that the :attr:`item's quantity <InventoryItem.recorded_quantity>`
        will be set based on the registered sellable's stock

        :param sellable: the |sellable| to be added
        :param batch_number: a batch number representing a |batch|
            for the given sellable. It's used like that instead of
            getting the |batch| directly since we may be adding an item
            not registered before
        """
        product = sellable.product
        storable = product.storable
        if storable is None:
            raise TypeError("product %r has no storable" % (product, ))

        if batch_number is not None:
            batch = StorableBatch.get_or_create(self.store,
                                                storable=storable,
                                                batch_number=batch_number)
            quantity = batch.get_balance_for_branch(self.branch)
        else:
            batch = None
            quantity = storable.get_balance_for_branch(self.branch)

        self.validate_batch(batch, sellable)

        return InventoryItem(store=self.store,
                             product=sellable.product,
                             batch=batch,
                             product_cost=sellable.cost,
                             recorded_quantity=quantity,
                             inventory=self)

    def is_open(self):
        """Checks if this inventory is opened

        :returns: ``True`` if the inventory process is open,
            ``False`` otherwise
        """
        return self.status == self.STATUS_OPEN

    def close(self):
        """Closes the inventory process

        :raises: :exc:`AssertionError` if the inventory is already closed
        """
        if not self.is_open():
            # FIXME: We should be raising a better error here.
            raise AssertionError("You can not close an inventory which is "
                                 "already closed!")

        for item in self.inventory_items:
            if item.counted_quantity != item.recorded_quantity:
                continue

            # FIXME: We are setting this here because, when generating a
            # sintegra file, even if this item wasn't really adjusted (e.g.
            # adjustment_qty bellow is 0) it needs to be specified and not
            # setting this would result on self.get_cost returning 0.  Maybe
            # we should resolve this in another way
            # We don't call item.adjust since it needs an invoice number
            item.is_adjusted = True

        self.close_date = StatementTimestamp()
        self.status = Inventory.STATUS_CLOSED

    def all_items_counted(self):
        """Checks if all items of this inventory were counted

        :returns: ``True`` if all inventory items are counted,
            ``False`` otherwise.
        """
        # FIXME: Why would items not be counted if the status is closed?
        # The status can only be closed if the items were counted and adjusted
        if self.status == self.STATUS_CLOSED:
            return False

        return self.inventory_items.find(counted_quantity=None).is_empty()

    def get_items(self):
        """Returns all the inventory items related to this inventory

        :returns: items
        :rtype: a sequence of :class:`InventoryItem`
        """
        store = self.store
        return store.find(InventoryItem, inventory=self)

    @classmethod
    def has_open(cls, store, branch):
        """Returns if there is an inventory opened at the moment or not.

        :returns: The open inventory, if there is one. None otherwise.
        """
        return store.find(cls, status=Inventory.STATUS_OPEN,
                          branch=branch).one()

    def get_items_for_adjustment(self):
        """Gets all the inventory items that needs adjustment

        An item needing adjustment is any :class:`InventoryItem`
        with :attr:`InventoryItem.recorded_quantity` different from
        :attr:`InventoryItem.counted_quantity`.

        :returns: items
        :rtype: a sequence of :class:`InventoryItem`
        """
        return self.inventory_items.find(
            And(InventoryItem.recorded_quantity != InventoryItem.counted_quantity,
                Eq(InventoryItem.is_adjusted, False)))

    def has_adjusted_items(self):
        """Returns if we already have an item adjusted or not.

        :returns: ``True`` if there is one or more items adjusted, False
          otherwise.
        """
        return not self.inventory_items.find(is_adjusted=True).is_empty()

    def cancel(self):
        """Cancel this inventory

        Note that you can only cancel an inventory as long
        as you haven't adjusted any :class:`InventoryItem`

        :raises: :exc:`AssertionError` if the inventory is not
            open or if any item was already adjusted
        """
        if not self.is_open():
            raise AssertionError(
                "You can't cancel an inventory that is not opened!")

        if self.has_adjusted_items():
            raise AssertionError(
                "You can't cancel an inventory that has adjusted items!")

        self.status = Inventory.STATUS_CANCELLED

    def get_status_str(self):
        return self.statuses[self.status]

    def get_branch_name(self):
        """
        This method returns the name for Branch of Person reference object
        :return: branch_name
        """
        return self.branch.person.name
コード例 #24
0
class Inventory(Domain):
    """ The Inventory handles the logic related to creating inventories
    for the available |product| (or a group of) in a certain |branch|.

    It has the following states:

    - STATUS_OPEN: an inventory is opened, at this point the products which
      are going to be counted (and eventually adjusted) are
      selected.
      And then, the inventory items are available for counting and
      adjustment.

    - STATUS_CLOSED: all the inventory items have been counted (and
      eventually) adjusted.

    - STATUS_CANCELLED: the process was cancelled before being finished,
      this can only happen before any items are adjusted.

    .. graphviz::

       digraph inventory_status {
         STATUS_OPEN -> STATUS_CLOSED;
         STATUS_OPEN -> STATUS_CANCELLED;
       }
    """

    __storm_table__ = 'inventory'

    #: The inventory process is open
    STATUS_OPEN = u'open'

    #: The inventory process is closed
    STATUS_CLOSED = u'closed'

    #: The inventory process was cancelled, eg never finished
    STATUS_CANCELLED = u'cancelled'

    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 inventory, either STATUS_OPEN, STATUS_CLOSED or
    #: STATUS_CANCELLED
    status = EnumCol(allow_none=False, default=STATUS_OPEN)

    #: number of the invoice if this inventory generated an adjustment
    invoice_number = IntCol(default=None)

    #: the date inventory process was started
    open_date = DateTimeCol(default_factory=localnow)

    #: the date inventory process was closed
    close_date = DateTimeCol(default=None)

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

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

    responsible_id = IdCol(allow_none=False)
    #: the responsible for this inventory. At the moment, the
    #: |loginuser| that opened the inventory
    responsible = Reference(responsible_id, 'LoginUser.id')

    branch_id = IdCol(allow_none=False)
    #: branch where the inventory process was done
    branch = Reference(branch_id, 'Branch.id')

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

    #: the |inventoryitems| of this inventory
    inventory_items = ReferenceSet('id', 'InventoryItem.inventory_id')

    #
    # Properties
    #

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

    @property
    def branch_name(self):
        """The |branch| name for this inventory"""
        return self.branch.get_description()

    @property
    def responsible_name(self):
        """The responsible for this inventory"""
        return self.responsible.get_description()

    #
    # Public API
    #

    def add_storable(self, storable, quantity, batch_number=None, batch=None):
        """Add a storable to this inventory.

        The parameters product, storable and batch are passed here to avoid
        future queries, increase the performance when opening the inventory

        :param storable: the |storable| to be added
        :param quantity: the current quantity of the product in stock
        :param batch_number: a batch number representing a |batch|
            for the given sellable. It's used like that instead of
            getting the |batch| directly since we may be adding an item
            not registered before
        :param batch: the corresponding batch to the batch_number
        """
        if batch_number is not None and not batch:
            batch = StorableBatch.get_or_create(self.store,
                                                storable=storable,
                                                batch_number=batch_number)

        product = storable.product
        sellable = product.sellable
        self.validate_batch(batch, sellable, storable=storable)
        return InventoryItem(store=self.store,
                             product=product,
                             batch=batch,
                             product_cost=sellable.cost,
                             recorded_quantity=quantity,
                             inventory=self)

    def is_open(self):
        """Checks if this inventory is opened

        :returns: ``True`` if the inventory process is open,
            ``False`` otherwise
        """
        return self.status == self.STATUS_OPEN

    def close(self):
        """Closes the inventory process

        :raises: :exc:`AssertionError` if the inventory is already closed
        """
        if not self.is_open():
            # FIXME: We should be raising a better error here.
            raise AssertionError("You can not close an inventory which is "
                                 "already closed!")

        for item in self.inventory_items:
            if (item.actual_quantity is None
                    or item.recorded_quantity == item.actual_quantity):
                continue

            # FIXME: We are setting this here because, when generating a
            # sintegra file, even if this item wasn't really adjusted (e.g.
            # adjustment_qty bellow is 0) it needs to be specified and not
            # setting this would result on self.get_cost returning 0.  Maybe
            # we should resolve this in another way
            # We don't call item.adjust since it needs an invoice number
            item.is_adjusted = True

        self.close_date = StatementTimestamp()
        self.status = Inventory.STATUS_CLOSED

    def all_items_counted(self):
        """Checks if all items of this inventory were counted

        :returns: ``True`` if all inventory items are counted,
            ``False`` otherwise.
        """
        # FIXME: Why would items not be counted if the status is closed?
        # The status can only be closed if the items were counted and adjusted
        if self.status == self.STATUS_CLOSED:
            return False

        return self.inventory_items.find(counted_quantity=None).is_empty()

    def get_items(self):
        """Returns all the inventory items related to this inventory

        :returns: items
        :rtype: a sequence of :class:`InventoryItem`
        """
        store = self.store
        return store.find(InventoryItem, inventory=self)

    @classmethod
    def has_open(cls, store, branch):
        """Returns if there is an inventory opened at the moment or not.

        :returns: The open inventory, if there is one. None otherwise.
        """
        return store.find(cls, status=Inventory.STATUS_OPEN,
                          branch=branch).one()

    def get_items_for_adjustment(self):
        """Gets all the inventory items that needs adjustment

        An item needing adjustment is any :class:`InventoryItem`
        with :attr:`InventoryItem.recorded_quantity` different from
        :attr:`InventoryItem.counted_quantity`.

        :returns: items
        :rtype: a sequence of :class:`InventoryItem`
        """
        return self.inventory_items.find(
            And(
                InventoryItem.recorded_quantity !=
                InventoryItem.counted_quantity,
                Eq(InventoryItem.is_adjusted, False)))

    def has_adjusted_items(self):
        """Returns if we already have an item adjusted or not.

        :returns: ``True`` if there is one or more items adjusted, False
          otherwise.
        """
        return not self.inventory_items.find(is_adjusted=True).is_empty()

    def cancel(self):
        """Cancel this inventory

        Note that you can only cancel an inventory as long
        as you haven't adjusted any :class:`InventoryItem`

        :raises: :exc:`AssertionError` if the inventory is not
            open or if any item was already adjusted
        """
        if not self.is_open():
            raise AssertionError(
                "You can't cancel an inventory that is not opened!")

        if self.has_adjusted_items():
            raise AssertionError(
                "You can't cancel an inventory that has adjusted items!")

        self.status = Inventory.STATUS_CANCELLED

    def get_inventory_data(self):
        """Returns a generator with the details of the Inventory

        Each item contains:

        - The |inventoryitem|
        - the |storable|
        - the |product|
        - the |sellable|
        - the |storablebatch|
        """
        store = self.store
        tables = [
            InventoryItem,
            Join(Product, Product.id == InventoryItem.product_id),
            Join(Storable, Storable.id == Product.id),
            Join(Sellable, Sellable.id == Product.id),
            LeftJoin(StorableBatch, StorableBatch.id == InventoryItem.batch_id)
        ]
        return store.using(*tables).find(
            (InventoryItem, Storable, Product, Sellable, StorableBatch),
            InventoryItem.inventory_id == self.id)

    @classmethod
    def get_sellables_for_inventory(cls, store, branch, extra_query=None):
        """Returns a generator with the necessary data about the stock to open an Inventory

        :param store: The store to fetch data from
        :param branch: The branch that is being inventoried
        :param query: A query that should be used to restrict the storables for
            the inventory. This can filter based on categories or other aspects
            of the product.

        :returns: a generator of the following objects:
            (Sellable, Product, Storable, StorableBatch, ProductStockItem)
        """
        # XXX: If we should want all storables to be inclued in the inventory, even if if
        #      never had a ProductStockItem before, than we should inclue this query in the
        #      LeftJoin with ProductStockItem below
        query = ProductStockItem.branch_id == branch.id
        if extra_query:
            query = And(query, extra_query)

        tables = [
            Sellable,
            Join(Product, Product.id == Sellable.id),
            Join(Storable, Storable.id == Product.id),
            LeftJoin(StorableBatch, StorableBatch.storable_id == Storable.id),
            LeftJoin(
                ProductStockItem,
                And(
                    ProductStockItem.storable_id == Storable.id,
                    Or(ProductStockItem.batch_id == StorableBatch.id,
                       Eq(ProductStockItem.batch_id, None)))),
        ]
        return store.using(*tables).find(
            (Sellable, Product, Storable, StorableBatch, ProductStockItem),
            query)

    @classmethod
    def create_inventory(cls, store, branch, responsible, query=None):
        """Create a inventory with products that match the given query

        :param store: A store to open the inventory in
        :param query: A query to restrict the products that should be in the inventory.
        """
        inventory = cls(store=store,
                        open_date=localnow(),
                        branch_id=branch.id,
                        responsible_id=responsible.id)

        for data in cls.get_sellables_for_inventory(store, branch, query):
            sellable, product, storable, batch, stock_item = data
            quantity = stock_item and stock_item.quantity or 0
            if storable.is_batch:
                # This used to test 'stock_item.quantity > 0' too to avoid
                # creating inventory items for old batches not used anymore.
                # We can't do that since that would make it impossible to
                # adjust a batch that was wrongly set to 0. We need to find a
                # way to mark the batches as "not used anymore" because they
                # tend to grow to very large proportions and we are duplicating
                # everyone here
                if batch and stock_item:
                    inventory.add_storable(storable, quantity, batch=batch)
            else:
                inventory.add_storable(storable, quantity)
        return inventory
コード例 #25
0
class WorkOrder(Domain):
    """Represents a work order

    Normally, this is a maintenance task, like:
        * The |client| reports a defect on an equipment.
        * The responsible for doing the quote analyzes the equipment
          and detects the real defect.
        * The |client| then approves the quote and the work begins.
        * After it's finished, a |sale| is created for it, the
          |client| pays and gets it's equipment back.

    .. graphviz::

       digraph work_order_status {
         STATUS_OPENED -> STATUS_APPROVED;
         STATUS_OPENED -> STATUS_CANCELLED;
         STATUS_APPROVED -> STATUS_OPENED;
         STATUS_APPROVED -> STATUS_CANCELLED;
         STATUS_APPROVED -> STATUS_WORK_IN_PROGRESS;
         STATUS_WORK_IN_PROGRESS -> STATUS_WORK_FINISHED;
         STATUS_WORK_FINISHED -> STATUS_CLOSED;
       }

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

    __storm_table__ = 'work_order'

    implements(IContainer)

    #: a request for an order has been created, the order has not yet
    #: been approved the |client|
    STATUS_OPENED = 0

    #: for some reason it was cancelled
    STATUS_CANCELLED = 1

    #: the |client| has approved the order, work has not begun yet
    STATUS_APPROVED = 2

    #: work is currently in progress
    STATUS_WORK_IN_PROGRESS = 3

    #: work has been finished, but no |sale| has been created yet.
    #: Work orders with this status will be displayed in the till/pos
    #: applications and it's possible to create a |sale| from them.
    STATUS_WORK_FINISHED = 4

    #: a |sale| has been created, delivery and payment handled there
    STATUS_CLOSED = 5

    statuses = {
        STATUS_OPENED: _(u'Waiting'),
        STATUS_CANCELLED: _(u'Cancelled'),
        STATUS_APPROVED: _(u'Approved'),
        STATUS_WORK_IN_PROGRESS: _(u'In progress'),
        STATUS_WORK_FINISHED: _(u'Finished'),
        STATUS_CLOSED: _(u'Closed')
    }

    status = IntCol(default=STATUS_OPENED)

    #: 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()

    #: defected equipment
    equipment = UnicodeCol()

    #: defect reported by the |client|
    defect_reported = UnicodeCol()

    #: defect detected by the :obj:`.quote_responsible`
    defect_detected = UnicodeCol()

    #: estimated hours needed to complete the work
    estimated_hours = DecimalCol(default=None)

    #: estimated cost of the work
    estimated_cost = PriceCol(default=None)

    #: estimated date the work will start
    estimated_start = DateTimeCol(default=None)

    #: estimated date the work will finish
    estimated_finish = DateTimeCol(default=None)

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

    #: date this work was approved (set by :obj:`.approve`)
    approve_date = DateTimeCol(default=None)

    #: date this work was finished (set by :obj:`.finish`)
    finish_date = DateTimeCol(default=None)

    branch_id = IntCol()
    #: the |branch| where this order was created and responsible for it
    branch = Reference(branch_id, 'Branch.id')

    current_branch_id = IntCol()
    #: the actual branch where the order is. Can differ from
    # :attr:`.branch` if the order was sent in a |workorderpackage|
    #: to another |branch| for execution
    current_branch = Reference(current_branch_id, 'Branch.id')

    quote_responsible_id = IntCol(default=None)
    #: the |loginuser| responsible for the :obj:`.defect_detected`
    quote_responsible = Reference(quote_responsible_id, 'LoginUser.id')

    execution_responsible_id = IntCol(default=None)
    #: the |loginuser| responsible for the execution of the work
    execution_responsible = Reference(execution_responsible_id, 'LoginUser.id')

    client_id = IntCol(default=None)
    #: the |client|, owner of the equipment
    client = Reference(client_id, 'Client.id')

    category_id = IntCol(default=None)
    #: the |workordercategory| this work belongs
    category = Reference(category_id, 'WorkOrderCategory.id')

    sale_id = IntCol(default=None)
    #: the |sale| created after this work is finished
    sale = Reference(sale_id, 'Sale.id')

    order_items = ReferenceSet('id', 'WorkOrderItem.order_id')

    @property
    def status_str(self):
        if self.is_in_transport():
            return _("In transport")
        return self.statuses[self.status]

    def __init__(self, *args, **kwargs):
        super(WorkOrder, self).__init__(*args, **kwargs)

        if self.current_branch is None:
            self.current_branch = self.branch

    #
    #  IContainer implementation
    #

    def add_item(self, item):
        assert item.order is None
        item.order = self

    def get_items(self):
        return self.order_items

    def remove_item(self, item):
        assert item.order is self
        # Setting the quantity to 0 and calling sync_stock
        # will return all the actual quantity to the stock
        item.quantity = 0
        item.sync_stock()
        self.store.remove(item)

    #
    #  Public API
    #

    def get_total_amount(self):
        """Returns the total amount of this work order

        This is the same as::

            sum(item.total for item in :obj:`.order_items`)

        """
        items = self.order_items.find()
        return (items.sum(WorkOrderItem.price * WorkOrderItem.quantity)
                or currency(0))

    def add_sellable(self, sellable, price=None, quantity=1, batch=None):
        """Adds a sellable to this work order

        :param sellable: the |sellable| being added
        :param price: the price the sellable will be sold when
            finishing this work order
        :param quantity: the sellable's quantity
        :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.
        :returns: the created |workorderitem|
        """
        self.validate_batch(batch, sellable=sellable)
        if price is None:
            price = sellable.base_price

        item = WorkOrderItem(store=self.store,
                             sellable=sellable,
                             batch=batch,
                             price=price,
                             quantity=quantity,
                             order=self)
        return item

    def sync_stock(self):
        """Synchronizes the stock for this work order's items

        Just a shortcut to call :meth:`WorkOrderItem.sync_stock` in all
        items in this work order.
        """
        for item in self.get_items():
            item.sync_stock()

    def is_in_transport(self):
        """Checks if this work order is in transport

        A work order is in transport if it's :attr:`.current_branch`
        is ``None``. The transportation of the work order is done in
        a |workorderpackage|

        :returns: ``True`` if in transport, ``False`` otherwise
        """
        return self.current_branch is None

    def is_finished(self):
        """Checks if this work order is finished

        A work order is finished when the work that needs to be done
        on it finished, so this will be ``True`` when :obj:`WorkOrder.status` is
        :obj:`.STATUS_WORK_FINISHED` and :obj:`.STATUS_CLOSED`
        """
        return self.status in [self.STATUS_WORK_FINISHED, self.STATUS_CLOSED]

    def is_late(self):
        """Checks if this work order is late

        Being late means we set an
        :obj:`estimated finish date <.estimated_finish>` and that
        date has already passed.
        """
        if self.status in [self.STATUS_WORK_FINISHED, self.STATUS_CLOSED]:
            return False
        if not self.estimated_finish:
            # No estimated_finish means we are not late
            return False

        today = localtoday().date()
        return self.estimated_finish.date() < today

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

        Only opened and approved orders can be cancelled. Once the
        work has started, it should not be possible to do that anymore.

        :returns: ``True`` if can be cancelled, ``False`` otherwise
        """
        return self.status in [self.STATUS_OPENED, self.STATUS_APPROVED]

    def can_approve(self):
        """Checks if this work order can be approved

        :returns: ``True`` if can be approved, ``False`` otherwise
        """
        return self.status == self.STATUS_OPENED

    def can_undo_approval(self):
        """Checks if this work order order can be unapproved

        Only approved orders can be unapproved. Once the work
        has started, it should not be possible to do that anymore

        :returns: ``True`` if can be unapproved, ``False`` otherwise
        """
        return self.status == self.STATUS_APPROVED

    def can_start(self):
        """Checks if this work order can start

        Note that the work needs to be approved before it can be started.

        :returns: ``True`` if can start, ``False`` otherwise
        """
        return self.status == self.STATUS_APPROVED

    def can_finish(self):
        """Checks if this work order can finish

        Note that the work needs to be started before you can finish.

        :returns: ``True`` if can finish, ``False`` otherwise
        """
        if not self.order_items.count():
            return False
        return self.status == self.STATUS_WORK_IN_PROGRESS

    def can_close(self):
        """Checks if this work order can close

        Note that the work needs to be finished before you can close.

        :returns: ``True`` if can close, ``False`` otherwise
        """
        return self.status == self.STATUS_WORK_FINISHED

    def cancel(self):
        """Cancels this work order

        Cancel the work order, probably because the |client|
        didn't approve it or simply gave up of doing it.
        """
        assert self.can_cancel()
        self.status = self.STATUS_CANCELLED

    def approve(self):
        """Approves this work order

        Approving means that the |client| has accepted the
        work's quote and it's cost and it can now start.
        """
        assert self.can_approve()
        self.approve_date = localnow()
        self.status = self.STATUS_APPROVED

    def undo_approval(self):
        """Unapproves this work order

        Unapproving means that the |client| once has approved the
        order's task and it's cost, but now he doesn't anymore.
        Different from :meth:`.cancel`, the |client| still can
        approve this again.
        """
        assert self.can_undo_approval()
        self.approve_date = None
        self.status = self.STATUS_OPENED

    def start(self):
        """Starts this work order's task

        The :obj:`.execution_responsible` started working on
        this order's task and will finish sometime in the future.
        """
        assert self.can_start()
        self.status = self.STATUS_WORK_IN_PROGRESS

    def finish(self):
        """Finishes this work order's task

        The :obj:`.execution_responsible` has finished working on
        this order's task. It's possible now to give the equipment
        back to the |client| and create a |sale| so we are able
        to :meth:`close <.close>` this order.
        """
        assert self.can_finish()
        self.finish_date = localnow()
        self.status = self.STATUS_WORK_FINISHED

    def close(self):
        """Closes this work order

        This order's task is done, the |client| got the equipment
        back and a |sale| was created for the |workorderitems|
        Nothing more needs to be done.
        """
        assert self.can_close()
        self.status = self.STATUS_CLOSED

    def change_status(self, new_status):
        """
        Change the status of this work order

        Using this function you can change the status is several steps.

        :returns: if the status was changed
        :raises: :exc:`stoqlib.exceptions.InvalidStatus` if the status cannot be changed
        """
        if self.status == WorkOrder.STATUS_WORK_FINISHED:
            raise InvalidStatus(
                _("This work order has already been finished, it cannot be modified."
                  ))

        # This is the logic order of status changes, this is the flow/ordering
        # of the status that should be used
        status_order = [
            WorkOrder.STATUS_OPENED, WorkOrder.STATUS_APPROVED,
            WorkOrder.STATUS_WORK_IN_PROGRESS, WorkOrder.STATUS_WORK_FINISHED
        ]

        old_index = status_order.index(self.status)
        new_index = status_order.index(new_status)
        direction = cmp(new_index, old_index)

        next_status = self.status
        while True:
            # Calculate what's the next status we should set in order to reach
            # our goal (new_status). Note that this can go either forward or backward
            # depending on the direction
            next_status = status_order[status_order.index(next_status) +
                                       direction]
            if next_status == WorkOrder.STATUS_WORK_IN_PROGRESS:
                if not self.can_start():
                    raise InvalidStatus(_("This work order cannot be started"))
                self.start()

            if next_status == WorkOrder.STATUS_WORK_FINISHED:
                if not self.can_finish():
                    raise InvalidStatus(
                        _('This work order cannot be finished'))
                self.finish()

            if next_status == WorkOrder.STATUS_APPROVED:
                if not self.can_approve():
                    raise InvalidStatus(
                        _("This work order cannot be approved, it's already in progress"
                          ))
                self.approve()

            if next_status == WorkOrder.STATUS_OPENED:
                if not self.can_undo_approval():
                    raise InvalidStatus(
                        _('This work order cannot be re-opened'))
                self.undo_approval()

            # We've reached our goal, bail out
            if next_status == new_status:
                break

    @classmethod
    def find_by_sale(cls, store, sale):
        """Returns all |workorders| associated with the given |sale|.

        :param sale: The |sale| used to filter the existing |workorders|
        :resturn: An iterable with all work orders:
        :rtype: resultset
        """
        return store.find(cls, sale=sale)
コード例 #26
0
ファイル: transfer.py プロジェクト: rosalin/stoq
class TransferOrder(Domain):
    """ Transfer Order class
    """
    __storm_table__ = 'transfer_order'

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

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

    status = IntCol(default=STATUS_PENDING)

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

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

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

    source_branch_id = IdCol()

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

    destination_branch_id = IdCol()

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

    source_responsible_id = IdCol()

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

    destination_responsible_id = IdCol()

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

    #
    # IContainer implementation
    #

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

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

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

    #
    # Public API
    #

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

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

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

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

        self.validate_batch(batch, sellable=sellable)

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

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

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

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

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

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

        self.status = self.STATUS_SENT

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

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

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

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

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

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

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

        return self.destination_responsible.person.name

    def get_total_items_transfer(self):
        """Retuns the |transferitems| quantity
        """
        return sum([item.quantity for item in self.get_items()], 0)
コード例 #27
0
ファイル: test_base_domain.py プロジェクト: esosaja/stoq
class Dung(Domain):
    __storm_table__ = 'dung'
    identifier = IdentifierCol()
    ding_id = IdCol()
    ding = Reference(ding_id, Ding.id)
コード例 #28
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)
コード例 #29
0
ファイル: stockdecrease.py プロジェクト: 5l1v3r1/stoq-1
class StockDecrease(IdentifiableDomain):
    """Stock Decrease object implementation.

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

    __storm_table__ = 'stock_decrease'

    #: Stock Decrease is still being edited
    STATUS_INITIAL = u'initial'

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

    #: Stock Decrease is cancelled and all items have been returned to stock.
    STATUS_CANCELLED = u'cancelled'

    statuses = collections.OrderedDict([
        (STATUS_INITIAL, _(u'Opened')),
        (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 sale
    status = EnumCol(allow_none=False, default=STATUS_INITIAL)

    reason = UnicodeCol(default=u'')

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

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

    #: The date the stock decrease was cancelled
    cancel_date = DateTimeCol(default=None)

    #: The reason stock decrease loan was cancelled
    cancel_reason = UnicodeCol()

    #: The key of the invoice referenced by the stock decrease, if exists
    referenced_invoice_key = UnicodeCol()

    responsible_id = IdCol()

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

    removed_by_id = IdCol()

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

    branch_id = IdCol()

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

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

    #: person who is receiving
    person_id = IdCol()

    person = Reference(person_id, 'Person.id')

    #: the choosen CFOP
    cfop_id = IdCol()

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

    #: the payment group related to this stock decrease
    group_id = IdCol()

    group = Reference(group_id, 'PaymentGroup.id')

    cost_center_id = IdCol()

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

    invoice_id = IdCol()

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

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

    #: The receiving order that the stock decrease can be related to
    receiving_order_id = IdCol()
    receiving_order = Reference(receiving_order_id, 'ReceivingOrder.id')

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

    #
    # IInvoice implementation
    #

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

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

    @property
    def invoice_subtotal(self):
        return currency(self.get_total_cost())

    @property
    def invoice_total(self):
        return currency(self.get_total_cost())

    @property
    def payments(self):
        if self.group:
            return self.group.get_valid_payments().order_by(Payment.open_date)
        return None

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

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

    @property
    def transporter(self):
        delivery_item = self.get_delivery_item()
        if delivery_item is None:
            return None

        return delivery_item.delivery_adaptor.transporter

    #
    # Classmethods
    #

    @classmethod
    def create_for_receiving_order(cls, receiving_order, branch: Branch,
                                   station: BranchStation, user: LoginUser):
        store = receiving_order.store
        employee = user.person.employee
        cfop_id = sysparam.get_object_id('DEFAULT_STOCK_DECREASE_CFOP')
        return_stock_decrease = cls(store=store,
                                    receiving_order=receiving_order,
                                    branch=branch,
                                    station=station,
                                    responsible=user,
                                    removed_by=employee,
                                    cfop_id=cfop_id)

        for receiving_item in receiving_order.get_items(with_children=False):
            if receiving_item.is_totally_returned():
                # Exclude items already totally returned
                continue

            if receiving_item.children_items.count():
                for child in receiving_item.children_items:
                    StockDecreaseItem.create_for_receiving_item(
                        return_stock_decrease, child)
            else:
                StockDecreaseItem.create_for_receiving_item(
                    return_stock_decrease, receiving_item)
        return return_stock_decrease

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

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

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

    # Status

    def can_confirm(self):
        """Only stock decreases with status equal to INITIAL can be confirmed

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

    def confirm(self, user):
        """Confirms the stock decrease

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

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

        old_status = self.status
        self.status = StockDecrease.STATUS_CONFIRMED

        self.invoice.branch = branch

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

        StockOperationConfirmedEvent.emit(self, old_status)

    #
    # Accessors
    #

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

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

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

        return self.removed_by.get_description()

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

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

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

    def get_delivery_item(self):
        delivery_service_id = sysparam.get_object_id('DELIVERY_SERVICE')
        for item in self.get_items():
            if item.sellable.id == delivery_service_id:
                return item
        return None

    # Other methods

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

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

        return StockDecreaseItem(store=self.store,
                                 quantity=quantity,
                                 stock_decrease=self,
                                 sellable=sellable,
                                 batch=batch,
                                 cost=cost)