Source code for stoqlib.domain.receiving

# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4

##
## Copyright (C) 2006-2009 Async Open Source <http://www.async.com.br>
## All rights reserved
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU Lesser General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU Lesser General Public License for more details.
##
## You should have received a copy of the GNU Lesser General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., or visit: http://www.gnu.org/.
##
##  Author(s): Stoq Team <stoq-devel@async.com.br>
##
""" Receiving management """

# pylint: enable=E1101

from decimal import Decimal
import collections

from kiwi.currency import currency
from storm.expr import And, Eq
from storm.references import Reference, ReferenceSet

from stoqlib.database.properties import (PriceCol, QuantityCol, IntCol,
                                         DateTimeCol, UnicodeCol, IdentifierCol,
                                         IdCol, EnumCol)
from stoqlib.domain.base import Domain
from stoqlib.domain.fiscal import FiscalBookEntry
from stoqlib.domain.payment.group import PaymentGroup
from stoqlib.domain.payment.method import PaymentMethod
from stoqlib.domain.payment.payment import Payment
from stoqlib.domain.product import (ProductHistory, StockTransactionHistory,
                                    StorableBatch)
from stoqlib.domain.purchase import PurchaseOrder
from stoqlib.lib.dateutils import localnow
from stoqlib.lib.defaults import quantize
from stoqlib.lib.parameters import sysparam
from stoqlib.lib.translation import stoqlib_gettext

_ = stoqlib_gettext


[docs]class ReceivingOrderItem(Domain): """This class stores information of the purchased items. Note that objects of this type should not be created manually, only by calling Receiving """ __storm_table__ = 'receiving_order_item' #: the total quantity received for a certain |product| quantity = QuantityCol() #: the cost for each |product| received cost = PriceCol() purchase_item_id = IdCol() purchase_item = Reference(purchase_item_id, 'PurchaseItem.id') # FIXME: This could be a product instead of a sellable, since we only buy # products from the suppliers. sellable_id = IdCol() #: the |sellable| sellable = Reference(sellable_id, 'Sellable.id') batch_id = IdCol() #: If the sellable is a storable, the |batch| that it was received in batch = Reference(batch_id, 'StorableBatch.id') receiving_order_id = IdCol() receiving_order = Reference(receiving_order_id, 'ReceivingOrder.id') parent_item_id = IdCol() parent_item = Reference(parent_item_id, 'ReceivingOrderItem.id') children_items = ReferenceSet('id', 'ReceivingOrderItem.parent_item_id') # # Properties # @property def unit_description(self): unit = self.sellable.unit return u"%s" % (unit and unit.description or u"") # # Accessors #
[docs] def get_remaining_quantity(self): """Get the remaining quantity from the purchase order this item is included in. :returns: the remaining quantity """ return self.purchase_item.get_pending_quantity()
def get_total(self): # We need to use the the purchase_item cost, since the current cost # might be different. cost = self.purchase_item.cost return currency(self.quantity * cost) def get_quantity_unit_string(self): unit = self.sellable.unit data = u"%s %s" % (self.quantity, unit and unit.description or u"") # The unit may be empty return data.strip()
[docs] def add_stock_items(self): """This is normally called from ReceivingOrder when a the receving order is confirmed. """ store = self.store if self.quantity > self.get_remaining_quantity(): raise ValueError( u"Quantity received (%d) is greater than " u"quantity ordered (%d)" % (self.quantity, self.get_remaining_quantity())) branch = self.receiving_order.branch storable = self.sellable.product_storable purchase = self.purchase_item.order if storable is not None: storable.increase_stock(self.quantity, branch, StockTransactionHistory.TYPE_RECEIVED_PURCHASE, self.id, self.cost, batch=self.batch) purchase.increase_quantity_received(self.purchase_item, self.quantity) ProductHistory.add_received_item(store, branch, self)
[docs]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 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') 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)
[docs] 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)
[docs] 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) # # 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.encode() @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_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)
[docs] 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)
[docs]class PurchaseReceivingMap(Domain): """This class stores a map for purchase and receivings. One purchase may be received more than once, for instance, if it was shippped in more than one package. Also, a receiving may be for different purchase orders, if more than one purchase order was shipped in the same package. """ __storm_table__ = 'purchase_receiving_map' purchase_id = IdCol() #: The purchase that was recieved purchase = Reference(purchase_id, 'PurchaseOrder.id') receiving_id = IdCol() #: In which receiving the purchase was received. receiving = Reference(receiving_id, 'ReceivingOrder.id')