# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4
##
## Copyright (C) 2005-2014 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>
##
""" Stock Decrease object and related objects implementation """
# pylint: enable=E1101
from decimal import Decimal
from kiwi.currency import currency
from storm.references import Reference
from zope.interface import implementer
from stoqlib.database.properties import (UnicodeCol, DateTimeCol, PriceCol,
QuantityCol, IdentifierCol,
IdCol, EnumCol)
from stoqlib.domain.base import Domain
from stoqlib.domain.events import StockOperationConfirmedEvent
from stoqlib.domain.fiscal import Invoice
from stoqlib.domain.interfaces import IContainer, IInvoice, IInvoiceItem
from stoqlib.domain.payment.payment import Payment
from stoqlib.domain.product import ProductHistory, StockTransactionHistory
from stoqlib.domain.taxes import check_tax_info_presence
from stoqlib.exceptions import DatabaseInconsistency
from stoqlib.lib.dateutils import localnow
from stoqlib.lib.translation import stoqlib_gettext
_ = stoqlib_gettext
#
# Base Domain Classes
#
@implementer(IInvoiceItem)
[docs]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()
@implementer(IContainer)
@implementer(IInvoice)
[docs]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'
#: Stock Decrease is cancelled and all items have been returned to stock.
STATUS_CANCELLED = u'cancelled'
statuses = {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()
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')
#: 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')
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
[docs] 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
[docs] 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
[docs] 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)