class DeviceConstant(Domain): """ Describes a device constant The constant_value field is only used by custom tax codes, eg when constant_type is TYPE_TAX and constant_enum is TaxType.CUSTOM @cvar constant_type: the type of constant @cvar constant_name: name of the constant @cvar constant_enum: enum value of the constant @cvar constant_value: value of the constant, only for TAX constants for which it represents the tax percentage @cvar device_value: the device value @cvar printer: printer """ implements(IDescribable) __storm_table__ = 'device_constant' constant_type = IntCol() constant_name = UnicodeCol() constant_value = DecimalCol(default=None) constant_enum = IntCol(default=None) device_value = BLOBCol() printer_id = IntCol() printer = Reference(printer_id, 'ECFPrinter.id') (TYPE_UNIT, TYPE_TAX, TYPE_PAYMENT) = range(3) constant_types = { TYPE_UNIT: _(u'Unit'), TYPE_TAX: _(u'Tax'), TYPE_PAYMENT: _(u'Payment') } def get_constant_type_description(self): """ Describe the type in a human readable form @returns: description of the constant type @rtype: str """ return DeviceConstant.constant_types[self.constant_type] @classmethod def get_custom_tax_constant(cls, printer, constant_value, store): """ Fetches a custom tax constant. @param printer: printer to fetch constants from @type printer: :class:`ECFPrinter` @param constant_enum: tax enum code @type constant_enum: int @param store: a store @returns: the constant @rtype: :class:`DeviceConstant` """ return store.find(DeviceConstant, printer=printer, constant_type=DeviceConstant.TYPE_TAX, constant_enum=int(TaxType.CUSTOM), constant_value=constant_value).one() @classmethod def get_tax_constant(cls, printer, constant_enum, store): """ Fetches a tax constant. Note that you need to use :class:`ECFPrinter.get_custom_tax_constant` for custom tax constants. @param printer: printer to fetch constants from @type printer: :class:`ECFPrinter` @param constant_enum: tax enum code @type constant_enum: int @param store: a store @returns: the constant @rtype: :class:`DeviceConstant` """ if constant_enum == TaxType.CUSTOM: raise ValueError("Use get_custom_tax_constant for custom " "tax codes") return store.find(DeviceConstant, printer=printer, constant_type=DeviceConstant.TYPE_TAX, constant_enum=int(constant_enum)).one() def get_description(self): return self.constant_name
class ReceivingInvoice(IdentifiableDomain): __storm_table__ = 'receiving_invoice' FREIGHT_FOB_PAYMENT = u'fob-payment' FREIGHT_FOB_INSTALLMENTS = u'fob-installments' FREIGHT_CIF_UNKNOWN = u'cif-unknown' FREIGHT_CIF_INVOICE = u'cif-invoice' freight_types = collections.OrderedDict([ (FREIGHT_FOB_PAYMENT, _(u"FOB - Freight value on a new payment")), (FREIGHT_FOB_INSTALLMENTS, _(u"FOB - Freight value on installments")), (FREIGHT_CIF_UNKNOWN, _(u"CIF - Freight value is unknown")), (FREIGHT_CIF_INVOICE, _(u"CIF - Freight value highlighted on invoice")), ]) FOB_FREIGHTS = ( FREIGHT_FOB_PAYMENT, FREIGHT_FOB_INSTALLMENTS, ) CIF_FREIGHTS = (FREIGHT_CIF_UNKNOWN, FREIGHT_CIF_INVOICE) #: A numeric identifier for this object. This value should be used instead of #: :obj:`Domain.id` when displaying a numerical representation of this object to #: the user, in dialogs, lists, reports and such. identifier = IdentifierCol() #: Type of freight freight_type = EnumCol(allow_none=False, default=FREIGHT_FOB_PAYMENT) #: Total of freight paid in receiving order. freight_total = PriceCol(default=0) surcharge_value = PriceCol(default=0) #: Discount value in receiving order's payment. discount_value = PriceCol(default=0) #: Secure value paid in receiving order's payment. secure_value = PriceCol(default=0) #: Other expenditures paid in receiving order's payment. expense_value = PriceCol(default=0) # This is Brazil-specific information icms_total = PriceCol(default=0) icms_st_total = PriceCol(default=0) ipi_total = PriceCol(default=0) #: The invoice number of the order that has been received. invoice_number = IntCol() #: The invoice total value of the order received invoice_total = PriceCol(default=0) #: The invoice key of the order received invoice_key = UnicodeCol() responsible_id = IdCol() responsible = Reference(responsible_id, 'LoginUser.id') branch_id = IdCol() branch = Reference(branch_id, 'Branch.id') station_id = IdCol(allow_none=False) #: The station this object was created at station = Reference(station_id, 'BranchStation.id') supplier_id = IdCol() supplier = Reference(supplier_id, 'Supplier.id') transporter_id = IdCol() transporter = Reference(transporter_id, 'Transporter.id') group_id = IdCol() group = Reference(group_id, 'PaymentGroup.id') receiving_orders = ReferenceSet('id', 'ReceivingOrder.receiving_invoice_id') @classmethod def check_unique_invoice_number(cls, store, invoice_number, supplier): count = store.find( cls, And(cls.invoice_number == invoice_number, ReceivingInvoice.supplier == supplier)).count() return count == 0 @property def total_surcharges(self): """Returns the sum of all surcharges (purchase & receiving)""" total_surcharge = 0 if self.surcharge_value: total_surcharge += self.surcharge_value if self.secure_value: total_surcharge += self.secure_value if self.expense_value: total_surcharge += self.expense_value if self.ipi_total: total_surcharge += self.ipi_total if self.icms_st_total: total_surcharge += self.icms_st_total for receiving in self.receiving_orders: total_surcharge += receiving.total_surcharges # CIF freights don't generate payments. if (self.freight_total and self.freight_type not in (self.FREIGHT_CIF_UNKNOWN, self.FREIGHT_CIF_INVOICE)): total_surcharge += self.freight_total return currency(total_surcharge) @property def total_discounts(self): """Returns the sum of all discounts (purchase & receiving)""" total_discount = 0 if self.discount_value: total_discount += self.discount_value for receiving in self.receiving_orders: total_discount += receiving.total_discounts return currency(total_discount) @property def products_total(self): return currency( sum((r.products_total for r in self.receiving_orders), 0)) @property def total(self): """Fetch the total, including discount and surcharge for both the purchase order and the receiving order. """ total = self.products_total total -= self.total_discounts total += self.total_surcharges return currency(total) @property def total_for_payment(self): """Fetch the total for the invoice payment. Exclude the freight value if it will be in a diferent pament """ total = self.total if self.freight_type == self.FREIGHT_FOB_PAYMENT: total -= self.freight_total return currency(total) @property def payments(self): """Returns all valid payments for this invoice This will return a list of valid payments for this invoice, that is, all payments on the payment group that were not cancelled. If you need to get the cancelled too, use self.group.payments. :returns: a list of |payment| """ return self.group.get_valid_payments() @property def supplier_name(self): if not self.supplier: return u"" return self.supplier.get_description() @property def transporter_name(self): if not self.transporter: return u"" return self.transporter.get_description() @property def branch_name(self): return self.branch.get_description() @property def responsible_name(self): return self.responsible.get_description() @property def discount_percentage(self): discount_value = self.discount_value if not discount_value: return currency(0) subtotal = self.products_total assert subtotal > 0, (u'the subtotal should not be zero ' u'at this point') total = subtotal - discount_value percentage = (1 - total / subtotal) * 100 return quantize(percentage) @discount_percentage.setter def discount_percentage(self, value): """Discount by percentage. Note that percentage must be added as an absolute value not as a factor like 1.05 = 5 % of surcharge The correct form is 'percentage = 3' for a discount of 3 % """ self.discount_value = self._get_percentage_value(value) @property def surcharge_percentage(self): """Surcharge by percentage. Note that surcharge must be added as an absolute value not as a factor like 0.97 = 3 % of discount. The correct form is 'percentage = 3' for a surcharge of 3 % """ surcharge_value = self.surcharge_value if not surcharge_value: return currency(0) subtotal = self.products_total assert subtotal > 0, (u'the subtotal should not be zero ' u'at this point') total = subtotal + surcharge_value percentage = ((total / subtotal) - 1) * 100 return quantize(percentage) @surcharge_percentage.setter def surcharge_percentage(self, value): self.surcharge_value = self._get_percentage_value(value) def create_freight_payment(self, group=None): store = self.store money_method = PaymentMethod.get_by_name(store, u'money') # If we have a transporter, the freight payment will be for him if not group: if self.transporter: recipient = self.transporter.person else: recipient = self.supplier.person group = PaymentGroup(store=store, recipient=recipient) description = _(u'Freight for receiving %s') % (self.identifier, ) payment = money_method.create_payment(self.branch, self.station, Payment.TYPE_OUT, group, self.freight_total, due_date=localnow(), description=description) payment.set_pending() return payment def guess_freight_type(self): """Returns a freight_type based on the purchase's freight_type""" purchases = list(self.get_purchase_orders()) assert len(purchases) == 1 purchase = purchases[0] if purchase.freight_type == PurchaseOrder.FREIGHT_FOB: if purchase.is_paid(): freight_type = ReceivingInvoice.FREIGHT_FOB_PAYMENT else: freight_type = ReceivingInvoice.FREIGHT_FOB_INSTALLMENTS elif purchase.freight_type == PurchaseOrder.FREIGHT_CIF: if purchase.expected_freight: freight_type = ReceivingInvoice.FREIGHT_CIF_INVOICE else: freight_type = ReceivingInvoice.FREIGHT_CIF_UNKNOWN return freight_type def confirm(self, user: LoginUser): self.invoice_total = self.total if self.group: self.group.confirm() for receiving in self.receiving_orders: receiving.invoice_number = self.invoice_number # XXX: Maybe FiscalBookEntry should not reference the payment group, but # lets keep this way for now until we refactor the fiscal book related # code, since it will pretty soon need a lot of changes. group = self.group or self.get_purchase_orders().pop().group FiscalBookEntry.create_product_entry(self.store, self.branch, user, group, receiving.cfop, self.invoice_number, self.icms_total, self.ipi_total) def add_receiving(self, receiving): receiving.receiving_invoice = self def get_purchase_orders(self): purchases = set() for receiving in self.receiving_orders: purchases.update(set(receiving.purchase_orders)) return purchases def _get_percentage_value(self, percentage): if not percentage: return currency(0) subtotal = self.products_total percentage = Decimal(percentage) return subtotal * (percentage / 100)
class PaymentRenegotiation(Domain): """Class for payments renegotiations """ __storm_table__ = 'payment_renegotiation' (STATUS_CONFIRMED, STATUS_PAID, STATUS_RENEGOTIATED) = range(3) statuses = { STATUS_CONFIRMED: _(u'Confirmed'), STATUS_PAID: _(u'Paid'), STATUS_RENEGOTIATED: _(u'Renegotiated') } #: A numeric identifier for this object. This value should be used instead of #: :obj:`Domain.id` when displaying a numerical representation of this object to #: the user, in dialogs, lists, reports and such. identifier = IdentifierCol() status = IntCol(default=STATUS_CONFIRMED) notes = UnicodeCol(default=None) open_date = DateTimeCol(default_factory=localnow) close_date = DateTimeCol(default=None) discount_value = PriceCol(default=0) surcharge_value = PriceCol(default=0) total = PriceCol(default=0) responsible_id = IdCol() responsible = Reference(responsible_id, 'LoginUser.id') client_id = IdCol(default=None) client = Reference(client_id, 'Client.id') branch_id = IdCol(default=None) branch = Reference(branch_id, 'Branch.id') group_id = IdCol() group = Reference(group_id, 'PaymentGroup.id') # # Public API # def can_set_renegotiated(self): """Only sales with status confirmed can be renegotiated. :returns: True if the sale can be renegotiated, False otherwise. """ # This should be as simple as: # return self.status == Sale.STATUS_CONFIRMED # But due to bug 3890 we have to check every payment. return any([ payment.status == Payment.STATUS_PENDING for payment in self.payments ]) def get_client_name(self): if not self.client: return u"" return self.client.person.name def get_responsible_name(self): return self.responsible.person.name def get_status_name(self): return self.statuses[self.status] def get_subtotal(self): return currency(self.total + self.discount_value - self.surcharge_value) def set_renegotiated(self): """Set the sale as renegotiated. The sale payments have been renegotiated and the operations will be done in other payment group.""" assert self.can_set_renegotiated() self.close_date = TransactionTimestamp() self.status = PaymentRenegotiation.STATUS_RENEGOTIATED @property def payments(self): return self.group.get_valid_payments() # # IContainer Implementation # def add_item(self, payment): # TODO: pass def remove_item(self, payment): # TODO: pass def get_items(self): return self.store.find(PaymentGroup, renegotiation=self)
class OpticalPatientMeasures(Domain): __storm_table__ = 'optical_patient_measures' EYE_LEFT = u'left' EYE_RIGHT = u'right' eye_options = { EYE_LEFT: _('Left Eye'), EYE_RIGHT: _('Right Eye'), } create_date = DateTimeCol(default_factory=StatementTimestamp) client_id = IdCol(allow_none=False) #: The related client client = Reference(client_id, 'Client.id') responsible_id = IdCol(allow_none=False) #: The user that registred this information responsible = Reference(responsible_id, 'LoginUser.id') dominant_eye = EnumCol(allow_none=False, default=EYE_LEFT) le_keratometer_horizontal = UnicodeCol() le_keratometer_vertical = UnicodeCol() le_keratometer_axis = UnicodeCol() re_keratometer_horizontal = UnicodeCol() re_keratometer_vertical = UnicodeCol() re_keratometer_axis = UnicodeCol() le_eyebrown = UnicodeCol() le_eyelash = UnicodeCol() le_conjunctiva = UnicodeCol() le_sclerotic = UnicodeCol() le_iris_diameter = UnicodeCol() le_eyelid = UnicodeCol() le_eyelid_opening = UnicodeCol() le_cornea = UnicodeCol() #: Tear breakup time. How much time the eye takes to produce a tear le_tbut = UnicodeCol() #: test that checks how much tear the eye produces le_schirmer = UnicodeCol() re_eyebrown = UnicodeCol() re_eyelash = UnicodeCol() re_conjunctiva = UnicodeCol() re_sclerotic = UnicodeCol() re_iris_diameter = UnicodeCol() re_eyelid = UnicodeCol() re_eyelid_opening = UnicodeCol() re_cornea = UnicodeCol() #: Tear breakup time. How much time the eye takes to produce a tear re_tbut = UnicodeCol() #: test that checks how much tear the eye produces re_schirmer = UnicodeCol() notes = UnicodeCol() @property def responsible_name(self): return self.responsible.get_description()
class OpticalProduct(Domain): """Stores information about products sold by optical stores. There are 3 main types of products sold by optical stores: - Glass frames (without lenses) - Glass lenses - Contact lenses """ __storm_table__ = 'optical_product' #: The frame of the glases (without lenses) TYPE_GLASS_FRAME = u'glass-frame' #: The glasses to be used with a frame TYPE_GLASS_LENSES = u'glass-lenses' #: Contact lenses TYPE_CONTACT_LENSES = u'contact-lenses' product_id = IdCol(allow_none=False) product = Reference(product_id, 'Product.id') # The type indicates what of the following fields should be edited. optical_type = EnumCol() #: If this product should be reserved automatically when added to the sale #: with work order auto_reserve = BoolCol(default=True) # # Glass frame details # #: The type of the frame (prescription or sunglasses) gf_glass_type = UnicodeCol() #: Size of the frame, accordingly to the manufacturer (may also be a string, #: for instance Large, one size fits all, etc..) gf_size = UnicodeCol() # The type of the lenses used in this frame. (for isntance: demo lens, # solar, polarized, mirrored) gf_lens_type = UnicodeCol() # Color of the frame, accordingly to the manufacturer specification gf_color = UnicodeCol() # # Glass lenses details # # Fotossensivel #: Type of the lenses photosensitivity (for instance: tints, sunsensors, #: transitions, etc...) gl_photosensitive = UnicodeCol() # Anti reflexo #: A description of the anti glare treatment the lenses have. gl_anti_glare = UnicodeCol() # Índice refração #: Decimal value describing the refraction index gl_refraction_index = DecimalCol() # Classificação #: lenses may be monofocal, bifocal or multifocal gl_classification = UnicodeCol() # Adição #: Free text describing the range of the possible additions. gl_addition = UnicodeCol() # Diametro # Free text describing the range of the possible diameters for the lens gl_diameter = UnicodeCol() # Altura #: Free text describing the height of the lens gl_height = UnicodeCol() # Disponibilidade #: Free text describint the avaiability of the lens (in what possible #: parameters they are avaiable. For instance: "-10,00 a -2,25 Cil -2,00" gl_availability = UnicodeCol() # # Contact lenses details # # Grau #: Degree of the lenses, a decimal from -30 to +30, in steps of +- 0.25 cl_degree = DecimalCol() # Classificação #: Free text describing the classification of the lenses (solid, gel, etc..) cl_classification = UnicodeCol() # tipo lente #: The type of the lenses (monofocal, toric, etc..) cl_lens_type = UnicodeCol() # Descarte #: How often the lens should be discarded (anually, daily, etc..) cl_discard = UnicodeCol() # Adição #: Free text describing the addition of the lenses. cl_addition = UnicodeCol() # Cilindrico # XXX: I still need to verify if a decimal column is ok, or if there are # possible text values. #: Cylindrical value of the lenses. cl_cylindrical = DecimalCol() # Eixo # XXX: I still need to verify if a decimal column is ok, or if there are # possible text values. #: Axix of the lenses. cl_axis = DecimalCol() #: Free text color description of the lens (for cosmetic use) cl_color = UnicodeCol() # Curvatura #: Free text description of the curvature. normaly a decimal, but may have #: textual descriptions cl_curvature = UnicodeCol() @classmethod def get_from_product(cls, product): return product.store.find(cls, product=product).one()
class ReturnedSale(IdentifiableDomain): """Holds information about a returned |sale|. This can be: * *trade*, a |client| is returning the |sale| and buying something new with that credit. In that case the returning sale is :obj:`.sale` and the replacement |sale| is in :obj:`.new_sale`. * *return sale* or *devolution*, a |client| is returning the |sale| without making a new |sale|. Normally the old sale which is returned is :obj:`.sale`, however it might be ``None`` in some situations for example, if the |sale| was done at a different |branch| that hasn't been synchronized or is using another system. """ __storm_table__ = 'returned_sale' #: This returned sale was received on another branch, but is not yet #: confirmed. A product goes back to stock only after confirmation STATUS_PENDING = u'pending' #: This return was confirmed, meaning the product stock was increased. STATUS_CONFIRMED = u'confirmed' #: This returned sale was canceled, ie, The product stock is decreased back #: and the original sale still have the products. STATUS_CANCELLED = 'cancelled' statuses = collections.OrderedDict([ (STATUS_PENDING, _(u'Pending')), (STATUS_CONFIRMED, _(u'Confirmed')), (STATUS_CANCELLED, _(u'Cancelled')), ]) #: A numeric identifier for this object. This value should be used instead of #: :obj:`Domain.id` when displaying a numerical representation of this object to #: the user, in dialogs, lists, reports and such. identifier = IdentifierCol() #: Status of the returned sale status = EnumCol(default=STATUS_PENDING) #: the date this return was done return_date = DateTimeCol(default_factory=localnow) #: the date that the |returned sale| with the status pending was received confirm_date = DateTimeCol(default=None) # When this returned sale was undone undo_date = DateTimeCol(default=None) #: the reason why this return was made reason = UnicodeCol(default=u'') #: The reason this returned sale was undone undo_reason = UnicodeCol(default=u'') sale_id = IdCol(default=None) #: the |sale| we're returning sale = Reference(sale_id, 'Sale.id') new_sale_id = IdCol(default=None) #: if not ``None``, :obj:`.sale` was traded for this |sale| new_sale = Reference(new_sale_id, 'Sale.id') responsible_id = IdCol() #: the |loginuser| responsible for doing this return responsible = Reference(responsible_id, 'LoginUser.id') confirm_responsible_id = IdCol() #: the |loginuser| responsible for receiving the pending return confirm_responsible = Reference(confirm_responsible_id, 'LoginUser.id') undo_responsible_id = IdCol() #: the |loginuser| responsible for undoing this returned sale. undo_responsible = Reference(undo_responsible_id, 'LoginUser.id') branch_id = IdCol() #: the |branch| in which this return happened branch = Reference(branch_id, 'Branch.id') station_id = IdCol(allow_none=False) #: The station this object was created at station = Reference(station_id, 'BranchStation.id') #: a list of all items returned in this return returned_items = ReferenceSet('id', 'ReturnedSaleItem.returned_sale_id') #: |payments| generated by this returned sale payments = None #: |transporter| used in returned sale transporter = None invoice_id = IdCol() #: The |invoice| generated by the returned sale invoice = Reference(invoice_id, 'Invoice.id') def __init__(self, store=None, **kwargs): kwargs['invoice'] = Invoice(store=store, invoice_type=Invoice.TYPE_IN) super(ReturnedSale, self).__init__(store=store, **kwargs) @property def group(self): """|paymentgroup| for this return sale. Can return: * For a *trade*, use the |paymentgroup| from the replacement |sale|. * For a *devolution*, use the |paymentgroup| from the returned |sale|. """ if self.new_sale: return self.new_sale.group if self.sale: return self.sale.group return None @property def client(self): """The |client| of this return Note that this is the same as :obj:`.sale.client` """ return self.sale and self.sale.client @property def sale_total(self): """The current total amount of the |sale|. This is calculated by getting the :attr:`total amount <stoqlib.domain.sale.Sale.total_amount>` of the returned sale and subtracting the sum of :obj:`.returned_total` of all existing returns for the same sale. """ if not self.sale: return currency(0) query = And(ReturnedSale.sale_id == self.sale.id, ReturnedSale.status == ReturnedSale.STATUS_CONFIRMED) returned = self.store.find(ReturnedSale, query) # This will sum the total already returned for this sale, # excluiding *self* within the same store returned_total = sum([ returned_sale.returned_total for returned_sale in returned if returned_sale != self ]) return currency(self.sale.total_amount - returned_total) @property def paid_total(self): """The total paid for this sale Note that this is the same as :meth:`stoqlib.domain.sale.Sale.get_total_paid` """ if not self.sale: return currency(0) return self.sale.get_total_paid() @property def returned_total(self): """The total being returned on this return This is done by summing the :attr:`ReturnedSaleItem.total` of all of this :obj:`returned items <.returned_items>` """ return currency(sum([item.total for item in self.returned_items])) @property def total_amount(self): """The total amount for this return See :meth:`.return_` for details of how this is used. """ return currency(self.sale_total - self.paid_total - self.returned_total) @property def total_amount_abs(self): """The absolute total amount for this return This is the same as abs(:attr:`.total_amount`). Useful for displaying it on a gui, just changing it's label to show if it's 'overpaid' or 'missing'. """ return currency(abs(self.total_amount)) # # IContainer implementation # def add_item(self, returned_item): assert not returned_item.returned_sale returned_item.returned_sale = self def get_items(self): return self.returned_items def remove_item(self, item): item.returned_sale = None self.store.maybe_remove(item) # # IInvoice implementation # @property def comments(self): return self.reason @property def discount_value(self): return currency(0) @property def invoice_subtotal(self): return self.returned_total @property def invoice_total(self): return self.returned_total @property def recipient(self): if self.sale and self.sale.client: return self.sale.client.person elif self.new_sale and self.new_sale.client: return self.new_sale.client.person return None @property def operation_nature(self): # TODO: Save the operation nature in new returned_sale table field. return _(u"Sale Return") # # Public API # @classmethod def get_pending_returned_sales(cls, store, branch): """Returns a list of pending |returned_sale| :param store: a store :param branch: the |branch| where the sale was made """ from stoqlib.domain.sale import Sale tables = [cls, Join(Sale, cls.sale_id == Sale.id)] # We want the returned_sale which sale was made on the branch # So we are comparing Sale.branch with |branch| to build the query return store.using(*tables).find( cls, And(cls.status == cls.STATUS_PENDING, Sale.branch == branch)) def is_pending(self): return self.status == ReturnedSale.STATUS_PENDING def is_undone(self): return self.status == ReturnedSale.STATUS_CANCELLED def can_undo(self): return self.status == ReturnedSale.STATUS_CONFIRMED def return_(self, method_name=u'money', login_user=None): """Do the return of this returned sale. :param unicode method_name: The name of the payment method that will be used to create this payment. If :attr:`.total_amount` is: * > 0, the client is returning more than it paid, we will create a |payment| with that value so the |client| can be reversed. * == 0, the |client| is returning the same amount that needs to be paid, so existing payments will be cancelled and the |client| doesn't owe anything to us. * < 0, than the payments need to be readjusted before calling this. .. seealso: :meth:`stoqlib.domain.sale.Sale.return_` as that will be called after that payment logic is done. """ assert self.sale and self.sale.can_return() self._clean_not_used_items() self._create_return_payment(method_name, self.returned_total) # FIXME: For now, we are not reverting the comission as there is a # lot of things to consider. See bug 5215 for information about it. self._revert_fiscal_entry() self.sale.return_(self) # Save operation_nature and branch in Invoice table. self.invoice.operation_nature = self.operation_nature self.invoice.branch = self.branch if self.sale.branch == self.branch: self.confirm(login_user) def trade(self): """Do a trade for this return Almost the same as :meth:`.return_`, but unlike it, this won't generate reversed payments to the client. Instead, it'll generate an inpayment using :obj:`.returned_total` value, so it can be used as an "already paid quantity" on :obj:`.new_sale`. """ assert self.new_sale if self.sale: assert self.sale.can_return() self._clean_not_used_items() store = self.store group = self.group method = PaymentMethod.get_by_name(store, u'trade') description = _(u'Traded items for sale %s') % ( self.new_sale.identifier, ) value = self.returned_total value_as_discount = sysparam.get_bool('USE_TRADE_AS_DISCOUNT') if value_as_discount: self.new_sale.discount_value = self.returned_total else: payment = method.create_payment(Payment.TYPE_IN, group, self.branch, value, description=description) payment.set_pending() payment.pay() self._revert_fiscal_entry() login_user = api.get_current_user(self.store) if self.sale: self.sale.return_(self) if self.sale.branch == self.branch: self.confirm(login_user) else: # When trade items without a registered sale, confirm the # new returned sale. self.confirm(login_user) def remove(self): """Remove this return and it's items from the database""" # XXX: Why do we remove this object from the database # We must remove children_items before we remove its parent_item for item in self.returned_items.find( Eq(ReturnedSaleItem.parent_item_id, None)): [ self.remove_item(child) for child in getattr(item, 'children_items') ] self.remove_item(item) self.store.remove(self) def confirm(self, login_user): """Receive the returned_sale_items from a pending |returned_sale| :param user: the |login_user| that received the pending returned sale """ assert self.status == self.STATUS_PENDING self._return_items() old_status = self.status self.status = self.STATUS_CONFIRMED self.confirm_responsible = login_user self.confirm_date = localnow() StockOperationConfirmedEvent.emit(self, old_status) def undo(self, reason): """Undo this returned sale. This includes removing the returned items from stock again (updating the quantity decreased on the sale). :param reason: The reason for this operation. """ assert self.can_undo() for item in self.get_items(): item.undo() payment = self._get_cancel_candidate_payment(pending_only=True) if payment: # If there are pending out payments which match the returned value # and those payments are all of the same method, we can just cancel any of # these payments right away. payment.cancel() else: # We now need to create a new in payment for the total amount of this # returned sale. payment = self._get_cancel_candidate_payment() method = payment.method if payment else PaymentMethod.get_by_name( self.store, 'money') description = _(u'%s return undone for sale %s') % ( method.description, self.sale.identifier) payment = method.create_payment(Payment.TYPE_IN, payment_group=self.group, branch=self.branch, value=self.returned_total, description=description, ignore_max_installments=True) payment.set_pending() payment.pay() self.status = self.STATUS_CANCELLED self.cancel_date = localnow() self.undo_reason = reason # if the sale status is returned, we must reset it to confirmed (only # confirmed sales can be returned) if self.sale.is_returned(): self.sale.set_not_returned() # # Private # def _create_return_payment(self, method_name, value): method = PaymentMethod.get_by_name(self.store, method_name) description = _(u'%s returned for sale %s') % (method.description, self.sale.identifier) payment = method.create_payment(Payment.TYPE_OUT, payment_group=self.group, branch=self.branch, value=value, description=description) payment.set_pending() if method_name == u'credit': payment.pay() def _get_cancel_candidate_payment(self, pending_only=False): """Try to find a payment to cancel for a canceled operation If the user cancels or undoes an operation - e.g. undoing a returned sale - we can either cancel the pending payment, or create a reversal payment for its value. :param pending_only: If True, this will consider only pending payments. """ queries = [ Payment.payment_type == Payment.TYPE_OUT, Payment.value == self.returned_total ] if pending_only: queries.append(Payment.status == Payment.STATUS_PENDING) # We search for the payments which are correspondent to our operation aiming to # finding which payment methods are involved. payments = list(self.sale.payments.find(And(*queries))) methods = set(payment.method.method_name for payment in payments) # If and only if there is only one method from all the payments found, then any of the # payments can be cancelled or reversed. if len(methods) == 1: return payments[0] return def _return_items(self): # We must have at least one item to return assert self.returned_items.count() # FIXME branch = get_current_branch(self.store) for item in self.returned_items: item.return_(branch) def _get_returned_percentage(self): return Decimal(self.returned_total / self.sale.total_amount) def _clean_not_used_items(self): query = Eq(ReturnedSaleItem.parent_item_id, None) for item in self.returned_items.find(query): item.maybe_remove() def _revert_fiscal_entry(self): entry = self.store.find(FiscalBookEntry, payment_group=self.group, is_reversal=False).one() if not entry: return # FIXME: Instead of doing a partial reversion of fiscal entries, # we should be reverting the exact tax for each returned item. returned_percentage = self._get_returned_percentage() entry.reverse_entry(self.invoice.invoice_number, icms_value=entry.icms_value * returned_percentage, iss_value=entry.iss_value * returned_percentage, ipi_value=entry.ipi_value * returned_percentage)
class OpticalWorkOrder(Domain): """This holds the necessary information to execute an work order for optical stores. This includes all the details present in the prescription. For reference: http://en.wikipedia.org/wiki/Eyeglass_prescription See http://en.wikipedia.org/wiki/Eyeglass_prescription#Abbreviations_and_terms for reference no the names used here. In some places, RE is used as a short for right eye, and LE for left eye """ __storm_table__ = 'optical_work_order' #: Lens used in glasses LENS_TYPE_OPHTALMIC = u'ophtalmic' #: Contact lenses LENS_TYPE_CONTACT = u'contact' #: The frame for the lens is a closed ring FRAME_TYPE_CLOSED_RING = u'closed-ring' #: The frame uses a nylon string to hold the lenses. FRAME_TYPE_NYLON = u'nylon' #: The frame is made 3 pieces FRAME_TYPE_3_PIECES = u'3-pieces' lens_types = { LENS_TYPE_OPHTALMIC: _('Ophtalmic'), LENS_TYPE_CONTACT: _('Contact'), } frame_types = { # Translators: Aro fechado FRAME_TYPE_3_PIECES: _('Closed ring'), # Translators: Fio de nylon FRAME_TYPE_NYLON: _('Nylon String'), # Translators: 3 preças FRAME_TYPE_CLOSED_RING: _('3 pieces'), } work_order_id = IdCol(allow_none=False) work_order = Reference(work_order_id, 'WorkOrder.id') medic_id = IdCol() medic = Reference(medic_id, 'OpticalMedic.id') prescription_date = DateTimeCol() #: The name of the patient. Note that we already have the client of the work #: order, but the patient may be someone else (like the son, father, #: etc...). Just the name is enough patient = UnicodeCol() #: The type of the lens, Contact or Ophtalmic lens_type = EnumCol(default=LENS_TYPE_OPHTALMIC) # # Frame # #: The type of the frame. One of OpticalWorkOrder.FRAME_TYPE_* frame_type = EnumCol(default=FRAME_TYPE_CLOSED_RING) #: The vertical frame measure frame_mva = DecimalCol(default=decimal.Decimal(0)) #: The horizontal frame measure frame_mha = DecimalCol(default=decimal.Decimal(0)) #: The diagonal frame measure frame_mda = DecimalCol(default=decimal.Decimal(0)) #: The brige is the part of the frame between the two lenses, above the nose. frame_bridge = DecimalCol() # # Left eye distance vision # le_distance_spherical = DecimalCol(default=0) le_distance_cylindrical = DecimalCol(default=0) le_distance_axis = DecimalCol(default=0) le_distance_prism = DecimalCol(default=0) le_distance_base = DecimalCol(default=0) le_distance_height = DecimalCol(default=0) #: Pupil distance (DNP in pt_BR) le_distance_pd = DecimalCol(default=0) le_addition = DecimalCol(default=0) # # Left eye distance vision # le_near_spherical = DecimalCol(default=0) le_near_cylindrical = DecimalCol(default=0) le_near_axis = DecimalCol(default=0) #: Pupil distance (DNP in pt_BR) le_near_pd = DecimalCol(default=0) # # Right eye distance vision # re_distance_spherical = DecimalCol(default=0) re_distance_cylindrical = DecimalCol(default=0) re_distance_axis = DecimalCol(default=0) re_distance_prism = DecimalCol(default=0) re_distance_base = DecimalCol(default=0) re_distance_height = DecimalCol(default=0) #: Pupil distance (DNP in pt_BR) re_distance_pd = DecimalCol(default=0) re_addition = DecimalCol(default=0) # # Right eye near vision # re_near_spherical = DecimalCol(default=0) re_near_cylindrical = DecimalCol(default=0) re_near_axis = DecimalCol(default=0) #: Pupil distance (DNP in pt_BR) re_near_pd = DecimalCol(default=0) @property def frame_type_str(self): return self.frame_types.get(self.frame_type, '') @property def lens_type_str(self): return self.lens_types.get(self.lens_type, '')
class Payment(Domain): """Payment, a transfer of money between a |branch| and |client| or a |supplier|. Payments between: * a client and a branch are :obj:`.TYPE_IN`, has a |sale| associated. * branch and a supplier are :obj:`.TYPE_OUT`, has a |purchase| associated. Payments are sometimes referred to as *installments*. Sales and purchase orders can be accessed via the :obj:`payment group <.group>` +-------------------------+-------------------------+ | **Status** | **Can be set to** | +-------------------------+-------------------------+ | :obj:`STATUS_PREVIEW` | :obj:`STATUS_PENDING` | +-------------------------+-------------------------+ | :obj:`STATUS_PENDING` | :obj:`STATUS_PAID`, | | | :obj:`STATUS_CANCELLED` | +-------------------------+-------------------------+ | :obj:`STATUS_PAID` | :obj:`STATUS_PENDING`, | | | :obj:`STATUS_CANCELLED` | +-------------------------+-------------------------+ | :obj:`STATUS_CANCELLED` | None | +-------------------------+-------------------------+ .. graphviz:: digraph status { STATUS_PREVIEW -> STATUS_PENDING; STATUS_PENDING -> STATUS_PAID; STATUS_PENDING -> STATUS_CANCELLED; STATUS_PAID -> STATUS_PENDING; STATUS_PAID -> STATUS_CANCELLED; } Simple sale workflow: * Creating a sale, status is set to :obj:`STATUS_PREVIEW` * Confirming the sale, status is set to :obj:`STATUS_PENDING` * Paying the installment, status is set to :obj:`STATUS_PAID` * Cancelling the payment, status is set to :obj:`STATUS_CANCELLED` See also: `schema <http://doc.stoq.com.br/schema/tables/payment.html>`__ """ __storm_table__ = 'payment' #: incoming to the company, accounts receivable, payment from #: a |client| to a |branch| TYPE_IN = u'in' #: outgoing from the company, accounts payable, a payment from #: |branch| to a |supplier| TYPE_OUT = u'out' #: payment group this payment belongs to hasn't been confirmed, # should normally be filtered when showing a payment list STATUS_PREVIEW = u'preview' #: payment group has been confirmed and the payment has not been received STATUS_PENDING = u'pending' #: the payment has been received STATUS_PAID = u'paid' # FIXME: Remove these two #: Unused. STATUS_REVIEWING = u'reviewing' #: Unused. STATUS_CONFIRMED = u'confirmed' #: payment was cancelled, for instance the payments of the group was changed, or #: the group was cancelled. STATUS_CANCELLED = u'cancelled' statuses = {STATUS_PREVIEW: _(u'Preview'), STATUS_PENDING: _(u'To Pay'), STATUS_PAID: _(u'Paid'), STATUS_REVIEWING: _(u'Reviewing'), STATUS_CONFIRMED: _(u'Confirmed'), STATUS_CANCELLED: _(u'Cancelled')} #: type of payment :obj:`.TYPE_IN` or :obj:`.TYPE_OUT` payment_type = EnumCol(allow_none=False, default=TYPE_IN) #: A numeric identifier for this object. This value should be used instead of #: :obj:`Domain.id` when displaying a numerical representation of this object to #: the user, in dialogs, lists, reports and such. identifier = IdentifierCol() #: status, see |payment| for more information. status = EnumCol(allow_none=False, default=STATUS_PREVIEW) #: description payment, usually something like "1/3 Money for Sale 1234" description = UnicodeCol(default=None) # FIXME: use TransactionTimestamp() instead to avoid server/client date # inconsistencies #: when this payment was opened open_date = DateTimeCol(default_factory=localnow) #: when this payment is due due_date = DateTimeCol() #: when this payment was paid paid_date = DateTimeCol(default=None) #: when this payment was cancelled cancel_date = DateTimeCol(default=None) # FIXME: Figure out when and why this differs from value #: base value base_value = PriceCol(default=None) #: value of the payment value = PriceCol() #: the actual amount that was paid, including penalties, interest, discount etc. paid_value = PriceCol(default=None) #: interest of this payment interest = PriceCol(default=0) #: discount, an absolute value with the difference between the #: sales price and :obj:`.value` discount = PriceCol(default=0) #: penalty of the payment penalty = PriceCol(default=0) # FIXME: Figure out what this is used for #: number of the payment payment_number = UnicodeCol(default=None) branch_id = IdCol(allow_none=False) #: |branch| associated with this payment. #: For a :obj:`.TYPE_IN` payment, this is the branch that will receive #: the money. For a :obj:`.TYPE_IN` payment, this is the branch that #: will make the payment branch = Reference(branch_id, 'Branch.id') method_id = IdCol() #: |paymentmethod| for this payment #: payment method = Reference(method_id, 'PaymentMethod.id') group_id = IdCol() #: |paymentgroup| for this payment group = Reference(group_id, 'PaymentGroup.id') category_id = IdCol() #: |paymentcategory| this payment belongs to, can be None category = Reference(category_id, 'PaymentCategory.id') #: list of :class:`comments <stoqlib.domain.payment.comments.PaymentComment>` for #: this payment comments = ReferenceSet('id', 'PaymentComment.payment_id') #: :class:`check data <stoqlib.domain.payment.method.CheckData>` for #: this payment check_data = Reference('id', 'CheckData.payment_id', on_remote=True) #: |accounttransaction| for this payment transaction = Reference('id', 'AccountTransaction.payment_id', on_remote=True) #: indicates if a bill has been received. They are usually delivered by #: mail before the due date. This is not indicating whether the payment has #: been paid, just that the receiver has notified the payer somehow. bill_received = BoolCol(default=False) attachment_id = IdCol() #: |attachment| for this payment attachment = Reference(attachment_id, 'Attachment.id') def __init__(self, store=None, **kw): if not 'value' in kw: raise TypeError('You must provide a value argument') if not 'base_value' in kw or not kw['base_value']: kw['base_value'] = kw['value'] Domain.__init__(self, store=store, **kw) def _check_status(self, status, operation_name): fmt = 'Invalid status for %s operation: %s' assert self.status == status, ( fmt % (operation_name, self.statuses[self.status])) # # ORMObject hooks # def delete(self): # First call hooks, do this first so the hook # have access to everything it needs self.method.operation.payment_delete(self) # FIXME: BUG 5581 check if it is really safe to remove the payment # when using with synced databases self.store.remove(self) @classmethod def create_repeated(cls, store, payment, repeat_type, start_date, end_date): """Create a set of repeated payments. Given a type of interval (*repeat_type*), a start date and an end_date, this creates a list of payments for that interval. Note, this will also update the description of the payment that's passed in. :param store: a store :param payment: the payment to repeat :param repeat_type: the kind of repetition (weekly, monthly etc) :param start_date: the date to start this repetition :param end_date: the date to end this repetition :returns: a list of repeated payments """ dates = create_date_interval(interval_type=repeat_type, start_date=start_date, end_date=end_date) n_dates = dates.count() if n_dates == 1: raise AssertionError description = payment.description payment.description = u'1/%d %s' % (n_dates, description) payment.due_date = dates[0] payments = [] for i, date in enumerate(dates[1:]): p = Payment(open_date=payment.open_date, branch=payment.branch, payment_type=payment.payment_type, status=payment.status, description=u'%d/%d %s' % (i + 2, n_dates, description), value=payment.value, base_value=payment.base_value, due_date=date, method=payment.method, group=payment.group, category=payment.category, store=store) payments.append(p) return payments # # Properties # @property def comments_number(self): """The number of |paymentcomments| for this payment""" return self.comments.count() @property def bank_account_number(self): """For check payments, the :class:`bank account <BankAccount>` number""" # This is used by test_payment_method, and is a convenience # property, ideally we should move it to payment operation # somehow if self.method.method_name == u'check': data = self.method.operation.get_check_data_by_payment(self) bank_account = data.bank_account if bank_account: return bank_account.bank_number @property def installment_number(self): payments = self.group.get_valid_payments().order_by( Payment.identifier) for i, payment in enumerate(payments): if self == payment: return i + 1 @property def status_str(self): """The :obj:`Payment.status` as a translated string""" if not self.status in self.statuses: raise DatabaseInconsistency('Invalid status for Payment ' 'instance, got %d' % self.status) return self.statuses[self.status] def get_days_late(self): """For due payments, the number of days late this payment is :returns: the number of days late """ if self.status == Payment.STATUS_PAID: return 0 days_late = localtoday().date() - self.due_date.date() if days_late.days < 0: return 0 return days_late.days def set_pending(self): """Set a :obj:`.STATUS_PREVIEW` payment as :obj:`.STATUS_PENDING`. This also means that this is valid payment and its owner actually can charge it """ self._check_status(self.STATUS_PREVIEW, u'set_pending') self.status = self.STATUS_PENDING def set_not_paid(self, change_entry): """Set a :obj:`.STATUS_PAID` payment as :obj:`.STATUS_PENDING`. This requires clearing paid_date and paid_value :param change_entry: a :class:`PaymentChangeHistory` object, that will hold the changes information """ self._check_status(self.STATUS_PAID, u'set_not_paid') if self.transaction: self.transaction.create_reverse() change_entry.last_status = self.STATUS_PAID change_entry.new_status = self.STATUS_PENDING sale = self.group and self.group.sale if sale and sale.can_set_not_paid(): sale.set_not_paid() self.status = self.STATUS_PENDING self.paid_date = None self.paid_value = None def pay(self, paid_date=None, paid_value=None, source_account=None, destination_account=None, account_transaction_number=None): """Pay the current payment set its status as :obj:`.STATUS_PAID` If this payment belongs to a sale, and all other payments from the sale are paid then the sale will be set as paid. """ if self.status != Payment.STATUS_PENDING: raise ValueError(_(u"This payment is already paid.")) self._check_status(self.STATUS_PENDING, u'pay') paid_value = paid_value or (self.value - self.discount + self.interest) self.paid_value = paid_value self.paid_date = paid_date or TransactionTimestamp() self.status = self.STATUS_PAID if (self.is_separate_payment() or self.method.operation.create_transaction()): AccountTransaction.create_from_payment( self, code=account_transaction_number, source_account=source_account, destination_account=destination_account) sale = self.group and self.group.sale if sale: sale.create_commission(self) # When paying payments of a sale, check if the other payments are # paid. If they are, this means you can change the sale status to # paid as well. if sale.can_set_paid(): sale.set_paid() if self.value == self.paid_value: msg = _(u"{method} payment with value {value:.2f} was paid").format( method=self.method.method_name, value=self.value) else: msg = _(u"{method} payment with value original value " u"{original_value:.2f} was paid with value " u"{value:.2f}").format(method=self.method.method_name, original_value=self.value, value=self.paid_value) Event.log(self.store, Event.TYPE_PAYMENT, msg.capitalize()) def cancel(self, change_entry=None): """Cancel the payment, set it's status to :obj:`.STATUS_CANCELLED` """ # TODO Check for till entries here and call cancel_till_entry if # it's possible. Bug 2598 if not self.can_cancel(): raise StoqlibError(_(u"Invalid status for cancel operation, " u"got %s") % self.status_str) if self.transaction: self.transaction.create_reverse() old_status = self.status self.status = self.STATUS_CANCELLED self.cancel_date = TransactionTimestamp() if change_entry is not None: change_entry.last_status = old_status change_entry.new_status = self.status msg = _(u"{method} payment with value {value:.2f} was cancelled").format( method=self.method.method_name, value=self.value) Event.log(self.store, Event.TYPE_PAYMENT, msg.capitalize()) def change_due_date(self, new_due_date): """Changes the payment due date. :param new_due_date: The new due date for the payment. :rtype: datetime.date """ if self.status in [Payment.STATUS_PAID, Payment.STATUS_CANCELLED]: raise StoqlibError(_(u"Invalid status for change_due_date operation, " u"got %s") % self.status_str) self.due_date = new_due_date def update_value(self, new_value): """Update the payment value. """ self.value = new_value def can_cancel(self): return self.status in (Payment.STATUS_PREVIEW, Payment.STATUS_PENDING, Payment.STATUS_PAID) def get_payable_value(self): """Returns the calculated payment value with the daily interest. Note that the payment group daily_interest must be between 0 and 100. :returns: the payable value """ if self.status in [self.STATUS_PREVIEW, self.STATUS_CANCELLED]: return self.value if self.status in [self.STATUS_PAID, self.STATUS_REVIEWING, self.STATUS_CONFIRMED]: return self.paid_value return self.value + self.get_interest() def get_penalty(self, date=None): """Calculate the penalty in an absolute value :param date: date of payment :returns: penalty :rtype: :class:`kiwi.currency.currency` """ if date is None: date = localtoday().date() elif date < self.open_date.date(): raise ValueError(_(u"Date can not be less then open date")) elif date > localtoday().date(): raise ValueError(_(u"Date can not be greather then future date")) if not self.method.penalty: return currency(0) # Don't add penalty if we pay in time! if self.due_date.date() >= date: return currency(0) return currency(self.method.penalty / 100 * self.value) def get_interest(self, date=None, pay_penalty=True): """Calculate the interest in an absolute value :param date: date of payment :returns: interest :rtype: :class:`kiwi.currency.currency` """ if date is None: date = localtoday().date() elif date < self.open_date.date(): raise ValueError(_(u"Date can not be less then open date")) elif date > localtoday().date(): raise ValueError(_(u"Date can not be greather then future date")) if not self.method.daily_interest: return currency(0) days = (date - self.due_date.date()).days if days <= 0: return currency(0) base_value = self.value + (pay_penalty and self.get_penalty(date=date)) return currency(days * self.method.daily_interest / 100 * base_value) def has_commission(self): """Check if this |payment| already has a |commission|""" from stoqlib.domain.commission import Commission return self.store.find(Commission, payment=self).any() def is_paid(self): """Check if the payment is paid. :returns: ``True`` if the payment is paid """ return self.status == Payment.STATUS_PAID def is_pending(self): """Check if the payment is pending. :returns: ``True`` if the payment is pending """ return self.status == Payment.STATUS_PENDING def is_preview(self): """Check if the payment is in preview state :returns: ``True`` if the payment is paid """ return self.status == Payment.STATUS_PREVIEW def is_cancelled(self): """Check if the payment was cancelled. :returns: ``True`` if the payment was cancelled """ return self.status == Payment.STATUS_CANCELLED def get_paid_date_string(self): """Get a paid date string :returns: the paid date string or PAID DATE if the payment isn't paid """ if self.paid_date: return self.paid_date.date().strftime('%x') return _(u'NOT PAID') def get_open_date_string(self): """Get a open date string :returns: the open date string or empty string """ if self.open_date: return self.open_date.date().strftime('%x') return u"" def is_inpayment(self): """Find out if a payment is :obj:`incoming <.TYPE_IN>` :returns: ``True`` if it's incoming """ return self.payment_type == self.TYPE_IN def is_outpayment(self): """Find out if a payment is :obj:`outgoing <.TYPE_OUT>` :returns: ``True`` if it's outgoing """ return self.payment_type == self.TYPE_OUT def is_separate_payment(self): """Find out if this payment is created separately from a sale, purchase or renegotiation :returns: ``True`` if it's separate. """ # FIXME: This is a hack, we should rather store a flag # in the database that tells us how the payment was # created. group = self.group if not group: # Should never happen return False if group.sale: return False elif group.purchase: return False elif group._renegotiation: return False return True def is_of_method(self, method_name): """Find out if the payment was made with a certain method :returns: ``True`` if it's a payment of that method """ return self.method.method_name == method_name
class PurchaseOrder(IdentifiableDomain): """Purchase and order definition.""" __storm_table__ = 'purchase_order' ORDER_QUOTING = u'quoting' ORDER_PENDING = u'pending' ORDER_CONFIRMED = u'confirmed' ORDER_CONSIGNED = u'consigned' ORDER_CANCELLED = u'cancelled' ORDER_CLOSED = u'closed' statuses = collections.OrderedDict([ (ORDER_QUOTING, _(u'Quoting')), (ORDER_PENDING, _(u'Pending')), (ORDER_CONFIRMED, _(u'Confirmed')), (ORDER_CONSIGNED, _(u'Consigned')), (ORDER_CANCELLED, _(u'Cancelled')), (ORDER_CLOSED, _(u'Closed')), ]) FREIGHT_FOB = u'fob' FREIGHT_CIF = u'cif' freight_types = {FREIGHT_FOB: _(u'FOB'), FREIGHT_CIF: _(u'CIF')} #: A numeric identifier for this object. This value should be used instead of #: :obj:`Domain.id` when displaying a numerical representation of this object to #: the user, in dialogs, lists, reports and such. identifier = IdentifierCol() status = EnumCol(allow_none=False, default=ORDER_QUOTING) open_date = DateTimeCol(default_factory=localnow, allow_none=False) quote_deadline = DateTimeCol(default=None) expected_receival_date = DateTimeCol(default_factory=localnow) expected_pay_date = DateTimeCol(default_factory=localnow) # XXX This column is not being used anywhere receival_date = DateTimeCol(default=None) confirm_date = DateTimeCol(default=None) notes = UnicodeCol(default=u'') salesperson_name = UnicodeCol(default=u'') freight_type = EnumCol(allow_none=False, default=FREIGHT_FOB) expected_freight = PriceCol(default=0) surcharge_value = PriceCol(default=0) discount_value = PriceCol(default=0) consigned = BoolCol(default=False) supplier_id = IdCol() supplier = Reference(supplier_id, 'Supplier.id') branch_id = IdCol() branch = Reference(branch_id, 'Branch.id') station_id = IdCol(allow_none=False) #: The station this object was created at station = Reference(station_id, 'BranchStation.id') transporter_id = IdCol(default=None) transporter = Reference(transporter_id, 'Transporter.id') responsible_id = IdCol() responsible = Reference(responsible_id, 'LoginUser.id') group_id = IdCol() group = Reference(group_id, 'PaymentGroup.id') #: Indicates if the order is from a work order work_order_id = IdCol() work_order = Reference(work_order_id, 'WorkOrder.id') # # IContainer Implementation # def get_items(self, with_children=True): """Get the items of the purchase order :param with_children: indicate if we should fetch children_items or not """ query = PurchaseItem.order == self if not with_children: query = And(query, Eq(PurchaseItem.parent_item_id, None)) return self.store.find(PurchaseItem, query) def remove_item(self, item): if item.order is not self: raise ValueError( _(u'Argument item must have an order attribute ' 'associated with the current purchase instance')) item.order = None self.store.maybe_remove(item) def add_item(self, sellable, quantity=Decimal(1), parent=None, cost=None, icms_st_value=0, ipi_value=0): """Add a sellable to this purchase. If the sellable is part of a package (parent is not None), then the actual cost and quantity will be calculated based on how many items of this component is on the package. :param sellable: the sellable being added :param quantity: How many units of this sellable we are adding :param cost: The price being paid for this sellable :param parent: The parent of this sellable, incase of a package """ if cost is None: cost = sellable.cost if parent: component = parent.sellable.product.get_component(sellable) cost = cost / component.quantity quantity = quantity * component.quantity else: if sellable.product.is_package: # If this is a package, the cost will be calculated and updated by the # compoents of the package cost = Decimal('0') store = self.store return PurchaseItem(store=store, order=self, sellable=sellable, quantity=quantity, cost=cost, parent_item=parent, icms_st_value=icms_st_value, ipi_value=ipi_value) # # Properties # @property def discount_percentage(self): """Discount by percentage. Note that percentage must be added as an absolute value not as a factor like 1.05 = 5 % of surcharge The correct form is 'percentage = 3' for a discount of 3 %""" discount_value = self.discount_value if not discount_value: return currency(0) subtotal = self.purchase_subtotal assert subtotal > 0, (u'the subtotal should not be zero ' u'at this point') total = subtotal - discount_value percentage = (1 - total / subtotal) * 100 return quantize(percentage) @discount_percentage.setter def discount_percentage(self, value): self.discount_value = self._get_percentage_value(value) @property def surcharge_percentage(self): """Surcharge by percentage. Note that surcharge must be added as an absolute value not as a factor like 0.97 = 3 % of discount. The correct form is 'percentage = 3' for a surcharge of 3 %""" surcharge_value = self.surcharge_value if not surcharge_value: return currency(0) subtotal = self.purchase_subtotal assert subtotal > 0, (u'the subtotal should not be zero ' u'at this point') total = subtotal + surcharge_value percentage = ((total / subtotal) - 1) * 100 return quantize(percentage) @surcharge_percentage.setter def surcharge_percentage(self, value): self.surcharge_value = self._get_percentage_value(value) @property def payments(self): """Returns all valid payments for this purchase This will return a list of valid payments for this purchase, that is, all payments on the payment group that were not cancelled. If you need to get the cancelled too, use self.group.payments. If this purchase does not have a payment group, return a empty list. :returns: a list of |payment| """ return self.group.get_valid_payments() if self.group else [] # # Private # def _get_percentage_value(self, percentage): if not percentage: return currency(0) subtotal = self.purchase_subtotal percentage = Decimal(percentage) return subtotal * (percentage / 100) def _payback_paid_payments(self): paid_value = self.group.get_total_paid() # If we didn't pay anything yet, there is no need to create a payback. if not paid_value: return money = PaymentMethod.get_by_name(self.store, u'money') payment = money.create_payment( self.branch, self.station, Payment.TYPE_IN, self.group, paid_value, description=_('%s Money Returned for Purchase %s') % ('1/1', self.identifier)) payment.set_pending() payment.pay() # # Public API # def is_paid(self): if not self.group: return False for payment in self.payments: if not payment.is_paid(): return False return True def can_cancel(self): """Find out if it's possible to cancel the order :returns: True if it's possible to cancel the order, otherwise False """ # FIXME: Canceling partial orders disabled until we fix bug 3282 for item in self.get_items(): if item.has_partial_received(): return False return self.status in [ self.ORDER_QUOTING, self.ORDER_PENDING, self.ORDER_CONFIRMED ] def can_close(self): """Find out if it's possible to close the order :returns: True if it's possible to close the order, otherwise False """ # Consigned orders can be closed only after being confirmed if self.status == self.ORDER_CONSIGNED: return False for item in self.get_items(): if not item.has_been_received(): return False return True def confirm(self, responsible: LoginUser, confirm_date=None): """Confirms the purchase order :param confirm_data: optional, datetime """ if confirm_date is None: confirm_date = TransactionTimestamp() if self.status not in [ PurchaseOrder.ORDER_PENDING, PurchaseOrder.ORDER_CONSIGNED ]: fmt = _(u'Invalid order status, it should be ' u'ORDER_PENDING or ORDER_CONSIGNED, got %s') raise ValueError(fmt % (self.status_str, )) # In consigned purchases there is no payments at this point. if self.status != PurchaseOrder.ORDER_CONSIGNED and self.group: for payment in self.payments: payment.set_pending() if self.supplier and self.group: self.group.recipient = self.supplier.person self.responsible = responsible self.status = PurchaseOrder.ORDER_CONFIRMED self.confirm_date = confirm_date Event.log( self.store, Event.TYPE_ORDER, _(u"Order %s, total value %2.2f, supplier '%s' " u"is now confirmed") % (self.identifier, self.purchase_total, self.supplier.person.name)) def set_consigned(self, responsible: LoginUser): if self.status != PurchaseOrder.ORDER_PENDING: raise ValueError( _(u'Invalid order status, it should be ' u'ORDER_PENDING, got %s') % (self.status_str, )) self.responsible = responsible self.status = PurchaseOrder.ORDER_CONSIGNED def close(self): """Closes the purchase order """ if self.status != PurchaseOrder.ORDER_CONFIRMED: raise ValueError( _(u'Invalid status, it should be confirmed ' u'got %s instead') % self.status_str) self.status = self.ORDER_CLOSED Event.log( self.store, Event.TYPE_ORDER, _(u"Order %s, total value %2.2f, supplier '%s' " u"is now closed") % (self.identifier, self.purchase_total, self.supplier.person.name)) def cancel(self): """Cancels the purchase order """ assert self.can_cancel() # we have to cancel the payments too self._payback_paid_payments() self.group.cancel() self.status = self.ORDER_CANCELLED def receive_item(self, item, quantity_to_receive): if not item in self.get_pending_items(): raise StoqlibError( _(u'This item is not pending, hence ' u'cannot be received')) quantity = item.quantity - item.quantity_received if quantity < quantity_to_receive: raise StoqlibError( _(u'The quantity that you want to receive ' u'is greater than the total quantity of ' u'this item %r') % item) self.increase_quantity_received(item, quantity_to_receive) def increase_quantity_received(self, purchase_item, quantity_received): sellable = purchase_item.sellable items = [ item for item in self.get_items() if item.sellable.id == sellable.id ] qty = len(items) if not qty: raise ValueError( _(u'There is no purchase item for ' u'sellable %r') % sellable) purchase_item.quantity_received += quantity_received def update_products_cost(self): """Update purchase's items cost Update the costs of all products on this purchase to the costs specified in the order. """ for item in self.get_items(): # Since the only way the item have ipi_value is through importer of # a xml from stoqlink, and the cost will be always without ipi item.sellable.cost = item.cost + item.unit_ipi_value product = item.sellable.product product_supplier = product.get_product_supplier_info( self.supplier, self.branch) product_supplier.base_cost = item.cost + item.unit_ipi_value @property def status_str(self): return PurchaseOrder.translate_status(self.status) @property def freight_type_name(self): if not self.freight_type in self.freight_types.keys(): raise DatabaseInconsistency( _(u'Invalid freight_type, got %d') % self.freight_type) return self.freight_types[self.freight_type] @property def branch_name(self): return self.branch.get_description() @property def supplier_name(self): return self.supplier.get_description() @property def transporter_name(self): if not self.transporter: return u"" return self.transporter.get_description() @property def responsible_name(self): return self.responsible and self.responsible.get_description() or '' @property def purchase_subtotal(self): """Get the subtotal of the purchase. The sum of all the items cost * items quantity """ return currency( self.get_items().sum(PurchaseItem.cost * PurchaseItem.quantity) or 0) @property def purchase_total(self): subtotal = self.purchase_subtotal total = subtotal - self.discount_value + self.surcharge_value if total < 0: raise ValueError(_(u'Purchase total can not be lesser than zero')) # XXX: Since the purchase_total value must have two digits # (at the moment) we need to format the value to a 2-digit number and # then convert it to currency data type, because the subtotal value # may return a 3-or-more-digit value, depending on COST_PRECISION_DIGITS # parameters. return currency(get_formatted_price(total)) @property def received_total(self): """Like {purchase_subtotal} but only takes into account the received items """ return currency(self.get_items().sum( PurchaseItem.cost * PurchaseItem.quantity_received) or 0) def get_remaining_total(self): """The total value to be paid for the items not received yet """ return self.purchase_total - self.received_total def get_pending_items(self, with_children=True): """ Returns a sequence of all items which we haven't received yet. """ return self.get_items(with_children=with_children).find( PurchaseItem.quantity_received < PurchaseItem.quantity) def get_partially_received_items(self): """ Returns a sequence of all items which are partially received. """ return self.get_items().find(PurchaseItem.quantity_received > 0) def get_open_date_as_string(self): return self.open_date and self.open_date.strftime("%x") or u"" def get_quote_deadline_as_string(self): return self.quote_deadline and self.quote_deadline.strftime( "%x") or u"" def get_receiving_orders(self): """Returns all ReceivingOrder related to this purchase order """ from stoqlib.domain.receiving import PurchaseReceivingMap, ReceivingOrder tables = [PurchaseReceivingMap, ReceivingOrder] query = And(PurchaseReceivingMap.purchase_id == self.id, PurchaseReceivingMap.receiving_id == ReceivingOrder.id) return self.store.using(*tables).find(ReceivingOrder, query) def get_data_for_labels(self): """ This function returns some necessary data to print the purchase's items labels """ for purchase_item in self.get_items(): sellable = purchase_item.sellable label_data = Settable(barcode=sellable.barcode, code=sellable.code, description=sellable.description, price=sellable.price, sellable=sellable, quantity=purchase_item.quantity) yield label_data def has_batch_item(self): """Fetch the storables from this purchase order and returns ``True`` if any of them is a batch storable. :returns: ``True`` if this purchase order has batch items, ``False`` if it doesn't. """ return not self.store.find( Storable, And(self.id == PurchaseOrder.id, PurchaseOrder.id == PurchaseItem.order_id, PurchaseItem.sellable_id == Sellable.id, Sellable.id == Storable.id, Eq(Storable.is_batch, True))).is_empty() def create_receiving_order(self, station: BranchStation): from stoqlib.domain.receiving import ReceivingOrder receiving = ReceivingOrder(self.store, branch=self.branch, station=station) receiving.add_purchase(self) for item in self.get_items(): receiving.add_purchase_item(item, quantity=item.quantity, ipi_value=item.ipi_value) return receiving # # Classmethods # @classmethod def translate_status(cls, status): if not status in cls.statuses: raise DatabaseInconsistency( _(u'Got an unexpected status value: ' u'%s') % status) return cls.statuses[status] @classmethod def find_by_work_order(cls, store, work_order): return store.find(PurchaseOrder, work_order=work_order)
quantity = WorkOrderItem.quantity tables = [ OpticalWorkOrder, LeftJoin(WorkOrder, WorkOrder.id == OpticalWorkOrder.work_order_id), LeftJoin(WorkOrderItem, WorkOrderItem.order_id == WorkOrder.id), Join(Sellable, Sellable.id == WorkOrderItem.sellable_id), Join(OpticalProduct, OpticalProduct.product_id == Sellable.id), ] group_by = [id, work_order_id, work_order_item_id, optical_product_id] @property def sellable(self): return self.work_order_item.sellable @classmethod def find_by_order(cls, store, work_order): """Find all items on of the given work_order :param work_order: |work_order| """ return store.find(cls, work_order_id=work_order.id) Product.optical = Reference('id', 'OpticalProduct.product_id', on_remote=True) WorkOrder.optical_work_order = Reference('id', 'OpticalWorkOrder.work_order_id', on_remote=True)
class OpticalWorkOrder(Domain): """This holds the necessary information to execute an work order for optical stores. This includes all the details present in the prescription. For reference: http://en.wikipedia.org/wiki/Eyeglass_prescription See http://en.wikipedia.org/wiki/Eyeglass_prescription#Abbreviations_and_terms for reference no the names used here. In some places, RE is used as a short for right eye, and LE for left eye """ __storm_table__ = 'optical_work_order' #: Lens used in glasses LENS_TYPE_OPHTALMIC = u'ophtalmic' #: Contact lenses LENS_TYPE_CONTACT = u'contact' #: The frame for the lens is a closed ring FRAME_TYPE_CLOSED_RING = u'closed-ring' #: The frame uses a nylon string to hold the lenses. FRAME_TYPE_NYLON = u'nylon' #: The frame is made 3 pieces FRAME_TYPE_3_PIECES = u'3-pieces' lens_types = { LENS_TYPE_OPHTALMIC: _('Ophtalmic'), LENS_TYPE_CONTACT: _('Contact'), } frame_types = { # Translators: Aro fechado FRAME_TYPE_3_PIECES: _('Closed ring'), # Translators: Fio de nylon FRAME_TYPE_NYLON: _('Nylon String'), # Translators: 3 preças FRAME_TYPE_CLOSED_RING: _('3 pieces'), } work_order_id = IdCol(allow_none=False) work_order = Reference(work_order_id, 'WorkOrder.id') medic_id = IdCol() medic = Reference(medic_id, 'OpticalMedic.id') prescription_date = DateTimeCol() #: The name of the patient. Note that we already have the client of the work #: order, but the patient may be someone else (like the son, father, #: etc...). Just the name is enough patient = UnicodeCol() #: The type of the lens, Contact or Ophtalmic lens_type = EnumCol(default=LENS_TYPE_OPHTALMIC) # # Frame # #: The type of the frame. One of OpticalWorkOrder.FRAME_TYPE_* frame_type = EnumCol(default=FRAME_TYPE_CLOSED_RING) #: The vertical frame measure frame_mva = DecimalCol(default=decimal.Decimal(0)) #: The horizontal frame measure frame_mha = DecimalCol(default=decimal.Decimal(0)) #: The diagonal frame measure frame_mda = DecimalCol(default=decimal.Decimal(0)) #: The brige is the part of the frame between the two lenses, above the nose. frame_bridge = DecimalCol() # # Left eye distance vision # le_distance_spherical = DecimalCol(default=0) le_distance_cylindrical = DecimalCol(default=0) le_distance_axis = DecimalCol(default=0) le_distance_prism = DecimalCol(default=0) le_distance_base = DecimalCol(default=0) le_distance_height = DecimalCol(default=0) #: Pupil distance (DNP in pt_BR) le_distance_pd = DecimalCol(default=0) le_addition = DecimalCol(default=0) # # Left eye distance vision # le_near_spherical = DecimalCol(default=0) le_near_cylindrical = DecimalCol(default=0) le_near_axis = DecimalCol(default=0) #: Pupil distance (DNP in pt_BR) le_near_pd = DecimalCol(default=0) # # Right eye distance vision # re_distance_spherical = DecimalCol(default=0) re_distance_cylindrical = DecimalCol(default=0) re_distance_axis = DecimalCol(default=0) re_distance_prism = DecimalCol(default=0) re_distance_base = DecimalCol(default=0) re_distance_height = DecimalCol(default=0) #: Pupil distance (DNP in pt_BR) re_distance_pd = DecimalCol(default=0) re_addition = DecimalCol(default=0) # # Right eye near vision # re_near_spherical = DecimalCol(default=0) re_near_cylindrical = DecimalCol(default=0) re_near_axis = DecimalCol(default=0) #: Pupil distance (DNP in pt_BR) re_near_pd = DecimalCol(default=0) # # Class methods # @classmethod def find_by_work_order(cls, store, work_order): return store.find(cls, work_order_id=work_order.id).one() # # Properties # @property def frame_type_str(self): return self.frame_types.get(self.frame_type, '') @property def lens_type_str(self): return self.lens_types.get(self.lens_type, '') # # Public API # def can_create_purchase(self): work_order = self.work_order if work_order.status != WorkOrder.STATUS_WORK_IN_PROGRESS: return False if not work_order.sale: return False purchases = [i.purchase_item for i in work_order.get_items()] # If there are any item in this work order that was not purchased yet, then we # can still create a purchase return None in purchases def create_purchase(self, supplier, work_order_item, is_freebie, branch: Branch, station: BranchStation, user: LoginUser): """Create a purchase :param supplier: the |supplier| of that purchase :param work_order_item: The work order item that a purchase is being created for. :param is_freebie: indicates if the item is a freebie """ sellable = work_order_item.sellable store = self.work_order.store purchase = PurchaseOrder(store=store, branch=branch, station=station, status=PurchaseOrder.ORDER_PENDING, supplier=supplier, responsible=user, work_order=self.work_order) if is_freebie: purchase.notes = _( 'The product %s is a freebie') % sellable.description # FIXME We may want the cost 0, but as it is we wont be able to # receive this purchase without changing the receiving. We must # evaluate the consequences of changing the receiving a little bit # further in order to change that behavior. cost = decimal.Decimal('0.01') else: psi = ProductSupplierInfo.find_by_product_supplier( store, sellable.product, supplier, branch) cost = psi.base_cost if psi else sellable.cost # Add the sellable to the purchase purchase_item = purchase.add_item(sellable, quantity=work_order_item.quantity, cost=cost) work_order_item.purchase_item = purchase_item purchase.confirm(user) return purchase def can_receive_purchase(self, purchase): work_order = self.work_order if not work_order.status == WorkOrder.STATUS_WORK_FINISHED: return False # XXX Lets assume that there is only on purchase return purchase and purchase.status == PurchaseOrder.ORDER_CONFIRMED def receive_purchase(self, purchase_order: PurchaseOrder, station: BranchStation, user: LoginUser, reserve=False): receiving = purchase_order.create_receiving_order(station) receiving.confirm(user) if reserve: self.reserve_products(purchase_order, user) def reserve_products(self, purchase_order, user: LoginUser): for item in self.work_order.get_items(): if not item.purchase_item: continue sale_item = item.sale_item to_reserve = sale_item.quantity - sale_item.quantity_decreased if to_reserve > 0: sale_item.reserve(user, quantize(to_reserve)) def copy(self, target): """Make a copy of self into a target |work_order| :param target: a |work_order| """ props = [ 'lens_type', 'le_distance_spherical', 'le_distance_cylindrical', 'le_distance_axis', 'le_distance_prism', 'le_distance_base', 'le_distance_height', 'le_distance_pd', 'le_addition', 'le_near_spherical', 'le_near_cylindrical', 'le_near_axis', 'le_near_pd', 're_distance_spherical', 're_distance_cylindrical', 're_distance_axis', 're_distance_prism', 're_distance_base', 're_distance_height', 're_distance_pd', 're_addition', 're_near_spherical', 're_near_cylindrical', 're_near_axis', 're_near_pd' ] for prop in props: value = getattr(self, prop) setattr(target, prop, value)
class Domain(ORMObject): """The base domain for Stoq. This builds on top of :class:`stoqlib.database.properties.ORMObject` and adds: * created and modified |transactionentry|, a log which is mainly used by database synchonization, it allows us to find out what has been modified and created within a specific time frame. * create/update/modify hooks, which some domain objects implement to fire of events that can be used to find out when a specific domain event is triggered, eg a |sale| is created, a |product| is modified. etc. * cloning of domains, when you want to create a new similar domain * function to check if an value of a column is unique within a domain. Not all domain objects need to subclass Domain, some, such as :class:`stoqlib.domain.system.SystemTable` inherit from ORMObject. """ # FIXME: this is only used by pylint __storm_table__ = 'invalid' #: A list of fields from this object that should de added on the #: representation of this object (when calling repr()) repr_fields = [] #: id of this domain class, it's usually the primary key. #: it will automatically update when a new insert is created. #: Note that there might be holes in the sequence numbers, which happens #: due to aborted transactions id = IdCol(primary=True, default=AutoReload) te_id = IntCol(default=AutoReload) #: a |transactionentry| for when the domain object was created and last #: modified te = Reference(te_id, TransactionEntry.id) def __init__(self, *args, **kwargs): self._listen_to_events() ORMObject.__init__(self, *args, **kwargs) def __repr__(self): parts = ['%r' % self.id] for field in self.repr_fields: parts.append('%s=%r' % (field, getattr(self, field))) desc = ' '.join(parts) return '<%s %s>' % (self.__class__.__name__, desc) def __storm_loaded__(self): self._listen_to_events() def __storm_pre_flush__(self): obj_info = get_obj_info(self) pending = obj_info.get("pending") stoq_pending = obj_info.get('stoq-status') store = obj_info.get("store") if pending is PENDING_ADD: obj_info['stoq-status'] = _OBJ_CREATED elif pending is PENDING_REMOVE: obj_info['stoq-status'] = _OBJ_DELETED else: # This is storm's approach to check if the obj has pending changes, # but only makes sense if the obj is not being created/deleted. if (store._get_changes_map(obj_info, True) and stoq_pending not in [_OBJ_CREATED, _OBJ_DELETED]): obj_info['stoq-status'] = _OBJ_UPDATED # # Private # def _listen_to_events(self): event = get_obj_info(self).event event.hook('changed', self._on_object_changed) event.hook('before-removed', self._on_object_before_removed) event.hook('before-commited', self._on_object_before_commited) def _on_object_changed(self, obj_info, variable, old_value, value, from_db): # Only call the hook if the value is coming from python, and is a valid # value (not auto reload) and the value actually changed. if from_db or value is AutoReload or old_value == value: return self.on_object_changed(variable.column.name, old_value, value) def _on_object_before_removed(self, obj_info): # If the obj was created and them removed, nothing needs to be done. # It never really got into the database. if obj_info.get('stoq-status') == _OBJ_CREATED: obj_info['stoq-status'] = None else: self.on_delete() # This is emited right before the object is removed from the store. # We must also remove the transaction entry, but the entry should be # deleted *after* the object is deleted, thats why we need to specify # the flush order. store = obj_info.get("store") store.remove(self.te) store.add_flush_order(self, self.te) def _on_object_before_commited(self, obj_info): # on_create/on_update hooks can modify the object and make it be # flushed again, so lets reset pending before calling them stoq_pending = obj_info.get('stoq-status') obj_info['stoq-status'] = None if stoq_pending == _OBJ_CREATED: self.on_create() elif stoq_pending == _OBJ_UPDATED: self.on_update() # # Public API # def serialize(self): """Returns the object as a dictionary""" dictionary = {} for cls in self.__class__.__mro__: for key, value in cls.__dict__.items(): if isinstance(value, Property): attribute = getattr(self, key) # Handle Identifier Columns as string instead of int if type(attribute) == Identifier: attribute = str(attribute) dictionary[key] = attribute return dictionary def on_create(self): """Called when *self* is about to be created on the database This hook can be overridden on child classes for improved functionality. A trick you may want to use: Use :meth:`ORMObject.get_store` to get the :class:`store <stoqlib.database.runtime.StoqlibStore>` in which *self* lives and do your modifications in it. """ def on_update(self): """Called when *self* is about to be updated on the database This hook can be overridden on child classes for improved functionality. A trick you may want to use: Use :meth:`ORMObject.get_store` to get the :class:`store <stoqlib.database.runtime.StoqlibStore>` in which *self* lives and do your modifications in it. """ def on_delete(self): """Called when *self* is about to be removed from the database This hook can be overridden on child classes for improved functionality. A trick you may want to use: Use :meth:`ORMObject.get_store` to get the :class:`store <stoqlib.database.runtime.StoqlibStore>` in which *self* lives and use it to do a cleanup in other objects for instance, so if the store is rolled back, your cleanup will be rolled back too. Note that modifying *self* doesn't make sense since it will soon be removed from the database. """ def on_object_changed(self, attr, old_value, value): """Hook for when an attribute of this object changes. This is called when any property of the object has the value changed. This will only be called when the value is known (ie, not AutoReload) and the value comes from python, not from the database, and the value has actually changed from the previous value. """ pass def clone(self): """Get a persistent copy of an existent object. Remember that we can not use copy because this approach will not activate ORMObject methods which allow creating persitent objects. We also always need a new id for each copied object. :returns: the copy of ourselves """ warnings.warn("don't use this", DeprecationWarning, stacklevel=2) kwargs = {} for column in get_cls_info(self.__class__).columns: # FIXME: Make sure this is cloning correctly name = column.name if name in ['id', 'identifier', 'te_id']: continue if name.endswith('_id'): name = name[:-3] kwargs[name] = getattr(self, name) klass = type(self) return klass(store=self.store, **kwargs) def check_unique_value_exists(self, attribute, value, case_sensitive=True): """Check database for attribute/value precense Check if we already the given attribute/value pair in the database, but ignoring this object's ones. :param attribute: the attribute that should be unique :param value: value that we will check if exists in the database :param case_sensitive: If the checking should be case sensitive or not. :returns: the existing object or ``None`` """ return self.check_unique_tuple_exists({attribute: value}, case_sensitive) def check_unique_tuple_exists(self, values, case_sensitive=True): """Check database for values presence Check if we already the given attributes and values in the database, but ignoring this object's ones. :param values: dictionary of attributes:values that we will check if exists in the database. :param case_sensitive: If the checking should be case sensitive or not. :returns: the existing object or ``None`` """ if all([value in ['', None] for value in values.values()]): return None clauses = [] for attr, value, in values.items(): self.__class__.validate_attr(attr) if not isinstance(value, unicode) or case_sensitive: clauses.append(attr == value) else: clauses.append(Like(attr, value, case_sensitive=False)) cls = type(self) # Remove myself from the results. if hasattr(self, 'id'): clauses.append(cls.id != self.id) query = And(*clauses) try: return self.store.find(cls, query).one() except NotOneError: # FIXME: Instead of breaking stoq if more than one tuple exists, # simply return the first object, but log a warning about the # database issue. We should have UNIQUE constraints in more places # to be sure that this would never happen values_str = ["%s => %s" % (k.name, v) for k, v in values.items()] log.warning( "more than one result found when trying to " "check_unique_tuple_exists on table '%s' for values: %r" % (self.__class__.__name__, ', '.join(sorted(values_str)))) return self.store.find(cls, query).any() def merge_with(self, other, skip=None, copy_empty_values=True): """Does automatic references updating when merging two objects. This will update all tables that reference the `other` object and make them reference `self` instead. After this it should be safe to remove the `other` object. Since there is no one referencing it anymore. :param skip: A set of (table, column) that should be skiped by the automatic update. This are normally tables that require a special treatment, like when there are constraints. :param copy_empty_values: If True, attributes that are either null or an empty string in self will be updated with the value from the other object (given that the other attribute is not empty as well) """ skip = skip or set() event_skip = DomainMergeEvent.emit(self, other) if event_skip: skip = skip.union(event_skip) if copy_empty_values: self.copy_empty_values(other) refs = self.store.list_references(type(self).id) for (table, column, other_table, other_column, u, d) in refs: if (table, column) in skip: continue clause = Field(table, column) == other.id self.store.execute(Update({column: self.id}, clause, table)) def copy_empty_values(self, other): """Copies the values from other object if missing in self This will copy all values from the other object that are missing from this one. """ empty_values = [None, u''] for attr_property, column in self._storm_columns.items(): self_value = getattr(self, column.name) other_value = getattr(other, column.name) if self_value in empty_values and other_value not in empty_values: setattr(self, column.name, other_value) def can_remove(self, skip=None): """Check if this object can be removed from the database This will check if there's any object referencing self :param skip: an itarable containing the (table, column) to skip the check. Use this to avoid false positives when you will delete those skipped by hand before self. """ skip = skip or set() selects = [] refs = self.store.list_references(self.__class__.id) for t_name, c_name, ot_name, oc_name, u, d in refs: if (t_name, c_name) in skip: continue column = Field(t_name, c_name) selects.append( Select(columns=[1], tables=[t_name], where=column == self.id, limit=1)) # If everything was skipped, there's no query to execute if not len(selects): return True # We can only pass limit=1 to UnionAll if there's more than one select. # If not, storm will put the limit anyway and it will join with the # select's limit producing an error: multiple LIMIT clauses not allowed if len(selects) > 1: extra = {'limit': 1} else: extra = {} return not any(self.store.execute(UnionAll(*selects, **extra))) # # Classmethods # @classmethod def get_temporary_identifier(cls, store): """Returns a temporary negative identifier This should be used when working with syncronized databases and a purchase order is being created in a branch different than the destination branch. The sincronizer will be responsible for setting the definitive identifier once the order arives at the destination """ lower_value = store.find(cls).min(cls.identifier) return min(lower_value or 0, 0) - 1 @classmethod def find_distinct_values(cls, store, attr, exclude_empty=True): """Find distinct values for a given attr :param store: a store :param attr: the attr we are going to get distinct values for :param exclude_empty: if ``True``, empty results (``None`` or empty strings) will be removed from the results :returns: an iterator of the results """ cls.validate_attr(attr) results = store.find(cls) results.config(distinct=True) for value in results.values(attr): if exclude_empty and not value: continue yield value @classmethod def get_max_value(cls, store, attr, validate_attr=True, query=Undef): """Get the maximum value for a given attr On text columns, trying to find the max value for them using MAX() on postgres would result in some problems, like '9' being considered greater than '10' (because the comparison is done from left to right). This will 0-"pad" the values for the comparison, making it compare the way we want. Note that because of that, in the example above, it would return '09' instead of '9' :para store: a store :param attr: the attribute to find the max value for :returns: the maximum value for the attr """ if validate_attr: cls.validate_attr(attr, expected_type=UnicodeCol) max_length = Alias( Select(columns=[Alias(Max(CharLength(attr)), 'max_length')], tables=[cls], where=query), '_max_length') # Using LPad with max_length will workaround most of the cases where # the string comparison fails. For example, the common case would # consider '9' to be greater than '10'. We could test just strings # with length equal to max_length, but than '010' would be greater # than '001' would be greater than '10' (that would be excluded from # the comparison). By doing lpad, '09' is lesser than '10' and '001' # is lesser than '010', working around those cases if query is not Undef and query is not None: data = store.using(cls, max_length).find(cls, query) else: data = store.using(cls, max_length).find(cls) max_batch = data.max( LPad(attr, Field('_max_length', 'max_length'), u'0')) # Make the api consistent and return an ampty string instead of None # if there's no batch registered on the database return max_batch or u'' @classmethod def get_or_create(cls, store, **kwargs): """Get the object from the database that matches the given criteria, and if it doesn't exist, create a new object, with the properties given already set. The properties given ideally should be the primary key, or a candidate key (unique values). :returns: an object matching the query or a newly created one if a matching one couldn't be found. """ obj = store.find(cls, **kwargs).one() if obj is not None: return obj obj = cls(store=store) # Use setattr instead of passing it to the constructor, since not all # constructors accept all properties. There is no check if the # properties are valid since it will fail in the store.find() call above. for key, value in kwargs.items(): setattr(obj, key, value) return obj @classmethod def validate_attr(cls, attr, expected_type=None): """Make sure attr belongs to cls and has the expected type :param attr: the attr we will check if it is on cls :param expected_type: the expected type for the attr to be an instance of. If ``None`` will default to Property :raises: :exc:`TypeError` if the attr is not an instance of expected_type :raises: :exc:`ValueError` if the attr does not belong to this class """ expected_type = expected_type or Property if not issubclass(expected_type, Property): raise TypeError("expected_type %s needs to be a %s subclass" % (expected_type, Property)) # We need to iterate over cls._storm_columns to find the # attr's property because there's no reference to that property # (the descriptor) on the attr for attr_property, column in cls._storm_columns.items(): if column is attr: break else: attr_property = None # pylint raise ValueError("Domain %s does not have a column %s" % (cls.__name__, attr.name)) if not isinstance(attr_property, expected_type): raise TypeError("attr %s needs to be a %s instance" % (attr.name, expected_type)) @classmethod def validate_batch(cls, batch, sellable, storable=None): """Verifies if the given |batch| is valid for the given |sellable|. :param batch: A |storablebatch| that is being validated :param sellable: A |sellable| that we should use to validate against the batch :param storable: If provided, the corresponding |storable| for the given batch, to avoid unecessary queries. """ if not storable: product = sellable.product storable = product and product.storable or None if not batch: # When batch is not given, the only accepted scenarios are that storable is # None or the storable does not require batches if not storable or not storable.is_batch: return raise ValueError('Batch should not be None for %r' % sellable) # From now on, batch is not None if not storable: raise ValueError('Batches should only be used with storables, ' 'but %r is not one' % sellable) if not storable.is_batch: raise ValueError('This storable %r does not require a batch' % storable) if batch.storable != storable: raise ValueError('Given batch %r and storable %r are not related' % (batch, storable))
def __new__(cls, name, bases, dict): if Storm in bases or SQLObjectBase in bases: # Do not parse abstract base classes. return type.__new__(cls, name, bases, dict) style = cls._get_attr("_style", bases, dict) if style is None: dict["_style"] = style = SQLObjectStyle() table_name = cls._get_attr("_table", bases, dict) if table_name is None: table_name = style.pythonClassToDBTable(name) id_name = cls._get_attr("_idName", bases, dict) if id_name is None: id_name = style.idForTable(table_name) # Handle this later to call _parse_orderBy() on the created class. default_order = cls._get_attr("_defaultOrder", bases, dict) dict["__storm_table__"] = table_name attr_to_prop = {} for attr, prop in list(dict.items()): attr_to_prop[attr] = attr if isinstance(prop, ForeignKey): db_name = prop.kwargs.get("dbName", attr) local_prop_name = style.instanceAttrToIDAttr(attr) dict[local_prop_name] = local_prop = Int( db_name, allow_none=not prop.kwargs.get("notNull", False), validator=prop.kwargs.get("storm_validator", None)) dict[attr] = Reference(local_prop, "%s.<primary key>" % prop.foreignKey) attr_to_prop[attr] = local_prop_name elif isinstance(prop, PropertyAdapter): db_name = prop.dbName or attr method_name = prop.alternateMethodName if method_name is None and prop.alternateID: method_name = "by" + db_name[0].upper() + db_name[1:] if method_name is not None: def func(cls, key, attr=attr): store = cls._get_store() obj = store.find(cls, getattr(cls, attr) == key).one() if obj is None: raise SQLObjectNotFound return obj func.__name__ = method_name dict[method_name] = classmethod(func) elif isinstance(prop, SQLMultipleJoin): # Generate addFoo/removeFoo names. def define_add_remove(dict, prop): capitalised_name = (prop._otherClass[0].capitalize() + prop._otherClass[1:]) def add(self, obj): prop._get_bound_reference_set(self).add(obj) add.__name__ = "add" + capitalised_name dict.setdefault(add.__name__, add) def remove(self, obj): prop._get_bound_reference_set(self).remove(obj) remove.__name__ = "remove" + capitalised_name dict.setdefault(remove.__name__, remove) define_add_remove(dict, prop) id_type = dict.setdefault("_idType", int) id_cls = { int: Int, six.binary_type: RawStr, six.text_type: AutoUnicode }[id_type] dict["id"] = id_cls(id_name, primary=True, default=AutoReload) attr_to_prop[id_name] = "id" # Notice that obj is the class since this is the metaclass. obj = super(SQLObjectMeta, cls).__new__(cls, name, bases, dict) property_registry = obj._storm_property_registry property_registry.add_property(obj, getattr(obj, "id"), "<primary key>") # Let's explore this same mechanism to register table names, # so that we can find them to handle prejoinClauseTables. property_registry.add_property(obj, getattr(obj, "id"), "<table %s>" % table_name) for fake_name, real_name in list(attr_to_prop.items()): prop = getattr(obj, real_name) if fake_name != real_name: property_registry.add_property(obj, prop, fake_name) attr_to_prop[fake_name] = prop obj._attr_to_prop = attr_to_prop if default_order is not None: cls_info = get_cls_info(obj) cls_info.default_order = obj._parse_orderBy(default_order) return obj
class ECFPrinter(Domain): """ @param model: @param brand: @param device_name: @param device_serial: @param station: @param is_active: @param constants: @param baudrate: @cvar last_sale: reference for the last Sale @cvar last_till_entry: reference for the last TillEntry @cvar user_number: the current registrer user in the printer @cvar register_date: when the current user was registred @cvar register_cro: cro when the user was registred """ implements(IActive, IDescribable) __storm_table__ = 'ecf_printer' model = UnicodeCol() brand = UnicodeCol() device_name = UnicodeCol() device_serial = UnicodeCol() station_id = IntCol() station = Reference(station_id, 'BranchStation.id') is_active = BoolCol(default=True) baudrate = IntCol() last_sale_id = IntCol(default=None) last_sale = Reference(last_sale_id, 'Sale.id') last_till_entry_id = IntCol(default=None) last_till_entry = Reference(last_till_entry_id, 'TillEntry.id') user_number = IntCol(default=None) register_date = DateTimeCol(default=None) register_cro = IntCol(default=None) constants = ReferenceSet('id', 'DeviceConstant.printer_id') # # Public API # def create_fiscal_printer_constants(self): """ Creates constants for a fiscal printer This can be called multiple times """ # We only want to populate 'empty' objects. if not self.constants.find().is_empty(): return store = self.store driver = self.get_fiscal_driver() constants = driver.get_constants() for constant in constants.get_items(): constant_value = None if isinstance(constant, PaymentMethodType): constant_type = DeviceConstant.TYPE_PAYMENT elif isinstance(constant, UnitType): constant_type = DeviceConstant.TYPE_UNIT else: continue DeviceConstant(constant_type=constant_type, constant_name=unicode(describe_constant(constant)), constant_value=constant_value, constant_enum=int(constant), device_value=constants.get_value(constant, None), printer=self, store=store) for constant, device_value, value in driver.get_tax_constants(): # FIXME: Looks like this is not used and/or is duplicating code from # ecfpriterdialog.py (_populate_constants) if constant == TaxType.CUSTOM: constant_name = '%0.2f %%' % value else: constant_name = describe_constant(constant) DeviceConstant(constant_type=DeviceConstant.TYPE_TAX, constant_name=unicode(constant_name), constant_value=value, constant_enum=int(constant), device_value=device_value, printer=self, store=store) def get_constants_by_type(self, constant_type): """ Fetchs a list of constants for the current ECFPrinter object. @param constant_type: type of constant @type constant_type: :class:`DeviceConstant` @returns: list of constants """ return self.store.find(DeviceConstant, printer=self, constant_type=constant_type) def get_payment_constant(self, payment): """ @param payment: the payment whose method we will lookup the constant @returns: the payment constant @rtype: :class:`DeviceConstant` """ constant_enum = payment.method.operation.get_constant(payment) if constant_enum is None: raise AssertionError return self.store.find(DeviceConstant, printer=self, constant_type=DeviceConstant.TYPE_PAYMENT, constant_enum=int(constant_enum)).one() def get_tax_constant_for_device(self, sellable): """ Returns a tax_constant for a device Raises DeviceError if a constant is not found @param sellable: sellable which has the tax codes @type sellable: :class:`stoqlib.domain.sellable.Sellable` @returns: the tax constant @rtype: :class:`DeviceConstant` """ sellable_constant = sellable.get_tax_constant() if sellable_constant is None: raise DeviceError("No tax constant set for sellable %r" % sellable) store = self.store if sellable_constant.tax_type == TaxType.CUSTOM: constant = DeviceConstant.get_custom_tax_constant( self, sellable_constant.tax_value, store) if constant is None: raise DeviceError( _("fiscal printer is missing a constant for the custom " "tax constant '%s'") % (sellable_constant.description, )) else: constant = DeviceConstant.get_tax_constant( self, sellable_constant.tax_type, store) if constant is None: raise DeviceError( _("fiscal printer is missing a constant for tax " "constant '%s'") % (sellable_constant.description, )) return constant def get_fiscal_driver(self): if self.brand == 'virtual': port = VirtualPort() else: port = SerialPort(device=self.device_name, baudrate=self.baudrate) return FiscalPrinter(brand=self.brand, model=self.model, port=port) def set_user_info(self, user_info): self.user_number = user_info.user_number self.register_cro = user_info.cro self.register_date = user_info.register_date # # IActive implementation # def inactivate(self): self.is_active = False def activate(self): self.is_active = True def get_status_string(self): if self.is_active: return _(u'Active') return _(u'Inactive') # # IDescribable implementation # def get_description(self): # Quick workaround to avoid calling FiscalPrinter.setup(), since that # may send commands to the ECF, and we just need the description. # TODO: Improve stoqdrivers so we can get this easyer port = VirtualPort() port.setTimeout = lambda x: True port.setParity = lambda x: True port.setWriteTimeout = lambda x: True driver = BasePrinter(brand=self.brand, model=self.model, port=port) return driver.get_model_name() @classmethod def get_last_document(cls, station, store): return store.find(cls, station=station, is_active=True).one()
class ProductionOrder(IdentifiableDomain): """Production Order object implementation. """ __storm_table__ = 'production_order' #: The production order is opened, production items might have been added. ORDER_OPENED = u'opened' #: The production order is waiting some conditions to start the #: manufacturing process. ORDER_WAITING = u'waiting' #: The production order have already started. ORDER_PRODUCING = u'producing' #: The production is in quality assurance phase. ORDER_QA = u'quality-assurance' #: The production have finished. ORDER_CLOSED = u'closed' #: Production cancelled ORDER_CANCELLED = u'cancelled' statuses = collections.OrderedDict([ (ORDER_OPENED, _(u'Opened')), (ORDER_WAITING, _(u'Waiting')), (ORDER_PRODUCING, _(u'Producing')), (ORDER_QA, _(u'Quality Assurance')), (ORDER_CLOSED, _(u'Closed')), (ORDER_CANCELLED, _(u'Cancelled')), ]) #: A numeric identifier for this object. This value should be used instead of #: :obj:`Domain.id` when displaying a numerical representation of this object to #: the user, in dialogs, lists, reports and such. identifier = IdentifierCol() #: the production order status status = EnumCol(allow_none=False, default=ORDER_OPENED) #: the date when the production order was created open_date = DateTimeCol(default_factory=localnow) #: the date when the production order have been closed close_date = DateTimeCol(default=None) #: the date when the production order have been cancelled cancel_date = DateTimeCol(default=None) #: the production order description description = UnicodeCol(default=u'') expected_start_date = DateTimeCol(default=None) start_date = DateTimeCol(default=None) responsible_id = IdCol(default=None) #: the person responsible for the production order responsible = Reference(responsible_id, 'Employee.id') branch_id = IdCol() #: branch this production belongs to branch = Reference(branch_id, 'Branch.id') station_id = IdCol(allow_none=False) #: The station this object was created at station = Reference(station_id, 'BranchStation.id') produced_items = ReferenceSet('id', 'ProductionProducedItem.order_id') # # IContainer implmentation # def get_items(self): return self.store.find(ProductionItem, order=self) def add_item(self, sellable, quantity=Decimal(1)): return ProductionItem(order=self, product=sellable.product, quantity=quantity, store=self.store) def remove_item(self, item): assert isinstance(item, ProductionItem) if item.order is not self: raise ValueError( _(u'Argument item must have an order attribute ' u'associated with the current production ' u'order instance.')) item.order = None self.store.maybe_remove(item) # # Public API # def can_cancel(self): """Checks if this order can be cancelled Only orders that didn't start yet can be canceled, this means only opened and waiting productions. """ return self.status in [self.ORDER_OPENED, self.ORDER_WAITING] def can_finalize(self): """Checks if this order can be finalized Only orders that didn't start yet can be canceled, this means only producing and waiting qa productions. """ return self.status in [self.ORDER_PRODUCING, self.ORDER_QA] def get_service_items(self): """Returns all the services needed by this production. :returns: a sequence of :class:`ProductionService` instances. """ return self.store.find(ProductionService, order=self) def remove_service_item(self, item): assert isinstance(item, ProductionService) if item.order is not self: raise ValueError( _(u'Argument item must have an order attribute ' u'associated with the current production ' u'order instance.')) item.order = None self.store.maybe_remove(item) def get_material_items(self): """Returns all the material needed by this production. :returns: a sequence of :class:`ProductionMaterial` instances. """ return self.store.find( ProductionMaterial, order=self, ) def start_production(self, user: LoginUser): """Start the production by allocating all the material needed. """ assert self.status in [ ProductionOrder.ORDER_OPENED, ProductionOrder.ORDER_WAITING ] for material in self.get_material_items(): material.allocate(user) self.start_date = localtoday() self.status = ProductionOrder.ORDER_PRODUCING def cancel(self): """Cancel the production when this is Open or Waiting. """ assert self.can_cancel() self.status = self.ORDER_CANCELLED self.cancel_date = localtoday() def is_completely_produced(self): return all(i.is_completely_produced() for i in self.get_items()) def is_completely_tested(self): # Produced items are only stored if there are quality tests for this # product produced_items = list(self.produced_items) if not produced_items: return True return all([item.test_passed for item in produced_items]) def try_finalize_production(self, user: LoginUser, ignore_completion=False): """When all items are completely produced, change the status of the production to CLOSED. """ assert self.can_finalize(), self.status if ignore_completion: is_produced = True else: is_produced = self.is_completely_produced() is_tested = self.is_completely_tested() if is_produced and not is_tested: # Fully produced but not fully tested. Keep status as QA self.status = ProductionOrder.ORDER_QA elif is_produced and is_tested: # All items must be completely produced and tested self.close_date = localtoday() self.status = ProductionOrder.ORDER_CLOSED # If the order is closed, return the the remaining allocated material to # the stock if self.status == ProductionOrder.ORDER_CLOSED: # Return remaining allocated material to the stock for m in self.get_material_items(): m.return_remaining(user) # Increase the stock for the produced items for p in self.produced_items: p.send_to_stock(user) def set_production_waiting(self): assert self.status == ProductionOrder.ORDER_OPENED self.status = ProductionOrder.ORDER_WAITING def get_status_string(self): return ProductionOrder.statuses[self.status] def get_branch_name(self): return self.branch.get_description() def get_responsible_name(self): if self.responsible is not None: return self.responsible.person.name return u'' # # IDescribable implementation # def get_description(self): return self.description
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) # # Properties # @property def unit_ipi_value(self): """Calculate the ipi for each unit of the item""" return currency(quantize(self.ipi_value / self.quantity)) # # Accessors # def get_total(self): return currency((self.quantity * self.cost) + self.ipi_value) def get_total_sold(self): return currency(self.quantity_sold * self.cost) def get_received_total(self): return currency((self.quantity_received * self.cost) + self.ipi_value) 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, user: LoginUser, 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, user=user) 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 ProductionProducedItem(Domain): """This class represents a composed product that was produced, but didn't enter the stock yet. Its used mainly for the quality assurance process """ __storm_table__ = 'production_produced_item' order_id = IdCol() order = Reference(order_id, 'ProductionOrder.id') # ProductionItem already has a reference to Product, but we need it for # constraint checks UNIQUE(product_id, serial_number) product_id = IdCol() product = Reference(product_id, 'Product.id') produced_by_id = IdCol() produced_by = Reference(produced_by_id, 'LoginUser.id') produced_date = DateTimeCol() serial_number = IntCol() entered_stock = BoolCol(default=False) test_passed = BoolCol(default=False) test_results = ReferenceSet( 'id', 'ProductionItemQualityResult.produced_item_id') def get_pending_tests(self): tests_done = set([t.quality_test for t in self.test_results]) all_tests = set(self.product.quality_tests) return list(all_tests.difference(tests_done)) @classmethod def get_last_serial_number(cls, product, store): return store.find(cls, product=product).max(cls.serial_number) or 0 @classmethod def is_valid_serial_range(cls, product, first, last, store): query = And(cls.product_id == product.id, cls.serial_number >= first, cls.serial_number <= last) # There should be no results for the range to be valid return store.find(cls, query).is_empty() def send_to_stock(self, user: LoginUser): # Already is in stock if self.entered_stock: return storable = self.product.storable storable.increase_stock(1, self.order.branch, StockTransactionHistory.TYPE_PRODUCTION_SENT, self.id, user) self.entered_stock = True def set_test_result_value(self, quality_test, value, tester): store = self.store result = store.find(ProductionItemQualityResult, quality_test=quality_test, produced_item=self).one() if not result: result = ProductionItemQualityResult(store=self.store, quality_test=quality_test, produced_item=self, tested_by=tester, result_value=u'') else: result.tested_by = tester result.tested_date = localnow() result.set_value(tester, value) return result def get_test_result(self, quality_test): store = self.store return store.find(ProductionItemQualityResult, quality_test=quality_test, produced_item=self).one() def check_tests(self, user: LoginUser): """Checks if all tests for this produced items passes. If all tests passes, sets self.test_passed = True """ results = [i.test_passed for i in self.test_results] passed = all(results) self.test_passed = (passed and len(results) == self.product.quality_tests.count()) if self.test_passed: self.order.try_finalize_production(user)
class QuoteGroup(IdentifiableDomain): __storm_table__ = 'quote_group' #: A numeric identifier for this object. This value should be used instead of #: :obj:`Domain.id` when displaying a numerical representation of this object to #: the user, in dialogs, lists, reports and such. identifier = IdentifierCol() branch_id = IdCol() branch = Reference(branch_id, 'Branch.id') station_id = IdCol(allow_none=False) #: The station this object was created at station = Reference(station_id, 'BranchStation.id') # # IContainer # def get_items(self): return self.store.find(Quotation, group=self) def remove_item(self, item): if item.group is not self: raise ValueError( _(u'You can not remove an item which does not ' u'belong to this group.')) order = item.purchase # FIXME: Bug 5581 Removing objects with synced databases is dangerous. # Investigate this usage self.store.remove(item) for order_item in order.get_items(): order.remove_item(order_item) self.store.remove(order) def add_item(self, item): store = self.store return Quotation(store=store, purchase=item, group=self, branch=self.branch, station=self.station) # # IDescribable # def get_description(self): return _(u"quote number %s") % self.identifier # # Public API # def cancel(self): """Cancel a quote group.""" store = self.store for quote in self.get_items(): quote.close() # FIXME: Bug 5581 Removing objects with synced databases is # dangerous. Investigate this usage store.remove(quote)
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 quantize(self.price * self.quantity) # # IInvoiceItem implementation # @property def base_price(self): return self.price @property def parent(self): return self.returned_sale @property def cfop_code(self): return u'1202' # # 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, StockTransactionHistory.TYPE_UNDO_RETURNED_SALE, self.id, batch=self.batch) if self.sale_item: self.sale_item.quantity_decreased += self.quantity def maybe_remove(self): """Will eventualy remove the object from database""" for child in self.children_items: # Make sure to remove children before remove itself if child.can_remove(): self.store.remove(child) if self.can_remove(): self.store.remove(self) def can_remove(self): """Check if the ReturnedSaleItem can be removed from database If the item is a package, check if all of its children are being returned """ product = self.sellable.product if product and product.is_package and not bool(self.quantity): return not any( bool(child.quantity) for child in self.children_items) return not bool(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 DeviceSettings(Domain): implements(IActive, IDescribable) __storm_table__ = 'device_settings' type = IntCol() brand = UnicodeCol() model = UnicodeCol() device_name = UnicodeCol() station_id = IntCol() station = Reference(station_id, 'BranchStation.id') is_active = BoolCol(default=True) (SCALE_DEVICE, _UNUSED, CHEQUE_PRINTER_DEVICE) = range(1, 4) device_types = { SCALE_DEVICE: _(u'Scale'), CHEQUE_PRINTER_DEVICE: _(u'Cheque Printer') } # # Domain # def get_printer_description(self): return u"%s %s" % (self.brand.capitalize(), self.model) def get_device_type_name(self, type=None): return DeviceSettings.device_types[type or self.type] # XXX: Maybe stoqdrivers can implement a generic way to do this? def get_interface(self): """ Based on the column values instantiate the stoqdrivers interface for the device itself. """ port = SerialPort(device=self.device_name) if self.type == DeviceSettings.CHEQUE_PRINTER_DEVICE: return ChequePrinter(brand=self.brand, model=self.model, port=port) elif self.type == DeviceSettings.SCALE_DEVICE: return Scale(brand=self.brand, model=self.model, device=self.device_name) raise DatabaseInconsistency("The device type referred by this " "record (%r) is invalid, given %r." % (self, self.type)) def is_a_printer(self): return self.type == DeviceSettings.CHEQUE_PRINTER_DEVICE def is_valid(self): return (all((self.model, self.device_name, self.brand, self.station)) and self.type in DeviceSettings.device_types) @classmethod def get_by_station_and_type(cls, store, station, type): """Fetch all settings for a specific station and type. :param store: a store :param station: a BranchStation instance :param type: device type """ return store.find(cls, station=station, type=type) @classmethod def get_scale_settings(cls, store): """ Get the scale device settings for the current station :param store: a store :returns: a :class:`DeviceSettings` object or None if there is none """ station = get_current_station(store) return store.find(cls, station=station, type=cls.SCALE_DEVICE).one() # # IActive implementation # def inactivate(self): self.is_active = False def activate(self): self.is_active = True def get_status_string(self): if self.is_active: return _(u'Active') return _(u'Inactive') # # IDescribable implementation # def get_description(self): return self.get_printer_description()
class OpticalPatientHistory(Domain): __storm_table__ = 'optical_patient_history' #: Never used lenses before TYPE_FIRST_USER = u'first-user' #: Is currently a user TYPE_SECOND_USER = u'second-user' #: Has used lenses before, but stopped TYPE_EX_USER = u'ex-user' user_types = collections.OrderedDict([ (TYPE_FIRST_USER, _('First User')), (TYPE_SECOND_USER, _('Second User')), (TYPE_EX_USER, _('Ex-User')), ]) create_date = DateTimeCol(default_factory=StatementTimestamp) client_id = IdCol(allow_none=False) #: The related client client = Reference(client_id, 'Client.id') responsible_id = IdCol(allow_none=False) #: The user that registred this information responsible = Reference(responsible_id, 'LoginUser.id') # # Section 1: General questions # #: If the patient is a first time user for contact lenses or not. user_type = EnumCol(allow_none=False, default=TYPE_FIRST_USER) #: What is the occupation of the patient occupation = UnicodeCol() #: Details about the work environment (if it as air conditioning, dust, #: chemical products) work_environment = UnicodeCol() # # First time user # #: If the patient has ever tested any contact lenses has_tested = UnicodeCol() #: What brands the patient has tested tested_brand = UnicodeCol() #: If previous tests irritated the eye eye_irritation = UnicodeCol() #: What is the main purpose for using contact lenses? purpose_of_use = UnicodeCol() #: How many hours per day the patient intends to use the contact lenses intended_hour_usage = UnicodeCol() # # Second time / ex user # #: Previous brand of the client. previous_brand = UnicodeCol() #: What the previous brand felt like previous_feeling = UnicodeCol() #: Have ever had any cornea issues cornea_issues = UnicodeCol() #: How many hours per day the client used the lenses hours_per_day_usage = UnicodeCol() # # Second time user # #: For how long is a user user_since = UnicodeCol() #: Bring the previous lenses? has_previous_lenses = UnicodeCol() #: Previous lenses observations previous_lenses_notes = UnicodeCol() # # Ex User # #: How long since the last use. last_use = UnicodeCol() #: why stopped using stop_reason = UnicodeCol() #: Did frequent removal of proteins? protein_removal = UnicodeCol() #: What cleaning product used? cleaning_product = UnicodeCol() #: Free notes. history_notes = UnicodeCol() # # Section 2: Adaptation test # #: If the patient ever had eye injuries eye_injury = UnicodeCol() #: Any kind of recent pathology, like pink-eye recent_pathology = UnicodeCol() #: Is currently using eye drops using_eye_drops = UnicodeCol() #: Does the patient have health problems health_problems = UnicodeCol() #: Is the patient is using any kind of medicament using_medicament = UnicodeCol() #: Does the patient family has any health problems family_health_problems = UnicodeCol() #: How the eyes feel at the end of the day (burn, itch, etc...) end_of_day_feeling = UnicodeCol() #: Free notes. adaptation_notes = UnicodeCol() @property def responsible_name(self): return self.responsible.get_description()
class SellableCategory(Domain): """ A Sellable category. A way to group several |sellables| together, like "Shoes", "Consumer goods", "Services". A category can define markup, tax and commission, the values of the category will only be used when the sellable itself lacks a value. Sellable categories can be grouped recursively. See also: `schema <http://doc.stoq.com.br/schema/tables/sellable_category.html>`__ """ __storm_table__ = 'sellable_category' #: The category description description = UnicodeCol() #: A color that will be used in reports and interface for this category color = UnicodeCol() #: The sort order of this category. Usefull when you need a category to appear before all other #: categories sort_order = IntCol(default=0) #: Define the suggested markup when calculating the sellable's price. suggested_markup = PercentCol(default=0) #: A percentage comission suggested for all the sales which products #: belongs to this category. salesperson_commission = PercentCol(default=0) category_id = IdCol(default=None) #: base category of this category, ``None`` for base categories themselves category = Reference(category_id, 'SellableCategory.id') tax_constant_id = IdCol(default=None) #: the |sellabletaxconstant| for this sellable category tax_constant = Reference(tax_constant_id, 'SellableTaxConstant.id') #: the children of this category children = ReferenceSet('id', 'SellableCategory.category_id') # # Properties # @property def full_description(self): """The full description of the category, including its parents, for instance: u"Clothes:Shoes:Black Shoe 14 SL" """ descriptions = [self.description] parent = self.category while parent: descriptions.append(parent.description) parent = parent.category return u':'.join(reversed(descriptions)) # # Public API # def get_children_recursively(self): """Return all the children from this category, recursively This will return all children recursively, e.g.:: A / \ B C / \ D E In this example, calling this from A will return ``set([B, C, D, E])`` """ children = set(self.children) if not len(children): # Base case for the leafs return set() for child in list(children): children |= child.get_children_recursively() return children def get_commission(self): """Returns the commission for this category. If it's unset, return the value of the base category, if any :returns: the commission """ if self.category: return (self.salesperson_commission or self.category.get_commission()) return self.salesperson_commission def get_markup(self): """Returns the markup for this category. If it's unset, return the value of the base category, if any :returns: the markup """ if self.category: # Compare to None as markup can be '0' if self.suggested_markup is not None: return self.suggested_markup return self.category.get_markup() return self.suggested_markup def get_tax_constant(self): """Returns the tax constant for this category. If it's unset, return the value of the base category, if any :returns: the tax constant """ if self.category: return self.tax_constant or self.category.get_tax_constant() return self.tax_constant # # IDescribable # def get_description(self): return self.description # # Classmethods # @classmethod def get_base_categories(cls, store): """Returns all available base categories :param store: a store :returns: categories """ return store.find(cls, category_id=None) # # Domain hooks # def on_create(self): CategoryCreateEvent.emit(self) def on_update(self): CategoryEditEvent.emit(self)
class OpticalPatientTest(Domain): __storm_table__ = 'optical_patient_test' create_date = DateTimeCol(default_factory=StatementTimestamp) client_id = IdCol(allow_none=False) #: The related client client = Reference(client_id, 'Client.id') responsible_id = IdCol(allow_none=False) #: The user that registred this information responsible = Reference(responsible_id, 'LoginUser.id') #: The contact lens that is being tested. This could be a reference to a #: |product in the future le_item = UnicodeCol() #: The brand of the tested contact lenses le_brand = UnicodeCol() #: Curva Base - CB le_base_curve = UnicodeCol() le_spherical_degree = UnicodeCol() le_cylindrical = UnicodeCol() le_axis = UnicodeCol() le_diameter = UnicodeCol() le_movement = UnicodeCol() le_centralization = UnicodeCol() le_spin = UnicodeCol() le_fluorescein = UnicodeCol() #: Sobre refração - SRF le_over_refraction = UnicodeCol() le_bichrome = UnicodeCol() #: If the client is satisfied with this product le_client_approved = BoolCol() #: If the client has purchased this product after the test. le_client_purchased = BoolCol() #: If the product being tested was delivered to the client le_delivered = BoolCol() re_item = UnicodeCol() re_brand = UnicodeCol() re_base_curve = UnicodeCol() re_spherical_degree = UnicodeCol() re_cylindrical = UnicodeCol() re_axis = UnicodeCol() re_diameter = UnicodeCol() re_movement = UnicodeCol() re_centralization = UnicodeCol() re_spin = UnicodeCol() re_fluorescein = UnicodeCol() re_over_refraction = UnicodeCol() re_bichrome = UnicodeCol() re_client_approved = BoolCol() re_client_purchased = BoolCol() re_delivered = BoolCol() #: Free notes notes = UnicodeCol() @property def responsible_name(self): return self.responsible.get_description()
class Sellable(Domain): """ Sellable information of a certain item such a |product| or a |service|. See also: `schema <http://doc.stoq.com.br/schema/tables/sellable.html>`__ """ __storm_table__ = 'sellable' #: the sellable is available and can be used on a |purchase|/|sale| STATUS_AVAILABLE = u'available' #: the sellable is closed, that is, it still exists for references, #: but it should not be possible to create a |purchase|/|sale| with it STATUS_CLOSED = u'closed' statuses = collections.OrderedDict([ (STATUS_AVAILABLE, _(u'Available')), (STATUS_CLOSED, _(u'Closed')), ]) #: a code used internally by the shop to reference this sellable. #: It is usually not printed and displayed to |clients|, barcode is for that. #: It may be used as an shorter alternative to the barcode. code = UnicodeCol(default=u'', validator=_validate_code) #: barcode, mostly for products, usually printed and attached to the #: package. barcode = UnicodeCol(default=u'', validator=_validate_barcode) #: status the sellable is in status = EnumCol(allow_none=False, default=STATUS_AVAILABLE) #: cost of the sellable, this is not tied to a specific |supplier|, #: which may have a different cost. This can also be the production cost of #: manufactured item by the company. cost = PriceCol(default=0) #: price of sellable, how much the |client| paid. base_price = PriceCol(default=0) #: the last time the cost was updated cost_last_updated = DateTimeCol(default_factory=localnow) #: the last time the price was updated price_last_updated = DateTimeCol(default_factory=localnow) #: full description of sellable description = UnicodeCol(default=u'') #: short description of sellable short_description = UnicodeCol(default=u'') #: maximum discount allowed max_discount = PercentCol(default=0) #: commission to pay after selling this sellable commission = PercentCol(default=0) #: A sort order to override default alphabetic order in lists. sort_order = IntCol() #: If this is a favorite sellable favorite = BoolCol() #: Some keywords for this sellable. keywords = UnicodeCol() #: notes for the sellable notes = UnicodeCol(default=u'') unit_id = IdCol(default=None) #: the |sellableunit|, quantities of this sellable are in this unit. unit = Reference(unit_id, 'SellableUnit.id') category_id = IdCol(default=None) #: a reference to category table category = Reference(category_id, 'SellableCategory.id') tax_constant_id = IdCol(default=None) #: the |sellabletaxconstant|, this controls how this sellable is taxed tax_constant = Reference(tax_constant_id, 'SellableTaxConstant.id') #: the |product| for this sellable or ``None`` product = Reference('id', 'Product.id', on_remote=True) #: the |service| for this sellable or ``None`` service = Reference('id', 'Service.id', on_remote=True) #: the |storable| for this |product|'s sellable product_storable = Reference('id', 'Storable.id', on_remote=True) default_sale_cfop_id = IdCol(default=None) #: the default |cfop| that will be used when selling this sellable default_sale_cfop = Reference(default_sale_cfop_id, 'CfopData.id') #: A special price used when we have a "on sale" state, this #: can be used for promotions on_sale_price = PriceCol(default=0) #: When the promotional/special price starts to apply on_sale_start_date = DateTimeCol(default=None) #: When the promotional/special price ends on_sale_end_date = DateTimeCol(default=None) #: This sellable's images images = ReferenceSet('id', 'Image.sellable_id') #: specifies whether the product requires kitchen production requires_kitchen_production = BoolCol(default=False) def __init__(self, store=None, category=None, cost=None, commission=None, description=None, price=None): """Creates a new sellable :param store: a store :param category: category of this sellable :param cost: the cost, defaults to 0 :param commission: commission for this sellable :param description: readable description of the sellable :param price: the price, defaults to 0 """ Domain.__init__(self, store=store) if category: if commission is None: commission = category.get_commission() if price is None and cost is not None: markup = category.get_markup() price = self._get_price_by_markup(markup, cost=cost) self.category = category self.commission = commission or currency(0) self.cost = cost or currency(0) self.description = description self.price = price or currency(0) # # Helper methods # def _get_price_by_markup(self, markup, cost=None): if cost is None: cost = self.cost return currency(quantize(cost + (cost * (markup / currency(100))))) def _get_from_override(self, attr, branch): """Get an attribute from SellableBranchOverride :param attr: a string with a sellable attribute name :param branch: a branch :returns: The value of an attribute from the sellable_branch_override of a sellable, or the attribute from the actual sellable """ override = SellableBranchOverride.find_by_sellable(sellable=self, branch=branch) value = getattr(self, attr) if override is None: return value override_value = getattr(override, attr) return override_value if override_value is not None else value # # Properties # @property def status_str(self): """The sellable status as a string""" return self.statuses[self.status] @property def unit_description(self): """Returns the description of the |sellableunit| of this sellable :returns: the unit description or an empty string if no |sellableunit| was set. :rtype: unicode """ return self.unit and self.unit.description or u"" @property def image(self): """This sellable's main image.""" # FIXME: Should we use .first() here? What will happen if there are # more than one image with "is_main" flag set to True? There's no way # to prevent that in the database return self.images.find(is_main=True).one() @property def markup(self): """Markup, the opposite of discount, a value added on top of the sale. It's calculated as:: ((cost/price)-1)*100 """ if self.cost == 0: return Decimal(0) return ((self.price / self.cost) - 1) * 100 @markup.setter def markup(self, markup): self.price = self._get_price_by_markup(markup) @property def price(self): if self.is_on_sale(): return self.on_sale_price else: category = sysparam.get_object(self.store, 'DEFAULT_TABLE_PRICE') if category: info = self.get_category_price_info(category) if info: return info.price return self.base_price @price.setter def price(self, price): if price < 0: # Just a precaution for gui validation fails. price = 0 if self.is_on_sale(): self.on_sale_price = price else: self.base_price = price # # Accessors # def is_available(self, branch): """Whether the sellable is available and can be sold. :returns: ``True`` if the item can be sold, ``False`` otherwise. """ # FIXME: Perhaps this should be done elsewhere. Johan 2008-09-26 if sysparam.compare_object('DELIVERY_SERVICE', self.service): return True status = self._get_from_override('status', branch) return status == self.STATUS_AVAILABLE def set_available(self, branch): """Mark the sellable as available Being available means that it can be ordered or sold. :raises: :exc:`ValueError`: if the sellable is already available """ if self.is_available(branch): raise ValueError('This sellable is already available') self.status = self.STATUS_AVAILABLE def is_closed(self, branch): """Whether the sellable is closed or not. :returns: ``True`` if closed, ``False`` otherwise. """ return not self.is_available(branch) def close(self, branch): """Mark the sellable as closed. After the sellable is closed, this will call the close method of the service or product related to this sellable. :raises: :exc:`ValueError`: if the sellable is already closed """ if self.is_closed(branch): raise ValueError('This sellable is already closed') assert self.can_close() self.status = Sellable.STATUS_CLOSED obj = self.service or self.product obj.close() def can_remove(self): """Whether we can delete this sellable from the database. ``False`` if the product/service was used in some cases below:: - Sold or received - The |product| is in a |purchase| """ if self.product and not self.product.can_remove(): return False if self.service and not self.service.can_remove(): return False return super(Sellable, self).can_remove( skip=[('product', 'id'), ('service', 'id'), ( 'image', 'sellable_id'), ('client_category_price', 'sellable_id')]) def can_close(self): """Whether we can close this sellable. :returns: ``True`` if the product has no stock left or the service is not required by the system (i.e. Delivery service is required). ``False`` otherwise. """ obj = self.service or self.product return obj.can_close() def get_commission(self): return self.commission def get_suggested_markup(self): """Returns the suggested markup for the sellable :returns: suggested markup :rtype: decimal """ return self.category and self.category.get_markup() def get_category_description(self): """Returns the description of this sellables category If it's unset, return the constant from the category, if any :returns: sellable category description or an empty string if no |sellablecategory| was set. :rtype: unicode """ category = self.category return category and category.description or u"" def get_tax_constant(self): """Returns the |sellabletaxconstant| for this sellable. If it's unset, return the constant from the category, if any :returns: the |sellabletaxconstant| or ``None`` if unset """ if self.tax_constant: return self.tax_constant if self.category: return self.category.get_tax_constant() def get_category_prices(self): """Returns all client category prices associated with this sellable. :returns: the client category prices """ return self.store.find(ClientCategoryPrice, sellable=self) def get_category_price_info(self, category): """Returns the :class:`ClientCategoryPrice` information for the given :class:`ClientCategory` and this |sellable|. :returns: the :class:`ClientCategoryPrice` or ``None`` """ info = self.store.find(ClientCategoryPrice, sellable=self, category=category).one() return info def get_price_for_category(self, category): """Given the |clientcategory|, returns the price for that category or the default sellable price. :param category: a |clientcategory| :returns: The value that should be used as a price for this sellable. """ info = self.get_category_price_info(category) if info: return info.price return self.price def get_maximum_discount(self, category=None, user=None): user_discount = user.profile.max_discount if user else 0 if category is not None: info = self.get_category_price_info(category) or self else: info = self return Decimal(max(user_discount, info.max_discount)) def get_requires_kitchen_production(self, branch): """Check if a sellable requires kitchen production :param branch: branch for checking if there is a sellable_branch_override :returns: Whether the sellable requires kitchen production for a given branch """ return self._get_from_override('requires_kitchen_production', branch) def check_code_exists(self, code): """Check if there is another sellable with the same code. :returns: ``True`` if we already have a sellable with the given code ``False`` otherwise. """ return self.check_unique_value_exists(Sellable.code, code) def check_barcode_exists(self, barcode): """Check if there is another sellable with the same barcode. :returns: ``True`` if we already have a sellable with the given barcode ``False`` otherwise. """ return self.check_unique_value_exists(Sellable.barcode, barcode) def check_taxes_validity(self, branch): """Check if icms taxes are valid. This check is done because some icms taxes (such as CSOSN 101) have a 'valid until' field on it. If these taxes has expired, we cannot sell the sellable. Check this method using assert inside a try clause. :raises: :exc:`TaxError` if there are any issues with the sellable taxes. """ icms_template = self.product and self.product.get_icms_template(branch) SellableCheckTaxesEvent.emit(self, branch) if not icms_template: return elif not icms_template.p_cred_sn: return elif not icms_template.is_p_cred_sn_valid(): # Translators: ICMS tax rate credit = Alíquota de crédito do ICMS raise TaxError( _("You cannot sell this item before updating " "the 'ICMS tax rate credit' field on '%s' " "Tax Class.\n" "If you don't know what this means, contact " "the system administrator.") % icms_template.product_tax_template.name) def is_on_sale(self): """Check if the price is currently on sale. :return: ``True`` if it is on sale, ``False`` otherwise """ if not self.on_sale_price: return False return is_date_in_interval(localnow(), self.on_sale_start_date, self.on_sale_end_date) def is_valid_quantity(self, new_quantity): """Whether the new quantity is valid for this sellable or not. If the new quantity is fractioned, check on this sellable unit if it allows fractioned quantities. If not, this new quantity cannot be used. Note that, if the sellable lacks a unit, we will not allow fractions either. :returns: ``True`` if new quantity is Ok, ``False`` otherwise. """ if self.unit and not self.unit.allow_fraction: return not bool(new_quantity % 1) return True def is_valid_price(self, newprice, category=None, user=None, extra_discount=None): """Checks if *newprice* is valid for this sellable Returns a dict indicating whether the new price is a valid price as allowed by the discount by the user, by the category or by the sellable maximum discount :param newprice: The new price that we are trying to sell this sellable for :param category: Optionally define a |clientcategory| that we will get the price info from :param user: The user role may allow a different discount percentage. :param extra_discount: some extra discount for the sellable to be considered for the min_price :returns: A dict with the following keys: * is_valid: ``True`` if the price is valid, else ``False`` * min_price: The minimum price for this sellable. * max_discount: The maximum discount for this sellable. """ if category is not None: info = self.get_category_price_info(category) or self else: info = self max_discount = self.get_maximum_discount(category=category, user=user) min_price = info.price * (1 - max_discount / 100) if extra_discount is not None: # The extra discount can be greater than the min_price, and # a negative min_price doesn't make sense min_price = max(currency(0), min_price - extra_discount) return { 'is_valid': newprice >= min_price, 'min_price': min_price, 'max_discount': max_discount, } def copy_sellable(self, target=None): """This method copies self to another sellable If the |sellable| target is None, a new sellable is created. :param target: The |sellable| target for the copy returns: a |sellable| identical to self """ if target is None: target = Sellable(store=self.store) props = [ 'base_price', 'category_id', 'cost', 'max_discount', 'commission', 'notes', 'unit_id', 'tax_constant_id', 'default_sale_cfop_id', 'on_sale_price', 'on_sale_start_date', 'on_sale_end_date' ] for prop in props: value = getattr(self, prop) setattr(target, prop, value) return target # # IDescribable implementation # def get_description(self, full_description=False): desc = self.description if full_description and self.get_category_description(): desc = u"[%s] %s" % (self.get_category_description(), desc) return desc # # Domain hooks # def on_update(self): obj = self.product or self.service obj.on_update() def on_object_changed(self, attr, old_value, value): if attr == 'cost': self.cost_last_updated = localnow() if self.product: self.product.update_product_cost(value) elif attr == 'base_price': self.price_last_updated = localnow() # # Classmethods # def remove(self): """ Remove this sellable. This will also remove the |product| or |sellable| and |categoryprice| """ assert self.can_remove() # Remove category price before delete the sellable. category_prices = self.get_category_prices() for category_price in category_prices: category_price.remove() for image in self.images: self.store.remove(image) if self.product: self.product.remove() elif self.service: self.service.remove() self.store.remove(self) @classmethod def get_available_sellables_query(cls, store): """Get the sellables that are available and can be sold. For instance, this will filter out the internal sellable used by a |delivery|. This is similar to `.get_available_sellables`, but it returns a query instead of the actual results. :param store: a store :returns: a query expression """ delivery = sysparam.get_object(store, 'DELIVERY_SERVICE') return And(cls.id != delivery.sellable.id, cls.status == cls.STATUS_AVAILABLE) @classmethod def get_available_sellables(cls, store): """Get the sellables that are available and can be sold. For instance, this will filter out the internal sellable used by a |delivery|. :param store: a store :returns: a resultset with the available sellables """ query = cls.get_available_sellables_query(store) return store.find(cls, query) @classmethod def get_unblocked_sellables_query(cls, store, storable=False, supplier=None, consigned=False): """Helper method for get_unblocked_sellables When supplier is not ```None``, you should use this query only with Viewables that join with supplier, like ProductFullStockSupplierView. :param store: a store :param storable: if ``True``, we should filter only the sellables that are also a |storable|. :param supplier: |supplier| to filter on or ``None`` :param consigned: if the sellables are consigned :returns: a query expression """ from stoqlib.domain.product import Product, ProductSupplierInfo query = And(cls.get_available_sellables_query(store), cls.id == Product.id, Product.consignment == consigned) if storable: from stoqlib.domain.product import Storable query = And(query, Sellable.id == Product.id, Storable.id == Product.id) if supplier: query = And(query, Sellable.id == Product.id, Product.id == ProductSupplierInfo.product_id, ProductSupplierInfo.supplier_id == supplier.id) return query @classmethod def get_unblocked_sellables(cls, store, storable=False, supplier=None, consigned=False): """ Returns unblocked sellable objects, which means the available sellables plus the sold ones. :param store: a store :param storable: if `True`, only return sellables that also are |storable| :param supplier: a |supplier| or ``None``, if set limit the returned object to this |supplier| :rtype: queryset of sellables """ query = cls.get_unblocked_sellables_query(store, storable, supplier, consigned) return store.find(cls, query) @classmethod def get_unblocked_by_categories_query(cls, store, categories, include_uncategorized=True): """Returns the available sellables by a list of categories. :param store: a store :param categories: a list of SellableCategory instances :param include_uncategorized: whether or not include the sellables without a category :rtype: generator of sellables """ queries = [] if len(categories): queries.append(In(Sellable.category_id, [c.id for c in categories])) if include_uncategorized: queries.append(Eq(Sellable.category_id, None)) query = cls.get_unblocked_sellables_query(store) return And(query, Or(*queries))
class ReceivingOrder(IdentifiableDomain): """Receiving order definition. """ __storm_table__ = 'receiving_order' #: Products in the order was not received or received partially. STATUS_PENDING = u'pending' #: All products in the order has been received then the order is closed. STATUS_CLOSED = u'closed' #: A numeric identifier for this object. This value should be used instead of #: :obj:`Domain.id` when displaying a numerical representation of this object to #: the user, in dialogs, lists, reports and such. identifier = IdentifierCol() #: status of the order status = EnumCol(allow_none=False, default=STATUS_PENDING) #: Date that order has been closed. receival_date = DateTimeCol(default_factory=localnow) #: Date that order was send to Stock application. confirm_date = DateTimeCol(default=None) #: Some optional additional information related to this order. notes = UnicodeCol(default=u'') #: The invoice number of the order that has been received. invoice_number = IntCol() # Número do Romaneio. The number used by the transporter to identify the packing packing_number = UnicodeCol() cfop_id = IdCol() cfop = Reference(cfop_id, 'CfopData.id') responsible_id = IdCol() responsible = Reference(responsible_id, 'LoginUser.id') branch_id = IdCol() branch = Reference(branch_id, 'Branch.id') station_id = IdCol(allow_none=False) #: The station this object was created at station = Reference(station_id, 'BranchStation.id') receiving_invoice_id = IdCol(default=None) receiving_invoice = Reference(receiving_invoice_id, 'ReceivingInvoice.id') purchase_orders = ReferenceSet('ReceivingOrder.id', 'PurchaseReceivingMap.receiving_id', 'PurchaseReceivingMap.purchase_id', 'PurchaseOrder.id') def __init__(self, store=None, **kw): super(ReceivingOrder, self).__init__(store=store, **kw) # These miss default parameters and needs to be set before # cfop, which triggers an implicit flush. self.branch = kw.pop('branch', None) if not 'cfop' in kw: self.cfop = sysparam.get_object(store, 'DEFAULT_RECEIVING_CFOP') # # Public API # def confirm(self, user: LoginUser): if self.receiving_invoice: self.receiving_invoice.confirm(user) for item in self.get_items(): item.add_stock_items(user) purchases = list(self.purchase_orders) for purchase in purchases: if purchase.can_close(): purchase.close() # XXX: Will the packing number aways be the same as the suppliert order? if purchase.work_order: self.packing_number = purchase.work_order.supplier_order def add_purchase(self, order): return PurchaseReceivingMap(store=self.store, purchase=order, receiving=self) def add_purchase_item(self, item, quantity=None, batch_number=None, parent_item=None, ipi_value=0, icms_st_value=0): """Add a |purchaseitem| on this receiving order :param item: the |purchaseitem| :param decimal.Decimal quantity: the quantity of that item. If ``None``, it will be get from the item's pending quantity :param batch_number: a batch number that will be used to get or create a |batch| it will be get from the item's pending quantity or ``None`` if the item's |storable| is not controlling batches. :raises: :exc:`ValueError` when validating the quantity and testing the item's order for equality with :obj:`.order` """ pending_quantity = item.get_pending_quantity() if quantity is None: quantity = pending_quantity if not (0 < quantity <= item.quantity): raise ValueError("The quantity must be higher than 0 and lower " "than the purchase item's quantity") if quantity > pending_quantity: raise ValueError("The quantity must be lower than the item's " "pending quantity") sellable = item.sellable storable = sellable.product_storable if batch_number is not None: batch = StorableBatch.get_or_create(self.store, storable=storable, batch_number=batch_number) else: batch = None self.validate_batch(batch, sellable) return ReceivingOrderItem(store=self.store, sellable=item.sellable, batch=batch, quantity=quantity, cost=item.cost, ipi_value=ipi_value, icms_st_value=icms_st_value, purchase_item=item, receiving_order=self, parent_item=parent_item) def update_payments(self, create_freight_payment=False): """Updates the payment value of all payments realated to this receiving. If create_freight_payment is set, a new payment will be created with the freight value. The other value as the surcharges and discounts will be included in the installments. :param create_freight_payment: True if we should create a new payment with the freight value, False otherwise. """ # If the invoice has more than one receiving, the values could be inconsistent assert self.receiving_invoice.receiving_orders.count() == 1 difference = self.receiving_invoice.total - self.receiving_invoice.products_total if create_freight_payment: difference -= self.receiving_invoice.freight_total if difference != 0: # Get app pending payments for the purchases associated with this # receiving, and update them. payments = self.payments.find(status=Payment.STATUS_PENDING) payments_number = payments.count() if payments_number > 0: # XXX: There is a potential rounding error here. per_installments_value = difference / payments_number for payment in payments: new_value = payment.value + per_installments_value payment.update_value(new_value) if self.receiving_invoice.freight_total and create_freight_payment: purchases = list(self.purchase_orders) if len(purchases ) == 1 and self.receiving_invoice.transporter is None: group = purchases[0].group else: group = None self.receiving_invoice.create_freight_payment(group=group) def get_items(self, with_children=True): store = self.store query = ReceivingOrderItem.receiving_order == self if not with_children: query = And(query, Eq(ReceivingOrderItem.parent_item_id, None)) return store.find(ReceivingOrderItem, query) def remove_items(self): for item in self.get_items(): item.receiving_order = None def remove_item(self, item): assert item.receiving_order == self type(item).delete(item.id, store=self.store) def is_totally_returned(self): return all(item.is_totally_returned() for item in self.get_items()) # # Properties # @property def payments(self): if self.receiving_invoice and self.receiving_invoice.group: return self.receiving_invoice.payments tables = [PurchaseReceivingMap, PurchaseOrder, Payment] query = And(PurchaseReceivingMap.receiving_id == self.id, PurchaseReceivingMap.purchase_id == PurchaseOrder.id, Payment.group_id == PurchaseOrder.group_id) return self.store.using(tables).find(Payment, query) # # Accessors # @property def cfop_code(self): return self.cfop.code @property def freight_type(self): if self.receiving_invoice: return self.receiving_invoice.freight_type return None @property def branch_name(self): return self.branch.get_description() @property def responsible_name(self): return self.responsible.get_description() @property def products_total(self): total = sum((item.get_received_total() for item in self.get_items()), currency(0)) return currency(total) @property def product_total_with_ipi(self): total = sum((item.get_received_total(with_ipi=True) for item in self.get_items()), currency(0)) return currency(total) @property def receival_date_str(self): return self.receival_date.strftime("%x") @property def total_surcharges(self): """Returns the sum of all surcharges (purchase & receiving)""" total_surcharge = 0 for purchase in self.purchase_orders: total_surcharge += purchase.surcharge_value return currency(total_surcharge) @property def total_quantity(self): """Returns the sum of all received quantities""" return sum(item.quantity for item in self.get_items(with_children=False)) @property def total_discounts(self): """Returns the sum of all discounts (purchase & receiving)""" total_discount = 0 for purchase in self.purchase_orders: total_discount += purchase.discount_value return currency(total_discount) @property def total(self): """Fetch the total, including discount and surcharge for purchase order """ total = self.product_total_with_ipi total -= self.total_discounts total += self.total_surcharges return currency(total)
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, user: LoginUser, quantity, 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 serials and 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=user, 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, user) self.produced += quantity self.order.try_finalize_production(user) ProductHistory.add_produced_item(self.store, self.order.branch, self) def add_lost(self, user: LoginUser, 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(user, quantity * component.quantity) except ValueError: store.rollback_to_savepoint(u'before_lose') raise self.lost += quantity self.order.try_finalize_production(user) ProductHistory.add_lost_item(store, self.order.branch, self)
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() #: 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) 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"") @property def returned_quantity(self): return self.store.find(StockDecreaseItem, receiving_order_item=self).sum( StockDecreaseItem.quantity) or Decimal('0') @property def purchase_cost(self): return self.purchase_item.cost @property def description(self): return self.sellable.description @property def cost_with_ipi(self): return currency(quantize(self.cost + self.unit_ipi_value)) @property def unit_ipi_value(self): """The Ipi value must be shared through the items""" return currency(quantize(self.ipi_value / self.quantity)) # # Accessors # 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(quantize(self.quantity * cost)) def get_total_with_ipi(self): cost = self.purchase_item.cost ipi_value = self.ipi_value return currency(quantize(self.quantity * cost + ipi_value)) def get_received_total(self, with_ipi=False): ipi = self.ipi_value if with_ipi else 0 return currency(quantize((self.quantity * self.cost) + ipi)) 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() def add_stock_items(self, user: LoginUser): """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: cost = self.cost + (self.ipi_value / self.quantity) storable.increase_stock( self.quantity, branch, StockTransactionHistory.TYPE_RECEIVED_PURCHASE, self.id, user, cost, batch=self.batch) purchase.increase_quantity_received(self.purchase_item, self.quantity) ProductHistory.add_received_item(store, branch, self) def is_totally_returned(self): children = self.children_items if children.count(): return all(child.quantity == child.returned_quantity for child in children) return self.quantity == self.returned_quantity def get_receiving_packing_number(self): return self.receiving_order.packing_number
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, user: LoginUser, 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, user) def return_remaining(self, user: LoginUser): """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, user) self.allocated -= remaining def add_lost(self, user: LoginUser, 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(user, 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 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) def add_purchase_item(self, item, quantity=None, batch_number=None): """Add a |purchaseitem| on this receiving order :param item: the |purchaseitem| :param decimal.Decimal quantity: the quantity of that item. If ``None``, it will be get from the item's pending quantity :param batch_number: a batch number that will be used to get or create a |batch| it will be get from the item's pending quantity or ``None`` if the item's |storable| is not controlling batches. :raises: :exc:`ValueError` when validating the quantity and testing the item's order for equality with :obj:`.order` """ pending_quantity = item.get_pending_quantity() if quantity is None: quantity = pending_quantity if not (0 < quantity <= item.quantity): raise ValueError("The quantity must be higher than 0 and lower " "than the purchase item's quantity") if quantity > pending_quantity: raise ValueError("The quantity must be lower than the item's " "pending quantity") sellable = item.sellable storable = sellable.product_storable if batch_number is not None: batch = StorableBatch.get_or_create(self.store, storable=storable, batch_number=batch_number) else: batch = None self.validate_batch(batch, sellable) return ReceivingOrderItem(store=self.store, sellable=item.sellable, batch=batch, quantity=quantity, cost=item.cost, purchase_item=item, receiving_order=self) def update_payments(self, create_freight_payment=False): """Updates the payment value of all payments realated to this receiving. If create_freight_payment is set, a new payment will be created with the freight value. The other value as the surcharges and discounts will be included in the installments. :param create_freight_payment: True if we should create a new payment with the freight value, False otherwise. """ 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): store = self.store return store.find(ReceivingOrderItem, receiving_order=self) def remove_items(self): for item in self.get_items(): item.receiving_order = None def remove_item(self, item): assert item.receiving_order == self type(item).delete(item.id, store=self.store) # # Properties # @property def 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) 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)
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 - Decimal(discount) / 100))