Skip to content

Commit

Permalink
Merge pull request #552 from gurch101/add-cash-payments
Browse files Browse the repository at this point in the history
Add cash payment method (SHUUP-2723)
  • Loading branch information
suutari-ai committed Jun 22, 2016
2 parents 411fc2a + fe141d5 commit 9333228
Show file tree
Hide file tree
Showing 12 changed files with 291 additions and 11 deletions.
2 changes: 2 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Unreleased
Core
~~~~

- Add cash payment method
- Add rounding behavior component
- Add contact group availability behavior component
- Fix bug: Do not display decimal values in scientific notation
- Fix bug: Taxes of child order lines are filled incorrectly
Expand Down
5 changes: 5 additions & 0 deletions shoop/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class ShoopAdminAppConfig(AppConfig):
"shoop.admin.modules.services.forms:WaivingCostBehaviorComponentForm",
"shoop.admin.modules.services.forms:WeightLimitsBehaviorComponentForm",
"shoop.admin.modules.services.forms:GroupAvailabilityBehaviorComponentForm",
"shoop.admin.modules.services.forms:RoundingBehaviorComponentForm",
],
"service_behavior_component_form_part": [
"shoop.admin.modules.services.weight_based_pricing.WeightBasedPricingFormPart"
Expand All @@ -58,6 +59,10 @@ class ShoopAdminAppConfig(AppConfig):
}

def ready(self):
from shoop.core.order_creator.signals import order_creator_finished
from shoop.admin.modules.orders.receivers import handle_custom_payment_return_requests

order_creator_finished.connect(handle_custom_payment_return_requests, dispatch_uid='shoop.admin.order_create')
validate_templates_configuration()


Expand Down
30 changes: 30 additions & 0 deletions shoop/admin/modules/orders/receivers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# This file is part of Shoop.
#
# Copyright (c) 2012-2016, Shoop Ltd. All rights reserved.
#
# This source code is licensed under the AGPLv3 license found in the
# LICENSE file in the root directory of this source tree.
from django.utils.timezone import now

from shoop.core.models import CustomPaymentProcessor


# TEMPORARY until we get admin orders also calling service methods


def _create_cash_payment_for_order(order):
if not order.is_paid():
order.create_payment(
order.taxful_total_price,
payment_identifier="Cash-%s" % now().isoformat(),
description="Cash Payment"
)


def handle_custom_payment_return_requests(sender, order, *args, **kwargs):
payment_processor = order.payment_method.payment_processor if order.payment_method else None
if isinstance(payment_processor, CustomPaymentProcessor):
service = order.payment_method.choice_identifier
if service == "cash":
_create_cash_payment_for_order(order)
9 changes: 7 additions & 2 deletions shoop/admin/modules/services/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from shoop.admin.forms import ShoopAdminForm
from shoop.core.models import (
FixedCostBehaviorComponent, GroupAvailabilityBehaviorComponent,
PaymentMethod, ServiceProvider, ShippingMethod,
PaymentMethod, RoundingBehaviorComponent, ServiceProvider, ShippingMethod,
WaivingCostBehaviorComponent, WeightLimitsBehaviorComponent
)

Expand Down Expand Up @@ -66,7 +66,6 @@ def _save_master(self, commit=True):
service_data = self._get_cleaned_data_without_translations()
provider = service_data.pop(self.service_provider_attr)
choice_identifier = service_data.pop("choice_identifier")

return provider.create_service(choice_identifier, **service_data)


Expand Down Expand Up @@ -128,3 +127,9 @@ class GroupAvailabilityBehaviorComponentForm(forms.ModelForm):
class Meta:
model = GroupAvailabilityBehaviorComponent
exclude = ["identifier"]


class RoundingBehaviorComponentForm(forms.ModelForm):
class Meta:
model = RoundingBehaviorComponent
exclude = ["identifier"]
37 changes: 35 additions & 2 deletions shoop/core/locale/en/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-06-17 19:37+0000\n"
"POT-Creation-Date: 2016-06-21 17:57+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: en <LL@li.org>\n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: en\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"Generated-By: Babel 2.1.1\n"

Expand Down Expand Up @@ -1037,6 +1037,36 @@ msgstr ""
msgid "Service is not available for any of the customers groups."
msgstr ""

msgid "round to nearest with ties going away from zero"
msgstr ""

msgid "round to nearest with ties going towards zero"
msgstr ""

msgid "round away from zero"
msgstr ""

msgid "round towards zero"
msgstr ""

msgid "Rounding"
msgstr ""

msgid "Round total order price to the nearest quant."
msgstr ""

msgid "rounding quant"
msgstr ""

msgid "rounding mode"
msgstr ""

msgid "rounding"
msgstr ""

msgid "Only administrators can process cash payments"
msgstr ""

msgid "payment processor"
msgstr ""

Expand All @@ -1049,6 +1079,9 @@ msgstr ""
msgid "Manually processed payment"
msgstr ""

msgid "Cash payment"
msgstr ""

msgid "carrier"
msgstr ""

Expand Down
29 changes: 29 additions & 0 deletions shoop/core/migrations/0028_roundingbehaviorcomponent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models
from decimal import Decimal
import shoop.core.models
import enumfields.fields


class Migration(migrations.Migration):

dependencies = [
('shoop', '0027_contact_group_behavior'),
]

operations = [
migrations.CreateModel(
name='RoundingBehaviorComponent',
fields=[
('servicebehaviorcomponent_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='shoop.ServiceBehaviorComponent')),
('quant', models.DecimalField(default=Decimal('0.05'), verbose_name='rounding quant', max_digits=36, decimal_places=9)),
('mode', enumfields.fields.EnumField(default=b'ROUND_HALF_UP', max_length=10, verbose_name='rounding mode', enum=shoop.core.models.RoundingMode)),
],
options={
'abstract': False,
},
bases=('shoop.servicebehaviorcomponent',),
),
]
7 changes: 5 additions & 2 deletions shoop/core/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@
)
from ._service_behavior import (
FixedCostBehaviorComponent, GroupAvailabilityBehaviorComponent,
WaivingCostBehaviorComponent, WeightBasedPriceRange,
WeightBasedPricingBehaviorComponent, WeightLimitsBehaviorComponent
RoundingBehaviorComponent, RoundingMode, WaivingCostBehaviorComponent,
WeightBasedPriceRange, WeightBasedPricingBehaviorComponent,
WeightLimitsBehaviorComponent
)
from ._service_payment import (
CustomPaymentProcessor, PaymentMethod, PaymentProcessor, PaymentUrls
Expand Down Expand Up @@ -118,6 +119,8 @@
"ProductVariationVariable",
"ProductVariationVariableValue",
"ProductVisibility",
"RoundingMode",
"RoundingBehaviorComponent",
"SalesUnit",
"SavedAddress",
"SavedAddressRole",
Expand Down
40 changes: 40 additions & 0 deletions shoop/core/models/_service_behavior.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@

from __future__ import unicode_literals

import decimal

from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import ugettext_lazy as _
from enumfields import Enum, EnumField
from parler.models import TranslatableModel, TranslatedField, TranslatedFields

from shoop.core.fields import MeasurementField, MoneyValueField
from shoop.utils.numbers import nickel_round

from ._service_base import (
ServiceBehaviorComponent, ServiceCost,
Expand Down Expand Up @@ -168,3 +172,39 @@ def get_unavailability_reasons(self, service, source):
groups_to_match = set(self.groups.all().values_list("pk", flat=True))
if not bool(customer_groups & groups_to_match):
yield ValidationError(_("Service is not available for any of the customers groups."))


class RoundingMode(Enum):
ROUND_HALF_UP = decimal.ROUND_HALF_UP
ROUND_HALF_DOWN = decimal.ROUND_HALF_DOWN
ROUND_UP = decimal.ROUND_UP
ROUND_DOWN = decimal.ROUND_DOWN

class Labels:
ROUND_HALF_UP = _("round to nearest with ties going away from zero")
ROUND_HALF_DOWN = _("round to nearest with ties going towards zero")
ROUND_UP = _("round away from zero")
ROUND_DOWN = _("round towards zero")


class RoundingBehaviorComponent(ServiceBehaviorComponent):
name = _("Rounding")
help_text = _("Round total order price to the nearest quant.")

quant = models.DecimalField(
max_digits=36, decimal_places=9, default=decimal.Decimal('0.05'),
verbose_name=_("rounding quant"))
mode = EnumField(
RoundingMode,
default=RoundingMode.ROUND_HALF_UP,
verbose_name=_("rounding mode"))

def get_costs(self, service, source):
total_price = source.total_price_of_products
rounded = nickel_round(total_price, self.quant, self.mode.value)
remainder = rounded - total_price
yield ServiceCost(remainder, _("rounding"))

def get_unavailability_reasons(self, service, source):
if not source.creator or not(source.creator.is_superuser or source.creator.is_staff):
yield ValidationError(_("Only administrators can process cash payments"))
16 changes: 15 additions & 1 deletion shoop/core/models/_service_payment.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from django.db import models
from django.http.response import HttpResponseRedirect
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from parler.models import TranslatedFields

Expand Down Expand Up @@ -129,4 +130,17 @@ class Meta:
verbose_name_plural = _("custom payment processors")

def get_service_choices(self):
return [ServiceChoice('manual', _("Manually processed payment"))]
return [
ServiceChoice('manual', _("Manually processed payment")),
ServiceChoice('cash', _("Cash payment"))
]

def process_payment_return_request(self, service, order, request):
if service == 'cash':
if not order.is_paid():
order.create_payment(
order.taxful_total_price,
payment_identifier="Cash-%s" % now().isoformat(),
description="Cash Payment"
)
super(CustomPaymentProcessor, self).process_payment_return_request(service, order, request)
15 changes: 11 additions & 4 deletions shoop_tests/admin/test_service_behavior_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,22 @@
# This source code is licensed under the AGPLv3 license found in the
# LICENSE file in the root directory of this source tree.

import decimal

import pytest
from django.test import override_settings

from shoop.admin.modules.services.views import (
PaymentMethodEditView, ShippingMethodEditView
)
from shoop.core.models import (
FixedCostBehaviorComponent, PaymentMethod, ShippingMethod,
WaivingCostBehaviorComponent, GroupAvailabilityBehaviorComponent,
WeightLimitsBehaviorComponent
FixedCostBehaviorComponent, GroupAvailabilityBehaviorComponent,
PaymentMethod, RoundingBehaviorComponent, RoundingMode, ShippingMethod,
WaivingCostBehaviorComponent, WeightLimitsBehaviorComponent
)
from shoop.testing.factories import (
get_default_payment_method, get_default_shipping_method, get_default_shop, get_default_customer_group
get_default_customer_group, get_default_payment_method,
get_default_shipping_method, get_default_shop
)
from shoop.testing.utils import apply_request_middleware

Expand Down Expand Up @@ -50,6 +53,10 @@ def get_default_behavior_settings():
},
GroupAvailabilityBehaviorComponent.__name__.lower(): {
"groups": [get_default_customer_group().pk]
},
RoundingBehaviorComponent.__name__.lower(): {
"mode": RoundingMode.ROUND_UP.value,
"quant": decimal.Decimal('0.05')
}
}

Expand Down
52 changes: 52 additions & 0 deletions shoop_tests/core/test_custom_payment_processor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
# This file is part of Shoop.
#
# Copyright (c) 2012-2016, Shoop Ltd. All rights reserved.
#
# This source code is licensed under the AGPLv3 license found in the
# LICENSE file in the root directory of this source tree.

from decimal import Decimal

import pytest

from shoop.core.models import (
CustomPaymentProcessor, PaymentMethod, PaymentStatus, PaymentUrls
)
from shoop.core.pricing import TaxfulPrice
from shoop.testing.factories import (
create_order_with_product, get_default_product, get_default_shop,
get_default_supplier, get_default_tax_class
)


@pytest.mark.django_db
@pytest.mark.parametrize('choice_identifier, expected_payment_status', [
('cash', PaymentStatus.FULLY_PAID),
('manual', PaymentStatus.DEFERRED)
])
def test_custom_payment_processor_cash_service(choice_identifier, expected_payment_status):
shop = get_default_shop()
product = get_default_product()
supplier = get_default_supplier()
processor = CustomPaymentProcessor.objects.create()
payment_method = PaymentMethod.objects.create(
shop=shop,
payment_processor=processor,
choice_identifier=choice_identifier,
tax_class=get_default_tax_class())
order = create_order_with_product(
product=product,
supplier=supplier,
quantity=1,
taxless_base_unit_price=Decimal('5.55'),
shop=shop)
order.taxful_total_price = TaxfulPrice(Decimal('5.55'), u'EUR')
order.payment_method = payment_method
order.save()

assert order.payment_status == PaymentStatus.NOT_PAID
processor.process_payment_return_request(choice_identifier, order, None)
assert order.payment_status == expected_payment_status
processor.process_payment_return_request(choice_identifier, order, None)
assert order.payment_status == expected_payment_status
Loading

0 comments on commit 9333228

Please sign in to comment.