class UserDecimal(UserMember): instantiable = True groups_order = UserMember.groups_order member_class = schema.Decimal member_min = schema.Decimal(listed_by_default=False, member_group="constraints") member_max = schema.Decimal(min=member_min, listed_by_default=False, member_group="constraints")
class PercentageDiscount(Discount): instantiable = True percentage = schema.Decimal( required = True ) def apply(self, item, costs): costs["price"]["percentage"] -= self.percentage
class PriceOverride(Discount): instantiable = True price = schema.Decimal( required = True ) def apply(self, item, costs): costs["price"]["cost"] = self.price
class PercentageTax(Tax): instantiable = True percentage = schema.Decimal( required = True ) def apply(self, item, costs): costs["tax"]["percentage"] += self.percentage
class CumulativeTax(Tax): instantiable = True cost = schema.Decimal( required = True ) def apply(self, item, costs): costs["tax"]["cost"] += self.cost
class CumulativeShippingCost(ShippingCost): instantiable = True cost = schema.Decimal( required = True ) def apply(self, item, costs): costs["shipping"] += self.cost
class ShippingCostOverride(ShippingCost): instantiable = True cost = schema.Decimal( required = True ) def apply(self, item, costs): costs["shipping"] = self.cost
class RelativeDiscount(Discount): instantiable = True amount = schema.Decimal( required = True ) def apply(self, item, costs): costs["price"]["cost"] -= self.amount
def schema(self): return schema.Schema( "ShopOrderCostFilter", members=[ schema.String( "operator", required=True, default="eq", enumeration=self.operators, text_search=False, translate_value=lambda value, language=None, **kwargs: "" if not value else translations( "cocktail.html.UserFilterEntry operator " + value, language, **kwargs)), schema.Boolean("include_shipping", default=False), schema.Boolean("include_taxes", default=False), schema.Boolean("include_discounts", default=False), schema.Decimal("value", required=True) ])
class Product(Publishable): instantiable = False view_class = None members_order = [ "price", "categories", "entries" ] default_controller = schema.DynamicDefault( lambda: Controller.get_instance(qname = "woost.product_controller") ) price = schema.Decimal( required = True, default = Decimal("0") ) categories = schema.Collection( items = "woost.extensions.shop.productcategory.ProductCategory", bidirectional = True ) entries = schema.Collection( items = "woost.extensions.shop.shoporderentry.ShopOrderEntry", bidirectional = True, visible = False, block_delete = True ) def discounts(self): """Returns the discounts that can be applied to the product. @rtype: L{Product<woost.extensions.shop.product.Product>} """ from woost.extensions.shop import ShopExtension return [discount for discount in ShopExtension.instance.discounts if discount.applies_to(self)]
class ShopOrderEntry(Item): listed_from_root = False members_order = [ "shop_order", "product", "quantity" ] shop_order = schema.Reference( type = "woost.extensions.shop.shoporder.ShopOrder", bidirectional = True, required = True ) product = schema.Reference( type = "woost.extensions.shop.product.Product", bidirectional = True, required = True ) quantity = schema.Integer( required = True, min = 1 ) cost = schema.Decimal( required = True, default = Decimal("0"), editable = False ) def __translate__(self, language, **kwargs): return "%s (%d)" % ( translations(self.product, language), self.quantity )
class ECommerceProduct(Publishable): instantiable = False members_order = [ "description", "price", "weight", "attachments", "purchase_model", "purchases", "template" ] default_controller = schema.DynamicDefault( lambda: Controller.get_instance( qname = "woost.extensions.ecommerce.product_controller" ) ) description = schema.String( translated = True, edit_control = "woost.views.RichTextEditor", member_group = "product_data" ) price = schema.Decimal( required = True, default = Decimal("0"), member_group = "product_data" ) weight = schema.Decimal( translate_value = lambda value, language = None, **kwargs: "" if not value else "%s Kg" % translations(value, language), member_group = "product_data" ) attachments = schema.Collection( items = schema.Reference(type = File), related_end = schema.Collection(), member_group = "product_data" ) purchase_model = schema.Reference( class_family = "woost.extensions.ecommerce.ecommercepurchase." "ECommercePurchase", default = schema.DynamicDefault( lambda: ECommerceProduct.purchase_model.class_family ), required = True, searchable = False, member_group = "product_data" ) purchases = schema.Collection( items = "woost.extensions.ecommerce.ecommercepurchase." "ECommercePurchase", bidirectional = True, visible = False, member_group = "product_data" ) template = schema.Reference( type = Template, related_end = schema.Collection(), default = schema.DynamicDefault( lambda: Template.get_instance( qname = "woost.extensions.ecommerce.product_template" ) ), member_group = "presentation" ) def get_image(self): for attachment in self.attachments: if attachment.resource_type == "image" \ and attachment.is_accessible(): return attachment def offers(self): from woost.extensions.ecommerce import ECommerceExtension for pricing in ECommerceExtension.instance.pricing: if not pricing.hidden and pricing.applies_to(self): yield pricing @getter def inherited_resources(self): if self.inherit_resources and self.parent is None: catalog = Document.get_instance( qname = "woost.extensions.ecommerce.catalog_page" ) if catalog: for resource in catalog.inherited_resources: yield resource for resource in catalog.branch_resources: yield resource else: for resource in Publishable.inherited_resources.__get__(self): yield resource
required=True, default=True, indexed=True, member_group="sitemap", listed_by_default=False), append=True) URI.default_sitemap_indexable = False Publishable.add_member(schema.String( "sitemap_change_frequency", enumeration=[ "always", "hourly", "daily", "weekly", "monthly", "yearly", "never" ], translate_value=lambda value, language=None, **kwargs: "" if not value else translations( "woost.extensions.sitemap.change_frequency " + value, language, ** kwargs), member_group="sitemap", text_search=False, listed_by_default=False), append=True) Publishable.add_member(schema.Decimal("sitemap_priority", default=Decimal("0.5"), min=0, max=1, listed_by_default=False, member_group="sitemap"), append=True)
class ECommerceOrder(Item): payment_types_completed_status = { "payment_gateway": "accepted", "transfer": "payment_pending", "cash_on_delivery": "payment_pending" } incoming = Event(doc=""" An event triggered when a new order is received. """) completed = Event(doc=""" An event triggered when an order is completed. """) groups_order = ["shipping_info", "billing"] members_order = [ "customer", "address", "town", "region", "country", "postal_code", "language", "status", "purchases", "payment_type", "total_price", "pricing", "total_shipping_costs", "shipping_costs", "total_taxes", "taxes", "total" ] customer = schema.Reference( type=User, related_end=schema.Collection(), required=True, default=schema.DynamicDefault(get_current_user)) address = schema.String(member_group="shipping_info", required=True, listed_by_default=False) town = schema.String(member_group="shipping_info", required=True, listed_by_default=False) region = schema.String(member_group="shipping_info", required=True, listed_by_default=False) country = schema.Reference( member_group="shipping_info", type=Location, relation_constraints=[Location.location_type.equal("country")], default_order="location_name", related_end=schema.Collection(), required=True, listed_by_default=False, user_filter="cocktail.controllers.userfilter.MultipleChoiceFilter") postal_code = schema.String(member_group="shipping_info", required=True, listed_by_default=False) language = schema.String( required=True, format="^[a-z]{2}$", editable=False, default=schema.DynamicDefault(get_language), text_search=False, translate_value=lambda value, language=None, **kwargs: u"" if not value else translations(value, language, **kwargs)) status = schema.String( required=True, indexed=True, enumeration=[ "shopping", "payment_pending", "accepted", "failed", "refund" ], default="shopping", text_search=False, translate_value=lambda value, language=None, **kwargs: u"" if not value else translations("ECommerceOrder.status-" + value, language, **kwargs)) purchases = schema.Collection( items="woost.extensions.ecommerce.ecommercepurchase." "ECommercePurchase", integral=True, bidirectional=True, min=1) payment_type = schema.String( member_group="billing", required=True, translate_value=lambda value, language=None, **kwargs: translations( "ECommerceOrder.payment_type-%s" % value, language=language), default=schema.DynamicDefault(_get_default_payment_type), text_search=False, edit_control="cocktail.html.RadioSelector", listed_by_default=False) total_price = schema.Decimal(member_group="billing", editable=False, listed_by_default=False, translate_value=_translate_amount) pricing = schema.Collection( member_group="billing", items=schema.Reference(type=ECommerceBillingConcept), related_end=schema.Collection(block_delete=True), editable=False) total_shipping_costs = schema.Decimal(member_group="billing", editable=False, listed_by_default=False, translate_value=_translate_amount) shipping_costs = schema.Collection( member_group="billing", items=schema.Reference(type=ECommerceBillingConcept), related_end=schema.Collection(block_delete=True), editable=False) total_taxes = schema.Decimal(member_group="billing", editable=False, listed_by_default=False, translate_value=_translate_amount) taxes = schema.Collection( member_group="billing", items=schema.Reference(type=ECommerceBillingConcept), related_end=schema.Collection(block_delete=True), editable=False) total = schema.Decimal(member_group="billing", editable=False, translate_value=_translate_amount) def calculate_cost(self, apply_pricing=True, apply_shipping_costs=True, apply_taxes=True): """Calculates the costs for the order. :rtype: dict """ from woost.extensions.ecommerce import ECommerceExtension extension = ECommerceExtension.instance order_costs = { "price": { "cost": Decimal("0.00"), "percentage": Decimal("0.00"), "concepts": [] }, "shipping_costs": { "cost": Decimal("0.00"), "percentage": Decimal("0.00"), "concepts": [] }, "taxes": { "cost": Decimal("0.00"), "percentage": Decimal("0.00"), "concepts": [] }, "purchases": {} } # Per purchase costs: for purchase in self.purchases: purchase_costs = purchase.calculate_costs( apply_pricing=apply_pricing, apply_shipping_costs=apply_shipping_costs, apply_taxes=apply_taxes) order_costs["purchases"][purchase] = purchase_costs order_costs["price"]["cost"] += purchase_costs["price"]["total"] order_costs["shipping_costs"]["cost"] += \ purchase_costs["shipping_costs"]["total"] order_costs["taxes"]["cost"] += purchase_costs["taxes"]["total"] # Order price order_price = order_costs["price"] if apply_pricing: for pricing in extension.pricing: if pricing.applies_to(self): pricing.apply(self, order_price) order_price["cost"] += \ order_price["cost"] * order_price["percentage"] / 100 order_price["total"] = order_price["cost"] # Order shipping costs order_shipping_costs = order_costs["shipping_costs"] if apply_shipping_costs: for shipping_cost in extension.shipping_costs: if shipping_cost.applies_to(self): shipping_cost.apply(self, order_shipping_costs) order_shipping_costs["total"] = ( order_shipping_costs["cost"] + order_price["total"] * order_shipping_costs["percentage"] / 100) # Order taxes order_taxes = order_costs["taxes"] if apply_taxes: for tax in extension.taxes: if tax.applies_to(self): tax.apply(self, order_taxes) order_taxes["total"] = ( order_taxes["cost"] + order_price["total"] * order_taxes["percentage"] / 100) # Total order_costs["total"] = (order_price["total"] + order_shipping_costs["total"] + order_taxes["total"]) return order_costs def update_cost(self, apply_pricing=True, apply_shipping_costs=True, apply_taxes=True): costs = self.calculate_cost(apply_pricing=apply_pricing, apply_shipping_costs=apply_shipping_costs, apply_taxes=apply_taxes) self.total_price = costs["price"]["total"] self.pricing = list(costs["price"]["concepts"]) self.total_shipping_costs = costs["shipping_costs"]["total"] self.shipping_costs = list(costs["shipping_costs"]["concepts"]) self.total_taxes = costs["taxes"]["total"] self.taxes = list(costs["taxes"]["concepts"]) self.total = costs["total"] for purchase, purchase_costs in costs["purchases"].iteritems(): purchase.total_price = purchase_costs["price"]["total"] purchase.pricing = list(purchase_costs["price"]["concepts"]) self.pricing.extend(purchase.pricing) purchase.total_shipping_costs = \ purchase_costs["shipping_costs"]["total"] purchase.shipping_costs = \ list(purchase_costs["shipping_costs"]["concepts"]) self.shipping_costs.extend(purchase.shipping_costs) purchase.total_taxes = purchase_costs["taxes"]["total"] purchase.taxes = list(purchase_costs["taxes"]["concepts"]) self.taxes.extend(purchase.taxes) purchase.total = purchase_costs["total"] def count_units(self): return sum(purchase.quantity for purchase in self.purchases) def get_weight(self): return sum(purchase.get_weight() for purchase in self.purchases) def add_purchase(self, purchase): for order_purchase in self.purchases: if order_purchase.__class__ is purchase.__class__ \ and order_purchase.product is purchase.product \ and all( order_purchase.get(option) == purchase.get(option) for option in purchase.get_options() if option.name != "quantity" ): order_purchase.quantity += purchase.quantity purchase.product = None if purchase.is_inserted: purchase.delete() break else: self.purchases.append(purchase) @classmethod def get_public_schema(cls): public_schema = schema.Schema("OrderCheckoutSummary") cls.get_public_adapter().export_schema(cls, public_schema) payment_type = public_schema.get_member("payment_type") if payment_type: payments = PaymentsExtension.instance if payments.enabled and payments.payment_gateway: translate_value = payment_type.translate_value def payment_type_translate_value(value, language=None, **kwargs): if value == "payment_gateway": return payments.payment_gateway.label else: return translate_value(value, language=language, **kwargs) payment_type.translate_value = payment_type_translate_value return public_schema @classmethod def get_public_adapter(cls): from woost.extensions.ecommerce import ECommerceExtension user = get_current_user() adapter = schema.Adapter() adapter.exclude(["customer", "status", "purchases"]) adapter.exclude([ member.name for member in cls.members().itervalues() if not member.visible or not member.editable or not issubclass(member.schema, ECommerceOrder) or not user.has_permission(ModifyMemberPermission, member=member) ]) if len(ECommerceExtension.instance.payment_types) == 1: adapter.exclude(["payment_type"]) return adapter @property def is_completed(self): return self.status \ and self.status == self.payment_types_completed_status.get( self.payment_type ) @event_handler def handle_changed(cls, event): item = event.source member = event.member if member.name == "status": if event.previous_value == "shopping" \ and event.value in ("payment_pending", "accepted"): item.incoming() if item.is_completed: item.completed() def get_description_for_gateway(self): site_name = Configuration.instance.get_setting("site_name") if site_name: return translations( "woost.extensions.ECommerceOrder description for gateway" ) % site_name else: return translations(self)
@author: Martí Congost @contact: [email protected] @organization: Whads/Accent SL @since: February 2010 """ from decimal import Decimal from cocktail.translations import translations from cocktail import schema from woost.models import Publishable, URI translations.load_bundle("woost.extensions.sitemap.publishable") URI.default_sitemap_indexable = False Publishable.add_member(schema.String("x_sitemap_change_frequency", enumeration=[ "always", "hourly", "daily", "weekly", "monthly", "yearly", "never" ], member_group="meta.robots", text_search=False, listed_by_default=False), append=True) Publishable.add_member(schema.Decimal("x_sitemap_priority", min=0, max=1, listed_by_default=False, member_group="meta.robots"), append=True)
class ShopOrder(Item): members_order = [ "address", "town", "region", "country", "postal_code", "cost", "entries" ] address = schema.String(member_group="shipping_info", required=True, listed_by_default=False) town = schema.String(member_group="shipping_info", required=True, listed_by_default=False) region = schema.String(member_group="shipping_info", required=True, listed_by_default=False) country = schema.Reference( member_group="shipping_info", type=Country, related_end=schema.Collection(), required=True, listed_by_default=False, user_filter="cocktail.controllers.userfilter.MultipleChoiceFilter") postal_code = schema.String(member_group="shipping_info", required=True, listed_by_default=False) cost = schema.Decimal(required=True, default=Decimal("0"), editable=False) language = schema.String( required=True, format="^[a-z]{2}$", editable=False, default=schema.DynamicDefault(get_language), text_search=False, translate_value=lambda value, language=None, **kwargs: u"" if not value else translations(value, language, **kwargs)) status = schema.String( required=True, indexed=True, enumeration=["pending", "accepted", "failed"], default="pending", text_search=False, translate_value=lambda value, language=None, **kwargs: u"" if not value else translations( "woost.extensions.shop.ShopOrder.status " + value, language, ** kwargs)) entries = schema.Collection( items="woost.extensions.shop.shoporderentry.ShopOrderEntry", integral=True, bidirectional=True, min=1) def calculate_cost(self, include_shipping=True, include_taxes=True, include_discounts=True): """Calculates the costs for the order. @rtype: dict """ costs = { "pricing_policies": [], "price": { "cost": 0, "percentage": 0, "total": None }, "shipping": 0, "tax": { "cost": 0, "percentage": 0 }, "entries": [{ "pricing_policies": [], "quantity": entry.quantity, "paid_quantity": entry.quantity, "price": { "cost": entry.product.price, "percentage": 0 }, "shipping": 0, "tax": { "cost": 0, "percentage": 0 } } for entry in self.entries] } from woost.extensions.shop import ShopExtension shop_ext = ShopExtension.instance policies = list() if include_discounts: policies.extend(shop_ext.discounts) if include_shipping: policies.extend(shop_ext.shipping_costs) if include_taxes: policies.extend(shop_ext.taxes) for pricing_policy in policies: matching_items = pricing_policy.select_matching_items() if issubclass(matching_items.type, ShopOrder): if pricing_policy.applies_to(self): pricing_policy.apply(self, costs) costs["pricing_policies"].append(pricing_policy) else: for entry, entry_costs in zip(self.entries, costs["entries"]): if pricing_policy.applies_to(entry.product): pricing_policy.apply(entry.product, entry_costs) entry_costs["pricing_policies"].append(pricing_policy) # Total price def apply_percentage(costs): cost = costs["cost"] percentage = costs["percentage"] if percentage: cost += cost * percentage / 100 costs["total"] = cost return cost total_price = apply_percentage(costs["price"]) for entry_costs in costs["entries"]: entry_price = apply_percentage(entry_costs["price"]) entry_total_price = entry_price * entry_costs["paid_quantity"] entry_costs["total_price"] = entry_total_price total_price += entry_total_price costs["total_price"] = total_price # Total taxes total_taxes = costs["tax"]["cost"] \ + total_price * costs["tax"]["percentage"] / 100 for entry_costs in costs["entries"]: quantity = entry_costs["paid_quantity"] entry_price = entry_costs["price"]["total"] * quantity entry_taxes = entry_costs["tax"]["cost"] * quantity \ + entry_price * entry_costs["tax"]["percentage"] / 100 total_taxes += entry_taxes entry_costs["tax"]["total"] = entry_taxes costs["total_taxes"] = total_taxes # Total shipping costs total_shipping_costs = costs["shipping"] \ + sum(entry_costs["shipping"] * entry_costs["quantity"] for entry_costs in costs["entries"]) costs["total_shipping_costs"] = total_shipping_costs # Grand total costs["total"] = total_price + total_taxes + total_shipping_costs return costs def count_items(self): """Gets the number of purchased product units in the order. @rtype: int """ return sum(entry.quantity for entry in self.entries) def get_product_entry(self, product): """Gets the entry in the order for the given product. @param product: The product to obtain the entry for. @type product: L{Product<woost.extensions.shop.product.Product>} @return: The matching entry, or None if the order doesn't contain an entry for the indicated product. @rtype: L{ShopOrderEntry <woost.extensions.shop.shoporderentry.ShopOrderEntry>} """ for entry in self.entries: if entry.product is product: return entry def set_product_quantity(self, product, quantity): """Updates the quantity of ordered units for the indicated product. If an entry for the given product already exists, its quantity will be updated to the indicated value. If the indicated quantity is zero, the entry will be removed from the order. If no matching entry exists, a new entry for the product will be created with the specified amount of units. @param product: The product to set the quantity for. @type product: L{Product<woost.extensions.shop.product.Product>} @param quantity: The number of units of the product to order. @type quantity: int """ entry = self.get_product_entry(product) if entry is None: if quantity: entry = ShopOrderEntry(product=product, quantity=quantity) self.entries.append(entry) if self.is_inserted: entry.insert() else: if quantity: entry.quantity = quantity else: if entry.is_inserted: entry.delete() else: self.entries.remove(entry)
class ECommercePurchase(Item): listed_from_root = False members_order = [ "order", "product", "quantity", "total_price", "pricing", "total_shipping_costs", "shipping_costs", "total_taxes", "taxes", "total" ] order = schema.Reference( type="woost.extensions.ecommerce.ecommerceorder.ECommerceOrder", bidirectional=True, required=True) product = schema.Reference( type="woost.extensions.ecommerce.ecommerceproduct.ECommerceProduct", bidirectional=True, required=True) quantity = schema.Integer(required=True, min=1, default=1) total_price = schema.Decimal(member_group="billing", editable=False, listed_by_default=False) pricing = schema.Collection( member_group="billing", items=schema.Reference(type=ECommerceBillingConcept), related_end=schema.Collection(block_delete=True), editable=False) total_shipping_costs = schema.Decimal(member_group="billing", editable=False, listed_by_default=False) shipping_costs = schema.Collection( member_group="billing", items=schema.Reference(type=ECommerceBillingConcept), related_end=schema.Collection(block_delete=True), editable=False) total_taxes = schema.Decimal(member_group="billing", editable=False, listed_by_default=False) taxes = schema.Collection( member_group="billing", items=schema.Reference(type=ECommerceBillingConcept), related_end=schema.Collection(block_delete=True), editable=False) total = schema.Decimal(member_group="billing", editable=False) def __translate__(self, language, **kwargs): desc = u"%d x %s" % (self.quantity, translations( self.product, language)) options = [] for member in self.get_options(): if member is ECommercePurchase.quantity: continue options.append( "%s: %s" % (translations(member, language), member.translate_value(self.get(member), language))) if options: desc += u" (%s)" % u", ".join(options) return desc def calculate_costs(self, apply_pricing=True, apply_shipping_costs=True, apply_taxes=True): from woost.extensions.ecommerce import ECommerceExtension extension = ECommerceExtension.instance purchase_costs = { "price": { "cost": self.get_unit_price(), "paid_units": self.quantity, "percentage": Decimal("0.00"), "concepts": [] }, "shipping_costs": { "cost": Decimal("0.00"), "paid_units": self.quantity, "percentage": Decimal("0.00"), "concepts": [] }, "taxes": { "cost": Decimal("0.00"), "paid_units": self.quantity, "percentage": Decimal("0.00"), "concepts": [] } } # Price purchase_price = purchase_costs["price"] if apply_pricing: for pricing in extension.pricing: if pricing.applies_to(self, purchase_costs): pricing.apply(self, purchase_price) purchase_price["cost"] += \ purchase_price["cost"] * purchase_price["percentage"] / 100 purchase_price["total"] = \ purchase_price["cost"] * purchase_price["paid_units"] # Shipping costs purchase_shipping_costs = purchase_costs["shipping_costs"] if apply_shipping_costs: for shipping_cost in extension.shipping_costs: if shipping_cost.applies_to(self, purchase_costs): shipping_cost.apply(self, purchase_shipping_costs) purchase_shipping_costs["cost"] += \ purchase_price["cost"] * purchase_shipping_costs["percentage"] / 100 purchase_shipping_costs["total"] = \ purchase_shipping_costs["cost"] * purchase_shipping_costs["paid_units"] # Taxes purchase_taxes = purchase_costs["taxes"] if apply_taxes: for tax in extension.taxes: if tax.applies_to(self, purchase_costs): tax.apply(self, purchase_taxes) purchase_taxes["cost"] += \ purchase_price["cost"] * purchase_taxes["percentage"] / 100 purchase_taxes["total"] = \ purchase_taxes["cost"] * purchase_taxes["paid_units"] # Total purchase_costs["total"] = (purchase_price["total"] + purchase_shipping_costs["total"] + purchase_taxes["total"]) return purchase_costs def get_unit_price(self): return self.product.price def get_weight(self): if self.product is None or self.product.weight is None: return 0 else: return self.quantity * self.product.weight @classmethod def get_options(cls): for member in cls.members().itervalues(): if (member is not cls.product and member is not cls.order and member.visible and member.editable and issubclass(member.schema, ECommercePurchase) and get_current_user().has_permission( ModifyMemberPermission, member=member)): yield member