class StorableBranchOverride(Domain): __storm_table__ = 'storable_branch_override' minimum_quantity = QuantityCol() maximum_quantity = QuantityCol() branch_id = IdCol() branch = Reference(branch_id, 'Branch.id') storable_id = IdCol() storable = Reference(storable_id, 'Storable.id')
class TransferOrderItem(Domain): """Transfer order item """ __storm_table__ = 'transfer_order_item' sellable_id = IntCol() # FIXME: This should be a product, since it does not make sense to transfer # serviçes #: The |sellable| to transfer sellable = Reference(sellable_id, 'Sellable.id') batch_id = IntCol() #: If the sellable is a storable, the |batch| that was transfered batch = Reference(batch_id, 'StorableBatch.id') transfer_order_id = IntCol() #: The |transfer| this item belongs to transfer_order = Reference(transfer_order_id, 'TransferOrder.id') #: The quantity to transfer quantity = QuantityCol() # # Public API # def get_total(self): """Returns the total cost of a transfer item eg quantity * cost""" return self.quantity * self.sellable.cost def send(self): """Sends this item to it's destination |branch|""" assert self.transfer_order.can_close() storable = self.sellable.product_storable storable.decrease_stock(self.quantity, self.transfer_order.source_branch, StockTransactionHistory.TYPE_TRANSFER_TO, self.id) ProductHistory.add_transfered_item(self.store, self.transfer_order.source_branch, self) def receive(self): """Receives this item, increasing the quantity in the stock """ storable = self.sellable.product_storable from_stock = storable.get_stock_item(self.transfer_order.source_branch, self.batch) storable.increase_stock(self.quantity, self.transfer_order.destination_branch, StockTransactionHistory.TYPE_TRANSFER_FROM, self.id, unit_cost=from_stock.stock_cost, batch=self.batch)
class ProductStockItem(Domain): __storm_table__ = 'product_stock_item' stock_cost = PriceCol(default=0) quantity = QuantityCol(default=0) storable_id = IntCol() storable = Reference(storable_id, Storable.id)
class ProductionService(Domain): """Production Service object implementation. :attribute order: The :class:`ProductionOrder` of this service. :attribute service: The service that will be used by the production. :attribute quantity: The service's quantity. """ implements(IDescribable) __storm_table__ = 'production_service' service_id = IntCol() service = Reference(service_id, 'Service.id') order_id = IntCol() order = Reference(order_id, 'ProductionOrder.id') quantity = QuantityCol(default=1) # # IDescribable Implementation # def get_description(self): return self.service.sellable.get_description() # Accessors def get_unit_description(self): return self.service.sellable.get_unit_description()
class ProductionService(Domain): """Production Service object implementation. """ __storm_table__ = 'production_service' service_id = IdCol() #: The service that will be used by the production. service = Reference(service_id, 'Service.id') order_id = IdCol() #: The :class:`ProductionOrder` of this service. order = Reference(order_id, 'ProductionOrder.id') #: The service's quantity. quantity = QuantityCol(default=1) # # IDescribable Implementation # def get_description(self): return self.service.sellable.get_description() # Properties @property def unit_description(self): return self.service.sellable.unit_description @property def sellable(self): return self.service.sellable
class ReturnedSaleItem(Domain): __storm_table__ = 'returned_sale_item' quantity = QuantityCol(default=0) price = PriceCol() sale_item_id = IntCol() sale_item = Reference(sale_item_id, SaleItem.id) sellable_id = IntCol() returned_sale_id = IntCol()
class StockDecreaseItem(Domain): """An item in a stock decrease object. Note that objects of this type should not be created manually, only by calling :meth:`StockDecrease.add_sellable` """ __storm_table__ = 'stock_decrease_item' stock_decrease_id = IntCol(default=None) #: The stock decrease this item belongs to stock_decrease = Reference(stock_decrease_id, 'StockDecrease.id') sellable_id = IntCol() #: the |sellable| for this decrease sellable = Reference(sellable_id, 'Sellable.id') batch_id = IntCol() #: If the sellable is a storable, the |batch| that it was removed from batch = Reference(batch_id, 'StorableBatch.id') #: the cost of the |sellable| on the moment this decrease was created cost = PriceCol(default=0) #: the quantity decreased for this item quantity = QuantityCol() def __init__(self, store=None, **kw): if not 'kw' in kw: if not 'sellable' in kw: raise TypeError('You must provide a sellable argument') Domain.__init__(self, store=store, **kw) def decrease(self, branch): assert branch storable = self.sellable.product_storable if storable: storable.decrease_stock(self.quantity, branch, StockTransactionHistory.TYPE_STOCK_DECREASE, self.id, cost_center=self.stock_decrease.cost_center) # # Accessors # def get_total(self): return currency(self.cost * self.quantity) def get_quantity_unit_string(self): return u"%s %s" % (self.quantity, self.sellable.get_unit_description()) def get_description(self): return self.sellable.get_description()
class InvoiceItemCofins(BaseCOFINS): """Invoice of COFINS tax.""" __storm_table__ = 'invoice_item_cofins' #: Value of COFINS tax v_cofins = PriceCol(default=0) #: Value of the COFINS tax calculation basis. v_bc = PriceCol(default=None) #: Quantity sold q_bc_prod = QuantityCol(default=None) # # Public API # def set_initial_values(self, invoice_item): self.update_values(invoice_item) def update_values(self, invoice_item): self.q_bc_prod = invoice_item.quantity # When the CST is contained in the list the calculation is not performed # because the taxpayer is exempt. if self.cst in [4, 5, 6, 7, 8, 9]: return # When the branch is Simples Nacional (CRT 1 or 2) and the cofins is 99, # the values should be zero if self.cst == 99 and invoice_item.parent.branch.crt in [1, 2]: self.v_bc = 0 self.p_cofins = 0 self.v_cofins = 0 return cost = self._get_item_cost(invoice_item) self.v_bc = invoice_item.quantity * (invoice_item.price - cost) if self.p_cofins is not None: self.v_cofins = self.v_bc * self.p_cofins / 100 @classmethod def get_tax_template(cls, invoice_item): return invoice_item.sellable.product.cofins_template # # Private API # def _get_item_cost(self, invoice_item): from stoqlib.domain.sale import SaleItem if isinstance(invoice_item, SaleItem): return invoice_item.average_cost return 0
class StockTransactionHistory(Domain): __storm_table__ = 'stock_transaction_history' TYPE_IMPORTED = 15 product_stock_item_id = IntCol() stock_cost = PriceCol() quantity = QuantityCol() responsible_id = IntCol() date = DateTimeCol() object_id = IntCol() type = IntCol()
class ProductComponent(Domain): """A |product| and it's related |component| eg other product See also: `schema <http://doc.stoq.com.br/schema/tables/product_component.html>`__ """ __storm_table__ = 'product_component' quantity = QuantityCol(default=Decimal(1)) product_id = IdCol() product = Reference(product_id, 'Product.id') component_id = IdCol() component = Reference(component_id, 'Product.id') design_reference = UnicodeCol(default=u'')
class InvoiceItemPis(BasePIS): """Invoice of PIS tax.""" __storm_table__ = 'invoice_item_pis' #: Value of PIS tax. v_pis = PriceCol(default=0) #: Value of the PIS tax calculation basis. v_bc = PriceCol(default=None) #: Quantity sold q_bc_prod = QuantityCol(default=None) # # Public API # def set_initial_values(self, invoice_item): self.update_values(invoice_item) def update_values(self, invoice_item): self.q_bc_prod = invoice_item.quantity # When the CST is contained in the list the calculation is not performed # because the taxpayer is exempt. if self.cst in [4, 5, 6, 7, 8, 9]: return cost = self._get_item_cost(invoice_item) self.v_bc = invoice_item.quantity * (invoice_item.price - cost) if self.p_pis is not None: self.v_pis = self.v_bc * self.p_pis / 100 @classmethod def get_tax_template(cls, invoice_item): return invoice_item.sellable.product.pis_template # # Private API # def _get_item_cost(self, invoice_item): from stoqlib.domain.sale import SaleItem if isinstance(invoice_item, SaleItem): return invoice_item.average_cost return 0
class BaseIPI(BaseTax): CALC_ALIQUOTA = u'aliquot' CALC_UNIDADE = u'unit' cl_enq = UnicodeCol(default=u'') cnpj_prod = UnicodeCol(default=u'') c_selo = UnicodeCol(default=u'') q_selo = IntCol(default=None) c_enq = UnicodeCol(default=u'') cst = IntCol(default=None) p_ipi = PercentCol(default=None) q_unid = QuantityCol(default=None) calculo = EnumCol(default=CALC_ALIQUOTA, allow_none=False)
class ProductStockItem(Domain): """Class that makes a reference to the |product| stock of a certain |branch|. See also: `schema <http://doc.stoq.com.br/schema/tables/product_stock_item.html>`__ """ __storm_table__ = 'product_stock_item' #: the average stock price, will be updated as new stock items are #: received. stock_cost = PriceCol(default=0) #: number of storables in the stock item quantity = QuantityCol(default=0) branch_id = IdCol() #: the |branch| this stock item belongs to branch = Reference(branch_id, 'Branch.id') storable_id = IdCol() #: the |storable| the stock item refers to storable = Reference(storable_id, 'Storable.id') batch_id = IdCol() #: The |batch| that the storable is in. batch = Reference(batch_id, 'StorableBatch.id') def update_cost(self, new_quantity, new_cost): """Update the stock_item according to new quantity and cost. :param new_quantity: The new quantity added to stock. :param new_cost: The cost of one unit of the added stock. """ total_cost = self.quantity * self.stock_cost total_cost += new_quantity * new_cost total_items = self.quantity + new_quantity self.stock_cost = total_cost / total_items
class PurchaseItem(Domain): """This class stores information of the purchased items. """ __storm_table__ = 'purchase_item' quantity = QuantityCol(default=1) quantity_received = QuantityCol(default=0) quantity_sold = QuantityCol(default=0) quantity_returned = QuantityCol(default=0) #: the cost which helps the purchaser to define the #: main cost of a certain product. base_cost = PriceCol() cost = PriceCol() #: The ICMS ST value for the product purchased icms_st_value = PriceCol(default=0) #: The IPI value for the product purchased ipi_value = PriceCol(default=0) expected_receival_date = DateTimeCol(default=None) sellable_id = IdCol() #: the |sellable| sellable = Reference(sellable_id, 'Sellable.id') order_id = IdCol() #: the |purchase| order = Reference(order_id, 'PurchaseOrder.id') parent_item_id = IdCol() parent_item = Reference(parent_item_id, 'PurchaseItem.id') children_items = ReferenceSet('id', 'PurchaseItem.parent_item_id') def __init__(self, store=None, **kw): if not 'sellable' in kw: raise TypeError('You must provide a sellable argument') if not 'order' in kw: raise TypeError('You must provide a order argument') # FIXME: Avoding shadowing sellable.cost kw['base_cost'] = kw['sellable'].cost if not 'cost' in kw: kw['cost'] = kw['sellable'].cost Domain.__init__(self, store=store, **kw) # # Accessors # def get_total(self): return currency(self.quantity * self.cost) def get_total_sold(self): return currency(self.quantity_sold * self.cost) def get_received_total(self): return currency(self.quantity_received * self.cost) def has_been_received(self): return self.quantity_received >= self.quantity def has_partial_received(self): return self.quantity_received > 0 def get_pending_quantity(self): return self.quantity - self.quantity_received def get_quantity_as_string(self): unit = self.sellable.unit return u"%s %s" % (format_quantity(self.quantity), unit and unit.description or u"") def get_quantity_received_as_string(self): unit = self.sellable.unit return u"%s %s" % (format_quantity(self.quantity_received), unit and unit.description or u"") @classmethod def get_ordered_quantity(cls, store, sellable): """Returns the quantity already ordered of a given sellable. :param store: a store :param sellable: the sellable we want to know the quantity ordered. :returns: the quantity already ordered of a given sellable or zero if no quantity have been ordered. """ query = And(PurchaseItem.sellable_id == sellable.id, PurchaseOrder.id == PurchaseItem.order_id, PurchaseOrder.status == PurchaseOrder.ORDER_CONFIRMED) ordered_items = store.find(PurchaseItem, query) return ordered_items.sum(PurchaseItem.quantity) or Decimal(0) def return_consignment(self, quantity): """ Return this as a consignment item :param quantity: the quantity to return """ storable = self.sellable.product_storable assert storable storable.decrease_stock(quantity=quantity, branch=self.order.branch, type=StockTransactionHistory.TYPE_CONSIGNMENT_RETURNED, object_id=self.id) def get_component_quantity(self, parent): """Get the quantity of a component. :param parent: the |purchase_item| parent_item of self :returns: the quantity of the component """ for component in parent.sellable.product.get_components(): if self.sellable.product == component.component: return component.quantity
class WorkOrderItem(Domain): """A |workorder| item This is an item in a |workorder|. That is, a |product| or a |service| (here referenced by their respective |sellable|) used on the work and that will be after used to compose the |saleitem| of the |sale|. Note that objects of this type should not be created manually, only by calling :meth:`WorkOrder.add_sellable` See also: `schema <http://doc.stoq.com.br/schema/tables/work_order_item.html>`__ """ __storm_table__ = 'work_order_item' #: |sellable|'s quantity used on the |workorder| quantity = QuantityCol(default=0) #: price of the |sellable|, this is how much the |client| is going #: to be charged for the sellable. This includes discounts and markup. price = PriceCol() sellable_id = IntCol() #: the |sellable| of this item, either a |service| or a |product| sellable = Reference(sellable_id, 'Sellable.id') batch_id = IntCol() #: If the sellable is a storable, the |batch| that it was removed from batch = Reference(batch_id, 'StorableBatch.id') order_id = IntCol() #: |workorder| this item belongs order = Reference(order_id, 'WorkOrder.id') @property def total(self): """The total value for this item Note that this is the same as :obj:`.quantity` * :obj:`.price` """ return currency(self.price * self.quantity) def __init__(self, *args, **kwargs): self._original_quantity = 0 super(WorkOrderItem, self).__init__(*args, **kwargs) def __storm_loaded__(self): super(WorkOrderItem, self).__storm_loaded__() self._original_quantity = self.quantity # # Public API # def sync_stock(self): """Synchronizes the stock, increasing/decreasing it accordingly. When setting :obj:`~.quantity` be sure to call this to properly synchronize the stock (increase or decrease it). That counts for object creation too. """ storable = self.sellable.product_storable if not storable: # Not a product return diff_quantity = self._original_quantity - self.quantity if diff_quantity > 0: storable.increase_stock( diff_quantity, self.order.branch, StockTransactionHistory.TYPE_WORK_ORDER_USED, self.id) elif diff_quantity < 0: diff_quantity = -diff_quantity storable.decrease_stock( diff_quantity, self.order.branch, StockTransactionHistory.TYPE_WORK_ORDER_USED, self.id) # Reset the values used to calculate the stock quantity, just like # when the object as loaded from the database again. self._original_quantity = self.quantity
class InvoiceItemCofins(BaseCOFINS): """Invoice of COFINS tax.""" __storm_table__ = 'invoice_item_cofins' COFINS_NAO_CUMULATIVO_PADRAO = Decimal('7.6') #: Value of COFINS tax v_cofins = PriceCol(default=0) #: Value of the COFINS tax calculation basis. v_bc = PriceCol(default=None) #: Quantity sold q_bc_prod = QuantityCol(default=None) # # Public API # def set_initial_values(self, invoice_item): self.update_values(invoice_item) def update_values(self, invoice_item): self.q_bc_prod = invoice_item.quantity # When the CST is contained in the list the calculation is not performed # because the taxpayer is exempt. if self.cst in [4, 5, 6, 7, 8, 9]: return # When the branch is Simples Nacional (CRT 1 or 2) and the cofins is 99, # the values should be zero if self.cst == 99 and invoice_item.parent.branch.crt in [1, 2]: self.v_bc = 0 self.p_cofins = 0 self.v_cofins = 0 return cost = self._get_item_cost(invoice_item) if self.p_cofins == self.COFINS_NAO_CUMULATIVO_PADRAO: # Regime de incidencia não cumulativa self.v_bc = quantize(invoice_item.quantity * (invoice_item.price - cost)) else: # Regime de incidencia cumulativa self.v_bc = quantize(invoice_item.quantity * invoice_item.price) if self.p_cofins is not None: self.v_cofins = quantize(self.v_bc * self.p_cofins / 100) @classmethod def get_tax_template(cls, invoice_item): default_cofins = sysparam.get_object(invoice_item.store, 'DEFAULT_PRODUCT_COFINS_TEMPLATE') # FIXME: Allow use COFINS templates in services if invoice_item.sellable.service: return default_cofins return invoice_item.sellable.product.cofins_template or default_cofins # # Private API # def _get_item_cost(self, invoice_item): from stoqlib.domain.sale import SaleItem if isinstance(invoice_item, SaleItem): return invoice_item.average_cost return 0
class ProductionMaterial(Domain): """Production Material object implementation. This represents the material needed by a production. It can either be consumed or lost (due to manufacturing process). """ __storm_table__ = 'production_material' product_id = IdCol() #: The |product| that will be consumed. product = Reference(product_id, 'Product.id') order_id = IdCol() #: The |production| that will consume this material. order = Reference(order_id, 'ProductionOrder.id') # The quantity needed of this material. needed = QuantityCol(default=1) #: The quantity that is actually allocated to this production. It may be #: more than the quantity required (and in this case, the remaining quantity #: will be returned to the stock later. allocated = QuantityCol(default=0) #: The quantity already used of this material. consumed = QuantityCol(default=0) #: The quantity lost of this material. lost = QuantityCol(default=0) #: The quantity to purchase of this material. to_purchase = QuantityCol(default=0) #: The quantity to manufacture of this material. to_make = QuantityCol(default=0) # # Public API # def can_add_lost(self, quantity): """Returns if we can loose a certain quantity of this material. :param quantity: the quantity that will be lost. """ return self.can_consume(quantity) def can_consume(self, quantity): assert quantity > 0 if self.order.status != ProductionOrder.ORDER_PRODUCING: return False return self.lost + quantity <= self.needed - self.consumed def allocate(self, quantity=None): """Allocates the needed quantity of this material by decreasing the stock quantity. If no quantity was specified, it will decrease all the stock needed or the maximum quantity available. Otherwise, allocate the quantity specified or raise a ValueError exception, if the quantity is not available. :param quantity: the quantity to be allocated or None to allocate the maximum quantity possible. """ storable = self.product.storable # If there is no storable for the product, than we just need to allocate # what is necessary if not storable: self.allocated = self.needed return stock = self.get_stock_quantity() if quantity is None: required = self.needed - self.allocated if stock > required: quantity = required else: quantity = stock elif quantity > stock: raise ValueError(_(u'Can not allocate this quantity.')) if quantity > 0: self.allocated += quantity storable.decrease_stock( quantity, self.order.branch, StockTransactionHistory.TYPE_PRODUCTION_ALLOCATED, self.id) def return_remaining(self): """Returns remaining allocated material to the stock This should be called only after the production order is closed. """ assert self.order.status == ProductionOrder.ORDER_CLOSED remaining = self.allocated - self.lost - self.consumed assert remaining >= 0 if not remaining: return storable = self.product.storable if not storable: return storable.increase_stock( remaining, self.order.branch, StockTransactionHistory.TYPE_PRODUCTION_RETURNED, self.id) self.allocated -= remaining def add_lost(self, quantity): """Adds the quantity lost of this material. The maximum quantity that can be lost is given by the formula:: - max_lost(quantity) = needed - consumed - lost - quantity :param quantity: the quantity that was lost. """ assert quantity > 0 if self.lost + quantity > self.needed - self.consumed: raise ValueError(_(u'Cannot loose this quantity.')) required = self.consumed + self.lost + quantity if required > self.allocated: self.allocate(required - self.allocated) self.lost += quantity store = self.store ProductHistory.add_lost_item(store, self.order.branch, self) def consume(self, quantity): """Consumes a certain quantity of material. The maximum quantity allowed to be consumed is given by the following formula: - max_consumed(quantity) = needed - consumed - lost - quantity :param quantity: the quantity to be consumed. """ assert quantity > 0 available = self.allocated - self.consumed - self.lost if quantity > available: raise ValueError(_(u'Can not consume this quantity.')) required = self.consumed + self.lost + quantity if required > self.allocated: # pragma nocover self.allocate(required - self.allocated) self.consumed += quantity store = self.store ProductHistory.add_consumed_item(store, self.order.branch, self) # # IDescribable Implementation # def get_description(self): return self.product.sellable.get_description() # Accessors @property def unit_description(self): return self.product.sellable.unit_description def get_stock_quantity(self): storable = self.product.storable assert storable is not None return storable.get_balance_for_branch(self.order.branch)
class LoanItem(Domain): """An item in a :class:`loan <Loan>` Note that when changing :obj:`~.quantity`, :obj:`~.return_quantity` or :obj:`~.sale_quantity` you will need to call :meth:`.sync_stock` to synchronize the stock (increase or decrease it). Also note that objects of this type should never be created manually, only by calling :meth:`Loan.add_sellable` See also: `schema <http://doc.stoq.com.br/schema/tables/loan_item.html>`__ """ __storm_table__ = 'loan_item' #: The total quantity that was loaned. The product stock for this #: will be decreased when the loan stock is synchonized quantity = QuantityCol() #: The loadned quantity that was sold. Will increase stock so #: it's decreased correctly when the #: :class:`sale <stoqlib.domain.sale.Sale>` is confirmed sale_quantity = QuantityCol(default=Decimal(0)) #: The loaned quantity that was returned. Will increase stock return_quantity = QuantityCol(default=Decimal(0)) #: price to use for this :obj:`~.sellable` when creating #: a :class:`sale <stoqlib.domain.sale.Sale>` price = PriceCol() #: original price of a sellable base_price = PriceCol() sellable_id = IdCol(allow_none=False) #: :class:`sellable <stoqlib.domain.sellable.Sellable>` that is loaned #: cannot be *None* sellable = Reference(sellable_id, 'Sellable.id') batch_id = IdCol() #: If the sellable is a storable, the |batch| that it was returned in batch = Reference(batch_id, 'StorableBatch.id') loan_id = IdCol() #: :class:`loan <Loan>` this item belongs to loan = Reference(loan_id, 'Loan.id') icms_info_id = IdCol() #: the :class:`stoqlib.domain.taxes.InvoiceItemIcms` tax for *self* icms_info = Reference(icms_info_id, 'InvoiceItemIcms.id') ipi_info_id = IdCol() #: the :class:`stoqlib.domain.taxes.InvoiceItemIpi` tax for *self* ipi_info = Reference(ipi_info_id, 'InvoiceItemIpi.id') pis_info_id = IdCol() #: the :class:`stoqlib.domain.taxes.InvoiceItemPis` tax for *self* pis_info = Reference(pis_info_id, 'InvoiceItemPis.id') cofins_info_id = IdCol() #: the :class:`stoqlib.domain.taxes.InvoiceItemCofins` tax for *self* cofins_info = Reference(cofins_info_id, 'InvoiceItemCofins.id') def __init__(self, *args, **kwargs): # stores the total quantity that was loaned before synching stock self._original_quantity = 0 # stores the loaned quantity that was returned before synching stock self._original_return_quantity = self.return_quantity check_tax_info_presence(kwargs, kwargs.get('store')) super(LoanItem, self).__init__(*args, **kwargs) product = self.sellable.product if product: self.ipi_info.set_item_tax(self) self.icms_info.set_item_tax(self) self.pis_info.set_item_tax(self) self.cofins_info.set_item_tax(self) def __storm_loaded__(self): super(LoanItem, self).__storm_loaded__() self._original_quantity = self.quantity self._original_return_quantity = self.return_quantity @property def branch(self): return self.loan.branch @property def storable(self): return self.sellable.product_storable # # IInvoiceItem implementation # @property def parent(self): return self.loan @property def item_discount(self): if self.price < self.base_price: return self.base_price - self.price return Decimal('0') @property def cfop_code(self): return u'5917' def sync_stock(self): """Synchronizes the stock, increasing/decreasing it accordingly. Using the stored values when this object is created/loaded, compute how much we should increase or decrease the stock quantity. When setting :obj:`~.quantity`, :obj:`~.return_quantity` or :obj:`~.sale_quantity` be sure to call this to properly synchronize the stock (increase or decrease it). That counts for object creation too. """ loaned = self._original_quantity - self.quantity returned = self.return_quantity - self._original_return_quantity diff_quantity = loaned + returned if diff_quantity > 0: self.storable.increase_stock( diff_quantity, self.branch, StockTransactionHistory.TYPE_RETURNED_LOAN, self.id, batch=self.batch) elif diff_quantity < 0: diff_quantity = -diff_quantity self.storable.decrease_stock(diff_quantity, self.branch, StockTransactionHistory.TYPE_LOANED, self.id, batch=self.batch) # Reset the values used to calculate the stock quantity, just like # when the object as loaded from the database again. self._original_quantity = self.quantity self._original_return_quantity = self.return_quantity def get_remaining_quantity(self): """The remaining quantity that wasn't returned/sold yet This is the same as :obj:`.quantity` - :obj:`.sale_quantity` - :obj:`.return_quantity` """ return self.quantity - self.sale_quantity - self.return_quantity def get_quantity_unit_string(self): return u"%s %s" % (self.quantity, self.sellable.unit_description) def get_total(self): return currency(self.price * self.quantity) def set_discount(self, discount): """Apply *discount* on this item Note that the discount will be applied based on :obj:`.base_price` and then substitute :obj:`.price`, making any previous discount/surcharge being lost :param decimal.Decimal discount: the discount to be applied as a percentage, e.g. 10.0, 22.5 """ self.price = quantize(self.base_price * (1 - discount / 100))
class TransferOrderItem(Domain): """Transfer order item """ __storm_table__ = 'transfer_order_item' sellable_id = IdCol() # FIXME: This should be a product, since it does not make sense to transfer # serviçes #: The |sellable| to transfer sellable = Reference(sellable_id, 'Sellable.id') batch_id = IdCol() #: If the sellable is a storable, the |batch| that was transfered batch = Reference(batch_id, 'StorableBatch.id') transfer_order_id = IdCol() #: The |transfer| this item belongs to transfer_order = Reference(transfer_order_id, 'TransferOrder.id') #: The quantity to transfer quantity = QuantityCol() #: Average cost of the item in the source branch at the time of transfer. stock_cost = PriceCol(default=0) icms_info_id = IdCol() #: the :class:`stoqlib.domain.taxes.InvoiceItemIcms` tax for *self* icms_info = Reference(icms_info_id, 'InvoiceItemIcms.id') ipi_info_id = IdCol() #: the :class:`stoqlib.domain.taxes.InvoiceItemIpi` tax for *self* ipi_info = Reference(ipi_info_id, 'InvoiceItemIpi.id') item_discount = Decimal('0') def __init__(self, store=None, **kwargs): if not 'sellable' in kwargs: raise TypeError('You must provide a sellable argument') kwargs['ipi_info'] = InvoiceItemIpi(store=store) kwargs['icms_info'] = InvoiceItemIcms(store=store) super(TransferOrderItem, self).__init__(store=store, **kwargs) product = self.sellable.product if product: self.ipi_info.set_item_tax(self) self.icms_info.set_item_tax(self) # # IInvoiceItem implementation # @property def parent(self): return self.transfer_order @property def base_price(self): return self.stock_cost @property def price(self): return self.stock_cost @property def nfe_cfop_code(self): source_branch = self.transfer_order.source_branch source_address = source_branch.person.get_main_address() destination_branch = self.transfer_order.destination_branch destination_address = destination_branch.person.get_main_address() same_state = True if (source_address.city_location.state != destination_address.city_location.state): same_state = False if same_state: return u'5152' else: return u'6152' # # Public API # def get_total(self): """Returns the total cost of a transfer item eg quantity * cost""" return self.quantity * self.sellable.cost def send(self): """Sends this item to it's destination |branch|. This method should never be used directly, and to send a transfer you should use TransferOrder.send(). """ product = self.sellable.product if product.manage_stock: storable = product.storable storable.decrease_stock(self.quantity, self.transfer_order.source_branch, StockTransactionHistory.TYPE_TRANSFER_TO, self.id, batch=self.batch) ProductHistory.add_transfered_item(self.store, self.transfer_order.source_branch, self) def receive(self): """Receives this item, increasing the quantity in the stock. This method should never be used directly, and to receive a transfer you should use TransferOrder.receive(). """ product = self.sellable.product if product.manage_stock: storable = product.storable storable.increase_stock(self.quantity, self.transfer_order.destination_branch, StockTransactionHistory.TYPE_TRANSFER_FROM, self.id, unit_cost=self.stock_cost, batch=self.batch)
class ProductionItem(Domain): """Production Item object implementation. """ __storm_table__ = 'production_item' #: The product's quantity that will be manufactured. quantity = QuantityCol(default=1) #: The product's quantity that was manufactured. produced = QuantityCol(default=0) #: The product's quantity that was lost. lost = QuantityCol(default=0) order_id = IdCol() #: The :class:`ProductionOrder` of this item. order = Reference(order_id, 'ProductionOrder.id') product_id = IdCol() #: The product that will be manufactured. product = Reference(product_id, 'Product.id') def get_description(self): return self.product.sellable.get_description() # # Properties # @property def unit_description(self): return self.product.sellable.unit_description @property def sellable(self): return self.product.sellable # # Private API # def _get_material_from_component(self, component): store = self.store return store.find(ProductionMaterial, product=component.component, order=self.order).one() # # Public API # def get_components(self): return self.product.get_components() def can_produce(self, quantity): """Returns if we can produce a certain quantity. We can produce a quantity items until we reach the total quantity that will be manufactured minus the quantity that was lost. :param quantity: the quantity that will be produced. """ assert quantity > 0 if self.order.status != ProductionOrder.ORDER_PRODUCING: return False return self.produced + quantity + self.lost <= self.quantity def is_completely_produced(self): return self.quantity == self.produced + self.lost def produce(self, quantity, produced_by=None, serials=None): """Sets a certain quantity as produced. The quantity will be marked as produced only if there are enough materials allocated, otherwise a ValueError exception will be raised. :param quantity: the quantity that will be produced. """ assert self.can_produce(quantity) # check if its ok to produce before consuming material if self.product.has_quality_tests(): # We have some quality tests to assure. Register it for later assert produced_by assert len(serials) == quantity # We only support yield quantity > 1 when there are no tests assert self.product.yield_quantity == 1 self.store.savepoint(u'before_produce') for component in self.get_components(): material = self._get_material_from_component(component) needed_material = quantity * component.quantity try: material.consume(needed_material) except ValueError: self.store.rollback_to_savepoint(u'before_produce') raise if self.product.has_quality_tests(): for serial in serials: ProductionProducedItem(store=self.store, order=self.order, product=self.product, produced_by=produced_by, produced_date=localnow(), serial_number=serial, entered_stock=False) else: # There are no quality tests for this product. Increase stock # right now. storable = self.product.storable # A production process may yield more than one unit of this product yield_quantity = quantity * self.product.yield_quantity storable.increase_stock( yield_quantity, self.order.branch, StockTransactionHistory.TYPE_PRODUCTION_PRODUCED, self.id) self.produced += quantity self.order.try_finalize_production() ProductHistory.add_produced_item(self.store, self.order.branch, self) def add_lost(self, quantity): """Adds a quantity that was lost. The maximum quantity that can be lost is the total quantity minus the quantity already produced. :param quantity: the quantity that was lost. """ if self.lost + quantity > self.quantity - self.produced: raise ValueError( _(u'Can not lost more items than the total production quantity.' )) store = self.store store.savepoint(u'before_lose') for component in self.get_components(): material = self._get_material_from_component(component) try: material.add_lost(quantity * component.quantity) except ValueError: store.rollback_to_savepoint(u'before_lose') raise self.lost += quantity self.order.try_finalize_production() ProductHistory.add_lost_item(store, self.order.branch, self)
class ReturnedSaleItem(Domain): """An item of a :class:`returned sale <ReturnedSale>` Note that objects of this type should never be created manually, only by calling :meth:`Sale.create_sale_return_adapter` """ __storm_table__ = 'returned_sale_item' #: the returned quantity quantity = QuantityCol(default=0) #: The price which this :obj:`.sale_item` was sold. #: When creating this object, if *price* is not passed to the #: contructor, it defaults to :obj:`.sale_item.price` or #: :obj:`.sellable.price` price = PriceCol() sale_item_id = IdCol(default=None) #: the returned |saleitem| sale_item = Reference(sale_item_id, 'SaleItem.id') sellable_id = IdCol() #: The returned |sellable| #: Note that if :obj:`.sale_item` != ``None``, this is the same as #: :obj:`.sale_item.sellable` sellable = Reference(sellable_id, 'Sellable.id') batch_id = IdCol() #: If the sellable is a storable, the |batch| that it was removed from batch = Reference(batch_id, 'StorableBatch.id') returned_sale_id = IdCol() #: the |returnedsale| which this item belongs returned_sale = Reference(returned_sale_id, 'ReturnedSale.id') #: Id of ICMS tax in product tax template icms_info_id = IdCol() #: the :class:`stoqlib.domain.taxes.InvoiceItemIcms` tax for *self* icms_info = Reference(icms_info_id, 'InvoiceItemIcms.id') #: Id of IPI tax in product tax template ipi_info_id = IdCol() #: the :class:`stoqlib.domain.taxes.InvoiceItemIpi` tax fo *self* ipi_info = Reference(ipi_info_id, 'InvoiceItemIpi.id') #: Id of PIS tax in product tax template pis_info_id = IdCol() #: the :class:`stoqlib.domain.taxes.InvoiceItemPis` tax fo *self* pis_info = Reference(pis_info_id, 'InvoiceItemPis.id') #: Id of COFINS tax in product tax template cofins_info_id = IdCol() #: the :class:`stoqlib.domain.taxes.InvoiceItemCofins` tax fo *self* cofins_info = Reference(cofins_info_id, 'InvoiceItemCofins.id') item_discount = Decimal('0') parent_item_id = IdCol() parent_item = Reference(parent_item_id, 'ReturnedSaleItem.id') children_items = ReferenceSet('id', 'ReturnedSaleItem.parent_item_id') def __init__(self, store=None, **kwargs): # TODO: Add batch logic here. (get if from sale_item or check if was # passed togheter with sellable) sale_item = kwargs.get('sale_item') sellable = kwargs.get('sellable') if not sale_item and not sellable: raise ValueError( "A sale_item or a sellable is mandatory to create this object") elif sale_item and sellable and sale_item.sellable != sellable: raise ValueError("sellable must be the same as sale_item.sellable") elif sale_item and not sellable: sellable = sale_item.sellable kwargs['sellable'] = sellable if not 'price' in kwargs: # sale_item.price takes priority over sellable.price kwargs['price'] = sale_item.price if sale_item else sellable.price check_tax_info_presence(kwargs, store) super(ReturnedSaleItem, self).__init__(store=store, **kwargs) product = self.sellable.product if product: self.ipi_info.set_item_tax(self) self.icms_info.set_item_tax(self) self.pis_info.set_item_tax(self) self.cofins_info.set_item_tax(self) @property def total(self): """The total being returned This is the same as :obj:`.price` * :obj:`.quantity` """ return self.price * self.quantity # # IInvoiceItem implementation # @property def base_price(self): return self.price @property def parent(self): return self.returned_sale @property def nfe_cfop_code(self): sale = self.returned_sale.sale client_address = sale.client.person.get_main_address() branch_address = sale.branch.person.get_main_address() same_state = True if branch_address.city_location.state != client_address.city_location.state: same_state = False if same_state: return u'1202' else: return u'2202' # # Public API # def get_total(self): return self.total def return_(self, branch): """Do the real return of this item When calling this, the real return will happen, that is, if :obj:`.sellable` is a |product|, it's stock will be increased on *branch*. """ storable = self.sellable.product_storable if storable: storable.increase_stock(self.quantity, branch, StockTransactionHistory.TYPE_RETURNED_SALE, self.id, batch=self.batch) if self.sale_item: self.sale_item.quantity_decreased -= self.quantity def undo(self): """Undo this item return. This is the oposite of the return, ie, the item will be removed back from stock and the sale item decreased quantity will be restored. """ storable = self.sellable.product_storable if storable: storable.decrease_stock( self.quantity, self.returned_sale.branch, # FIXME: Create a new type StockTransactionHistory.TYPE_RETURNED_SALE, self.id, batch=self.batch) if self.sale_item: self.sale_item.quantity_decreased += self.quantity def get_component_quantity(self, parent): for component in parent.sellable.product.get_components(): if self.sellable.product == component.component: return component.quantity
class StockTransactionHistory(Domain): """ This class stores information about all transactions made in the stock Everytime a |storable| has its stock increased or decreased, an object of this class will be created, registering the quantity, cost, responsible and reason for the transaction """ __storm_table__ = 'stock_transaction_history' #: the transaction is an initial stock adustment. Note that with this #: transaction, there is no related object. TYPE_INITIAL = 0 #: the transaction is a sale TYPE_SELL = 1 #: the transaction is a return of a sale TYPE_RETURNED_SALE = 2 #: the transaction is the cancellation of a sale TYPE_CANCELED_SALE = 3 #: the transaction is the receival of a purchase TYPE_RECEIVED_PURCHASE = 4 #: the transaction is the return of a loan TYPE_RETURNED_LOAN = 5 #: the transaction is a loan TYPE_LOANED = 6 #: the transaction is the allocation of a product to a production TYPE_PRODUCTION_ALLOCATED = 7 #: the transaction is the production of a product TYPE_PRODUCTION_PRODUCED = 8 #: the transaction is the return of an item from a production TYPE_PRODUCTION_RETURNED = 9 #: the transaction is a stock decrease TYPE_STOCK_DECREASE = 10 #: the transaction is a transfer from a branch TYPE_TRANSFER_FROM = 11 #: the transaction is a transfer to a branch TYPE_TRANSFER_TO = 12 #: the transaction is the adjustment of an inventory TYPE_INVENTORY_ADJUST = 13 #: the transaction is the production of a product that didn't enter #: stock right after its creation TYPE_PRODUCTION_SENT = 14 #: this transaction was created automatically by an upgrade from a previous #: version of Stoq, when this history didn't exist. TYPE_IMPORTED = 15 #: the transaction is the return of a product to another company (ie, this #: shop is giving the product back to the other company). TYPE_CONSIGNMENT_RETURNED = 16 #: the transaction is the utilization of a #: |product|/|service| on a |workorderitem| TYPE_WORK_ORDER_USED = 17 types = {TYPE_INVENTORY_ADJUST: _(u'Adjustment for inventory %s'), TYPE_RETURNED_LOAN: _(u'Returned from loan %s'), TYPE_LOANED: _(u'Loaned for loan %s'), TYPE_PRODUCTION_ALLOCATED: _(u'Allocated for production %s'), TYPE_PRODUCTION_PRODUCED: _(u'Produced in production %s'), TYPE_PRODUCTION_SENT: _(u'Produced in production %s'), TYPE_PRODUCTION_RETURNED: _(u'Returned remaining from ' u'production %s'), TYPE_RECEIVED_PURCHASE: _(u'Received for receiving order %s'), TYPE_RETURNED_SALE: _(u'Returned sale %s'), TYPE_CANCELED_SALE: _(u'Returned from canceled sale %s'), TYPE_SELL: _(u'Sold in sale %s'), TYPE_STOCK_DECREASE: _(u'Product removal for stock decrease %s'), TYPE_TRANSFER_FROM: _(u'Transfered from branch in transfer ' u'order %s'), TYPE_TRANSFER_TO: _(u'Transfered to this branch in transfer ' u'order %s'), TYPE_INITIAL: _(u'Registred initial stock'), TYPE_IMPORTED: _(u'Imported from previous version'), TYPE_CONSIGNMENT_RETURNED: _(u'Consigned product returned.'), TYPE_WORK_ORDER_USED: _(u'Used on work order %s.'), } #: the date and time the transaction was made date = DateTimeCol(default_factory=localnow) product_stock_item_id = IdCol() #: the |productstockitem| used in the transaction product_stock_item = Reference(product_stock_item_id, 'ProductStockItem.id') #: the stock cost of the transaction on the time it was made stock_cost = PriceCol() #: The quantity that was removed or added to the stock. #: Positive value if the value was increased, negative if decreased. quantity = QuantityCol() responsible_id = IdCol() #: the |loginuser| responsible for the transaction responsible = Reference(responsible_id, 'LoginUser.id') #: the id of the object who altered the stock object_id = IdCol() #: the type of the transaction type = IntCol() @property def total(self): return currency(abs(self.stock_cost * self.quantity)) def get_object(self): if self.type in [self.TYPE_INITIAL, self.TYPE_IMPORTED]: return None elif self.type in [self.TYPE_SELL, self.TYPE_CANCELED_SALE]: from stoqlib.domain.sale import SaleItem return self.store.get(SaleItem, self.object_id) elif self.type == self.TYPE_RETURNED_SALE: from stoqlib.domain.returnedsale import ReturnedSaleItem return self.store.get(ReturnedSaleItem, self.object_id) elif self.type == self.TYPE_PRODUCTION_PRODUCED: from stoqlib.domain.production import ProductionItem return self.store.get(ProductionItem, self.object_id) elif self.type in [self.TYPE_PRODUCTION_ALLOCATED, self.TYPE_PRODUCTION_RETURNED]: from stoqlib.domain.production import ProductionMaterial return self.store.get(ProductionMaterial, self.object_id) elif self.type == self.TYPE_RECEIVED_PURCHASE: from stoqlib.domain.receiving import ReceivingOrderItem return self.store.get(ReceivingOrderItem, self.object_id) elif self.type in [self.TYPE_RETURNED_LOAN, self.TYPE_LOANED]: from stoqlib.domain.loan import LoanItem return self.store.get(LoanItem, self.object_id) elif self.type == self.TYPE_STOCK_DECREASE: from stoqlib.domain.stockdecrease import StockDecreaseItem return self.store.get(StockDecreaseItem, self.object_id) elif self.type in [self.TYPE_TRANSFER_FROM, self.TYPE_TRANSFER_TO]: from stoqlib.domain.transfer import TransferOrderItem return self.store.get(TransferOrderItem, self.object_id) elif self.type == self.TYPE_INVENTORY_ADJUST: from stoqlib.domain.inventory import InventoryItem return self.store.get(InventoryItem, self.object_id) elif self.type == self.TYPE_PRODUCTION_SENT: from stoqlib.domain.production import ProductionProducedItem return self.store.get(ProductionProducedItem, self.object_id) elif self.type == self.TYPE_CONSIGNMENT_RETURNED: from stoqlib.domain.purchase import PurchaseItem return self.store.get(PurchaseItem, self.object_id) elif self.type == self.TYPE_CONSIGNMENT_RETURNED: from stoqlib.domain.purchase import PurchaseItem return self.store.get(PurchaseItem, self.object_id) elif self.type == self.TYPE_WORK_ORDER_USED: from stoqlib.domain.workorder import WorkOrderItem return self.store.get(WorkOrderItem, self.object_id) else: raise ValueError(_('%s has invalid type: %s.') % (self, self.type)) def get_object_parent(self): obj = self.get_object() if not obj: return None from stoqlib.domain.inventory import InventoryItem from stoqlib.domain.loan import LoanItem from stoqlib.domain.production import ProductionItem from stoqlib.domain.production import ProductionMaterial from stoqlib.domain.purchase import PurchaseItem from stoqlib.domain.receiving import ReceivingOrderItem from stoqlib.domain.returnedsale import ReturnedSaleItem from stoqlib.domain.sale import SaleItem from stoqlib.domain.stockdecrease import StockDecreaseItem from stoqlib.domain.workorder import WorkOrderItem from stoqlib.domain.transfer import TransferOrderItem if isinstance(obj, SaleItem): return obj.sale elif isinstance(obj, ReturnedSaleItem): return obj.returned_sale elif isinstance(obj, ProductionItem): return obj.order elif isinstance(obj, ProductionMaterial): return obj.order elif isinstance(obj, ReceivingOrderItem): return obj.receiving_order elif isinstance(obj, LoanItem): return obj.loan elif isinstance(obj, StockDecreaseItem): return obj.stock_decrease elif isinstance(obj, TransferOrderItem): return obj.transfer_order elif isinstance(obj, InventoryItem): return obj.inventory elif isinstance(obj, PurchaseItem): return obj.inventory elif isinstance(obj, WorkOrderItem): return obj.order else: raise TypeError(_('Object %s has invalid type (%s)') % (obj, type(obj))) def get_description(self): """ Based on the type of the transaction, returns the string description """ if self.type in [self.TYPE_INITIAL, self.TYPE_IMPORTED]: return self.types[self.type] object_parent = self.get_object_parent() number = unicode(object_parent.identifier) return self.types[self.type] % number
class InventoryItem(Domain): """The InventoryItem belongs to an Inventory. It contains the recorded quantity and the actual quantity related to a specific product. If those quantities are not identitical, it will also contain a reason and a cfop describing that. See also: `schema <http://doc.stoq.com.br/schema/tables/inventory_item.html>`__ """ __storm_table__ = 'inventory_item' product_id = IntCol() #: the item product = Reference(product_id, 'Product.id') batch_id = IntCol() #: If the product is a storable, the |batch| of the product that is being #: inventored batch = Reference(batch_id, 'StorableBatch.id') #: the recorded quantity of a product recorded_quantity = QuantityCol() #: the actual quantity of a product actual_quantity = QuantityCol(default=None) #: the product's cost when the product was adjusted. product_cost = PriceCol() #: the reason of why this item has been adjusted reason = UnicodeCol(default=u"") cfop_data_id = IntCol(default=None) #: the cfop used to adjust this item, this is only set when #: an adjustment is done cfop_data = Reference(cfop_data_id, 'CfopData.id') inventory_id = IntCol() #: the inventory process that contains this item inventory = Reference(inventory_id, 'Inventory.id') def _add_inventory_fiscal_entry(self, invoice_number): inventory = self.inventory return FiscalBookEntry( entry_type=FiscalBookEntry.TYPE_INVENTORY, invoice_number=inventory.invoice_number, branch=inventory.branch, cfop=self.cfop_data, store=self.store) def adjust(self, invoice_number): """Create an entry in fiscal book registering the adjustment with the related cfop data and change the product quantity available in stock. :param invoice_number: invoice number to register """ storable = self.product.storable if storable is None: raise TypeError( "The adjustment item must be a storable product.") adjustment_qty = self.get_adjustment_quantity() if not adjustment_qty: return elif adjustment_qty > 0: storable.increase_stock(adjustment_qty, self.inventory.branch, StockTransactionHistory.TYPE_INVENTORY_ADJUST, self.id) else: storable.decrease_stock(abs(adjustment_qty), self.inventory.branch, StockTransactionHistory.TYPE_INVENTORY_ADJUST, self.id) self._add_inventory_fiscal_entry(invoice_number) def adjusted(self): """Find out if this item has been adjusted. :returns: ``True`` if the item have already been adjusted, ``False`` otherwise. """ # We check reason and cfop_data attributes because they only # exist after the item be adjusted return self.reason and self.cfop_data def get_code(self): """Get the product code of this item :returns: the product code """ return self.product.sellable.code def get_description(self): """Returns the product description""" return self.product.sellable.get_description() def get_fiscal_description(self): """Returns a description of the product tax constant""" return self.product.sellable.tax_constant.get_description() def get_unit_description(self): """Returns the product unit description or None if it's not set """ sellable = self.product.sellable if sellable.unit: return sellable.unit.description def get_adjustment_quantity(self): """Returns the adjustment quantity, the actual quantity minus the recorded quantity or None if there is no actual quantity yet. """ if self.actual_quantity is not None: return self.actual_quantity - self.recorded_quantity def get_total_cost(self): """Returns the total cost of this item, the actual quantity multiplied by the product cost in the moment it was adjusted. If the item was not adjusted yet, the total cost will be zero. """ if not self.adjusted(): return Decimal(0) return self.product_cost * self.actual_quantity
class StockDecreaseItem(Domain): """An item in a stock decrease object. Note that objects of this type should not be created manually, only by calling :meth:`StockDecrease.add_sellable` """ __storm_table__ = 'stock_decrease_item' stock_decrease_id = IdCol(default=None) #: The stock decrease this item belongs to stock_decrease = Reference(stock_decrease_id, 'StockDecrease.id') sellable_id = IdCol() #: the |sellable| for this decrease sellable = Reference(sellable_id, 'Sellable.id') batch_id = IdCol() #: If the sellable is a storable, the |batch| that it was removed from batch = Reference(batch_id, 'StorableBatch.id') #: the cost of the |sellable| on the moment this decrease was created cost = PriceCol(default=0) #: the quantity decreased for this item quantity = QuantityCol() #: Id of ICMS tax in product tax template icms_info_id = IdCol() #:the :class:`stoqlib.domain.taxes.InvoiceItemIcms` tax for *self* icms_info = Reference(icms_info_id, 'InvoiceItemIcms.id') #: Id of IPI tax in product tax template ipi_info_id = IdCol() #: the :class:`stoqlib.domain.taxes.InvoiceItemIpi` tax for *self* ipi_info = Reference(ipi_info_id, 'InvoiceItemIpi.id') #: Id of PIS tax in product tax template pis_info_id = IdCol() #: the :class:`stoqlib.domain.taxes.InvoiceItemPis` tax for *self* pis_info = Reference(pis_info_id, 'InvoiceItemPis.id') #: Id of COFINS tax in product tax template cofins_info_id = IdCol() #: the :class:`stoqlib.domain.taxes.InvoiceItemCofins` tax for *self* cofins_info = Reference(cofins_info_id, 'InvoiceItemCofins.id') item_discount = Decimal('0') def __init__(self, store=None, sellable=None, **kwargs): if sellable is None: raise TypeError('You must provide a sellable argument') check_tax_info_presence(kwargs, store) super(StockDecreaseItem, self).__init__(store=store, sellable=sellable, **kwargs) product = self.sellable.product if product: self.ipi_info.set_item_tax(self) self.icms_info.set_item_tax(self) self.pis_info.set_item_tax(self) self.cofins_info.set_item_tax(self) # # Properties # @property def total_cost(self): return currency(self.cost * self.quantity) # # IInvoiceItem implementation # @property def parent(self): return self.stock_decrease @property def base_price(self): return self.cost @property def price(self): return self.cost @property def cfop_code(self): cfop = self.stock_decrease.cfop.code return cfop.replace('.', '') # # Public API # def decrease(self, branch): # FIXME: We should not be receiving a branch here. We should be using # self.stock_decrease.branch for that. assert branch storable = self.sellable.product_storable if storable: storable.decrease_stock( self.quantity, branch, StockTransactionHistory.TYPE_STOCK_DECREASE, self.id, cost_center=self.stock_decrease.cost_center, batch=self.batch) # # Accessors # def get_total(self): return currency(self.cost * self.quantity) def get_quantity_unit_string(self): unit = self.sellable.unit_description if unit: return u"%s %s" % (self.quantity, unit) return unicode(self.quantity) def get_description(self): return self.sellable.get_description()
class ProductHistory(Domain): """Stores |product| history, such as sold (via |sale|), received (via |receive|), transfered (via |transfer|) and decreased quantities. See also: `schema <http://doc.stoq.com.br/schema/tables/product_history.html>`__ .. note:: We keep a reference to |sellable| instead of |product| because we want to display the sellable id in the interface instead of the product id for consistency with interfaces that display both. """ __storm_table__ = 'product_history' quantity_sold = QuantityCol(default=None) quantity_received = QuantityCol(default=None) quantity_transfered = QuantityCol(default=None) quantity_produced = QuantityCol(default=None) quantity_consumed = QuantityCol(default=None) quantity_lost = QuantityCol(default=None) quantity_decreased = QuantityCol(default=None) production_date = DateTimeCol(default=None) sold_date = DateTimeCol(default=None) received_date = DateTimeCol(default=None) decreased_date = DateTimeCol(default=None) branch_id = IdCol() #: the |branch| branch = Reference(branch_id, 'Branch.id') sellable_id = IdCol() #: the |sellable| sellable = Reference(sellable_id, 'Sellable.id') @classmethod def add_sold_item(cls, store, branch, product_sellable_item): """Adds a |saleitem| to the history. *product_sale_item* is an item that was created during a |sale|. :param store: a store :param branch: the |branch| :param product_sellable_item: the |saleitem| for the sold |product| """ cls(branch=branch, sellable=product_sellable_item.sellable, quantity_sold=product_sellable_item.quantity, sold_date=TransactionTimestamp(), store=store) @classmethod def add_received_item(cls, store, branch, receiving_order_item): """ Adds a received item, populates the ProductHistory table using a *receiving_order_item* created during a |purchase| :param store: a store :param branch: the |branch| :param receiving_order_item: the item received for |purchase| """ cls(branch=branch, sellable=receiving_order_item.sellable, quantity_received=receiving_order_item.quantity, received_date=receiving_order_item.receiving_order.receival_date, store=store) @classmethod def add_transfered_item(cls, store, branch, transfer_order_item): """ Adds a transfered_item, populates the ProductHistory table using a *transfered_order_item* created during a |transfer|. :param store: a store :param branch: the source branch :param transfer_order_item: the item transfered from source branch """ cls(branch=branch, sellable=transfer_order_item.sellable, quantity_transfered=transfer_order_item.quantity, received_date=transfer_order_item.transfer_order.receival_date, store=store) @classmethod def add_consumed_item(cls, store, branch, consumed_item): """ Adds a consumed_item, populates the ProductHistory table using a production_material item that was used in a |production|. :param store: a store :param branch: the source branch :param retained_item: a ProductionMaterial instance """ cls(branch=branch, sellable=consumed_item.product.sellable, quantity_consumed=consumed_item.consumed, production_date=localtoday().date(), store=store) @classmethod def add_produced_item(cls, store, branch, produced_item): """ Adds a produced_item, populates the ProductHistory table using a production_item that was produced in a production order. :param store: a store :param branch: the source branch :param retained_item: a ProductionItem instance """ cls(branch=branch, sellable=produced_item.product.sellable, quantity_produced=produced_item.produced, production_date=localtoday().date(), store=store) @classmethod def add_lost_item(cls, store, branch, lost_item): """ Adds a lost_item, populates the ProductHistory table using a production_item/product_material that was lost in a production order. :param store: a store :param branch: the source branch :param lost_item: a ProductionItem or ProductionMaterial instance """ cls(branch=branch, sellable=lost_item.product.sellable, quantity_lost=lost_item.lost, production_date=localtoday().date(), store=store) @classmethod def add_decreased_item(cls, store, branch, item): """ Adds a decreased item, populates the ProductHistory table informing how many items wore manually decreased from stock. :param store: a store :param branch: the source |branch| :param item: a StockDecreaseItem instance """ cls(branch=branch, sellable=item.sellable, quantity_decreased=item.quantity, decreased_date=localtoday().date(), store=store)
class ProductSupplierInfo(Domain): """Supplier information for a |product|. Each product can has more than one |supplier|. See also: `schema <http://doc.stoq.com.br/schema/tables/product_supplier_info.html>`__ """ __storm_table__ = 'product_supplier_info' #: the cost which helps the purchaser to define the main cost of a #: certain product. Each product can have multiple |suppliers| and for #: each |supplier| a base_cost is available. The purchaser in this case #: must decide how to define the main cost based in the base cost avarage #: of all suppliers. base_cost = PriceCol(default=0) notes = UnicodeCol(default=u'') #: if this object stores information for the main |supplier|. is_main_supplier = BoolCol(default=False) #: the number of days needed to deliver the product to purchaser. lead_time = IntCol(default=1) #: the minimum amount that we can buy from this supplier. minimum_purchase = QuantityCol(default=Decimal(1)) #: a Brazil-specific attribute that means 'Imposto sobre circulacao #: de mercadorias e prestacao de servicos' icms = PercentCol(default=0) supplier_id = IdCol() #: the |supplier| supplier = Reference(supplier_id, 'Supplier.id') product_id = IdCol() #: the |product| product = Reference(product_id, 'Product.id') #: the product code in the supplier supplier_code = UnicodeCol(default=u'') # # Auxiliary methods # def get_name(self): if self.supplier: return self.supplier.get_description() def get_lead_time_str(self): if self.lead_time > 1: day_str = _(u"Days") lead_time = self.lead_time else: day_str = _(u"Day") lead_time = self.lead_time or 0 return u"%d %s" % (lead_time, day_str)
class Storable(Domain): '''Storable represents the stock of a |product|. The actual stock of an item is in ProductStockItem. See also: `schema <http://doc.stoq.com.br/schema/tables/storable.html>`__ ''' __storm_table__ = 'storable' product_id = IdCol() #: the |product| the stock represents product = Reference(product_id, 'Product.id') #: If this storable should have a finer grain control by batches. When this #: is true, stock for this storable will also require a batch information. #: This will allow us to control from what batch a sale item came from, and #: it will also let us know from what purchase this batch came from. is_batch = BoolCol(default=False) #: minimum quantity of stock items allowed minimum_quantity = QuantityCol(default=0) #: maximum quantity of stock items allowed maximum_quantity = QuantityCol(default=0) # # Classmethods # @classmethod def get_storables_without_stock_item(cls, store, branch): """Get |storables| without a |productstockitem| This will get all storables that doesn't have a |productstockitem| on the given branch. :param store: the store used to query the storables :param branch: the |branch| used to check for the stock item existence :returns: a result set of |storables| """ join = LeftJoin(ProductStockItem, And(ProductStockItem.storable_id == cls.id, ProductStockItem.branch_id == branch.id)) return store.using(cls, join).find(cls, Eq(ProductStockItem.id, None)) # # Public API # def increase_stock(self, quantity, branch, type, object_id, unit_cost=None, batch=None): """When receiving a product, update the stock reference for this new item on a specific |branch|. :param quantity: amount to increase :param branch: |branch| that stores the stock :param type: the type of the stock increase. One of the StockTransactionHistory.types :param object_id: the id of the object responsible for the transaction :param unit_cost: unit cost of the new stock or `None` :param batch: The batch of the storable. Should be not ``None`` if self.is_batch is ``True`` """ assert isinstance(type, int) if quantity <= 0: raise ValueError(_(u"quantity must be a positive number")) if branch is None: raise ValueError(u"branch cannot be None") stock_item = self.get_stock_item(branch, batch) # If the stock_item is missing create a new one if stock_item is None: store = self.store stock_item = ProductStockItem(store=store, storable=self, batch=batch, branch=store.fetch(branch)) # Unit cost must be updated here as # 1) we need the stock item which might not exist # 2) it needs to be updated before we change the quantity of the # stock item if unit_cost is not None: stock_item.update_cost(quantity, unit_cost) old_quantity = stock_item.quantity stock_item.quantity += quantity StockTransactionHistory(product_stock_item=stock_item, quantity=quantity, stock_cost=stock_item.stock_cost, responsible=get_current_user(self.store), type=type, object_id=object_id, store=self.store) ProductStockUpdateEvent.emit(self.product, branch, old_quantity, stock_item.quantity) def decrease_stock(self, quantity, branch, type, object_id, cost_center=None, batch=None): """When receiving a product, update the stock reference for the sold item this on a specific |branch|. Returns the stock item that was decreased. :param quantity: amount to decrease :param branch: a |branch| :param type: the type of the stock increase. One of the StockTransactionHistory.types :param object_id: the id of the object responsible for the transaction :param cost_center: the |costcenter| to which this stock decrease is related, if any :param batch: The batch of the storable. Should be not ``None`` if self.is_batch is ``True`` """ # FIXME: Put this back once 1.6 is released # assert isinstance(type, int) if quantity <= 0: raise ValueError(_(u"quantity must be a positive number")) if branch is None: raise ValueError(u"branch cannot be None") stock_item = self.get_stock_item(branch, batch) if stock_item is None or quantity > stock_item.quantity: raise StockError( _('Quantity to sell is greater than the available stock.')) old_quantity = stock_item.quantity stock_item.quantity -= quantity stock_transaction = StockTransactionHistory( product_stock_item=stock_item, quantity=-quantity, stock_cost=stock_item.stock_cost, responsible=get_current_user(self.store), type=type, object_id=object_id, store=self.store) if cost_center is not None: cost_center.add_stock_transaction(stock_transaction) ProductStockUpdateEvent.emit(self.product, branch, old_quantity, stock_item.quantity) return stock_item def register_initial_stock(self, quantity, branch, unit_cost, batch_number=None): """Register initial stock, by increasing the amount of this storable, for the given quantity and |branch| :param quantity: The inital stock quantity for this storable :param branch: The branch where the given quantity is avaiable for this storable :param unit_cost: The unitary cost for the initial stock :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. Must be ``None`` if the :obj:`.is_batch` is ``False``. """ if batch_number is not None: batch = StorableBatch.get_or_create( self.store, storable=self, batch_number=batch_number) else: batch = None self.increase_stock(quantity, branch, StockTransactionHistory.TYPE_INITIAL, object_id=None, unit_cost=unit_cost, batch=batch) def get_total_balance(self): """Return the stock balance for the |product| in all |branches| :returns: the amount of stock available in all |branches| """ stock_items = self.get_stock_items() return stock_items.sum(ProductStockItem.quantity) or Decimal(0) def get_balance_for_branch(self, branch): """Return the stock balance for the |product| in a |branch|. If this storable have batches, the balance for all batches will be returned. If you want the balance for a specific |batch|, the :param branch: the |branch| to get the stock balance for :returns: the amount of stock available in the |branch| """ store = self.store stock_items = store.find(ProductStockItem, storable=self, branch=branch) return stock_items.sum(ProductStockItem.quantity) or Decimal(0) def get_stock_items(self): """Fetches the stock items available for all |branches|. :returns: a sequence of stock items """ return self.store.find(ProductStockItem, storable=self) def get_stock_item(self, branch, batch): """Fetches the stock item for the given |branch|, |batch| and this |storable|. :param branch: the |branch| to get the stock item for :param batch: the |batch| to get the stock item for :returns: a stock item """ self.validate_batch(batch, sellable=self.product.sellable) return self.store.find(ProductStockItem, branch=branch, storable=self, batch=batch).one() def get_available_batches(self, branch): """Return all batches that have some stock left in the given branch :param branch: the |branch| that we are getting the avaiable batches :returns: all batches available for this storable in the given branch """ tables = [StorableBatch, LeftJoin(ProductStockItem, And(ProductStockItem.storable_id == StorableBatch.storable_id, ProductStockItem.batch_id == StorableBatch.id))] query = And(ProductStockItem.storable_id == self.id, ProductStockItem.branch_id == branch.id, ProductStockItem.quantity > 0) return self.store.using(*tables).find(StorableBatch, query)