Commit bb6f52ac by zubair-arbi Committed by Zubair Afzal

ENT-804 Link order with enrollment code to the provided business client

parent cb5cc595
...@@ -8,7 +8,7 @@ msgid "" ...@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-03-07 13:03+0000\n" "POT-Creation-Date: 2018-03-14 09:32+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
...@@ -42,7 +42,7 @@ msgid "A valid course ID is required" ...@@ -42,7 +42,7 @@ msgid "A valid course ID is required"
msgstr "" msgstr ""
#: ecommerce/static/js/models/coupon_model.js:106 #: ecommerce/static/js/models/coupon_model.js:106
#, javascript-format #, c-format
msgid "Email domain {%s} is invalid." msgid "Email domain {%s} is invalid."
msgstr "" msgstr ""
...@@ -162,43 +162,43 @@ msgstr "" ...@@ -162,43 +162,43 @@ msgstr ""
msgid "All credit seats must designate a number of credit hours." msgid "All credit seats must designate a number of credit hours."
msgstr "" msgstr ""
#: ecommerce/static/js/pages/basket_page.js:26 #: ecommerce/static/js/pages/basket_page.js:27
msgid "Problem occurred during checkout. Please contact support." msgid "Problem occurred during checkout. Please contact support."
msgstr "" msgstr ""
#: ecommerce/static/js/pages/basket_page.js:100 #: ecommerce/static/js/pages/basket_page.js:101
msgid "This field is required" msgid "This field is required"
msgstr "" msgstr ""
#: ecommerce/static/js/pages/basket_page.js:135 #: ecommerce/static/js/pages/basket_page.js:136
msgid "Invalid card number" msgid "Invalid card number"
msgstr "" msgstr ""
#: ecommerce/static/js/pages/basket_page.js:137 #: ecommerce/static/js/pages/basket_page.js:138
msgid "Unsupported card type" msgid "Unsupported card type"
msgstr "" msgstr ""
#: ecommerce/static/js/pages/basket_page.js:139 #: ecommerce/static/js/pages/basket_page.js:140
msgid "Invalid security number" msgid "Invalid security number"
msgstr "" msgstr ""
#: ecommerce/static/js/pages/basket_page.js:144 #: ecommerce/static/js/pages/basket_page.js:145
msgid "Invalid month" msgid "Invalid month"
msgstr "" msgstr ""
#: ecommerce/static/js/pages/basket_page.js:146 #: ecommerce/static/js/pages/basket_page.js:147
msgid "Invalid year" msgid "Invalid year"
msgstr "" msgstr ""
#: ecommerce/static/js/pages/basket_page.js:148 #: ecommerce/static/js/pages/basket_page.js:149
msgid "Card expired" msgid "Card expired"
msgstr "" msgstr ""
#: ecommerce/static/js/pages/basket_page.js:486 #: ecommerce/static/js/pages/basket_page.js:485
msgid "<Choose state/province>" msgid "<Choose state/province>"
msgstr "" msgstr ""
#: ecommerce/static/js/pages/basket_page.js:487 #: ecommerce/static/js/pages/basket_page.js:486
msgid "State/Province (required)" msgid "State/Province (required)"
msgstr "" msgstr ""
...@@ -253,23 +253,30 @@ msgstr "" ...@@ -253,23 +253,30 @@ msgstr ""
#: ecommerce/static/js/payment_processors/cybersource.js:249 #: ecommerce/static/js/payment_processors/cybersource.js:249
msgid "" msgid ""
"An error occurred while processing your payment. You have NOT been charged. " "An error occurred while processing your payment. You have NOT been charged. "
"Please try again, or select another payment method."
msgstr "" msgstr ""
#: ecommerce/static/js/payment_processors/stripe.js:71 #: ecommerce/static/js/payment_processors/stripe.js:71
msgid "" msgid ""
"An error occurred while attempting to process your payment. You have not " "An error occurred while attempting to process your payment. You have not "
"been charged. Please check your payment details, and try again." "been "
msgstr "" msgstr ""
#: ecommerce/static/js/payment_processors/stripe.js:110 #: ecommerce/static/js/payment_processors/stripe.js:110
msgid "An error occurred while processing your payment. Please try again." msgid "An error occurred while processing your payment. "
msgstr "" msgstr ""
#: ecommerce/static/js/utils/utils.js:184 #: ecommerce/static/js/utils/utils.js:184
msgid "Trailing comma not allowed." msgid "Trailing comma not allowed."
msgstr "" msgstr ""
#: ecommerce/static/js/utils/validation_patterns.js:18
msgid "The course ID is invalid."
msgstr ""
#: ecommerce/static/js/utils/validation_patterns.js:26
msgid "The product name cannot contain HTML."
msgstr ""
#: ecommerce/static/js/views/coupon_detail_view.js:107 #: ecommerce/static/js/views/coupon_detail_view.js:107
#: ecommerce/static/js/views/coupon_form_view.js:60 #: ecommerce/static/js/views/coupon_form_view.js:60
msgid "Can be used once by one customer" msgid "Can be used once by one customer"
...@@ -373,15 +380,12 @@ msgid "Professional Education" ...@@ -373,15 +380,12 @@ msgid "Professional Education"
msgstr "" msgstr ""
#: ecommerce/static/js/views/course_form_view.js:66 #: ecommerce/static/js/views/course_form_view.js:66
msgid "" msgid "Paid certificate track with initial verification and Professional "
"Paid certificate track with initial verification and Professional Education "
"Certificate"
msgstr "" msgstr ""
#: ecommerce/static/js/views/course_form_view.js:72 #: ecommerce/static/js/views/course_form_view.js:72
msgid "" msgid ""
"Paid certificate track with initial verification and Verified Certificate, " "Paid certificate track with initial verification and Verified Certificate, "
"and option to purchase credit"
msgstr "" msgstr ""
#. Translators: _START_, _END_, and _TOTAL_ are placeholders. Do NOT translate them. #. Translators: _START_, _END_, and _TOTAL_ are placeholders. Do NOT translate them.
......
...@@ -7,14 +7,14 @@ msgid "" ...@@ -7,14 +7,14 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-03-07 13:03+0000\n" "POT-Creation-Date: 2018-03-14 09:32+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: ecommerce/static/js/models/coupon_model.js #: ecommerce/static/js/models/coupon_model.js
...@@ -46,7 +46,7 @@ msgid "A valid course ID is required" ...@@ -46,7 +46,7 @@ msgid "A valid course ID is required"
msgstr "À välïd çöürsé ÌD ïs réqüïréd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#" msgstr "À välïd çöürsé ÌD ïs réqüïréd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#"
#: ecommerce/static/js/models/coupon_model.js #: ecommerce/static/js/models/coupon_model.js
#, javascript-format #, c-format
msgid "Email domain {%s} is invalid." msgid "Email domain {%s} is invalid."
msgstr "Émäïl dömäïn {%s} ïs ïnvälïd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#" msgstr "Émäïl dömäïn {%s} ïs ïnvälïd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#"
...@@ -281,35 +281,38 @@ msgstr "" ...@@ -281,35 +281,38 @@ msgstr ""
#: ecommerce/static/js/payment_processors/cybersource.js #: ecommerce/static/js/payment_processors/cybersource.js
msgid "" msgid ""
"An error occurred while processing your payment. You have NOT been charged. " "An error occurred while processing your payment. You have NOT been charged. "
"Please try again, or select another payment method."
msgstr "" msgstr ""
"Àn érrör öççürréd whïlé pröçéssïng ýöür päýmént. Ýöü hävé NÖT ßéén çhärgéd. " "Àn érrör öççürréd whïlé pröçéssïng ýöür päýmént. Ýöü hävé NÖT ßéén çhärgéd."
"Pléäsé trý ägäïn, ör séléçt änöthér päýmént méthöd. Ⱡ'σяє#" " Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυ#"
#: ecommerce/static/js/payment_processors/stripe.js #: ecommerce/static/js/payment_processors/stripe.js
msgid "" msgid ""
"An error occurred while attempting to process your payment. You have not " "An error occurred while attempting to process your payment. You have not "
"been charged. Please check your payment details, and try again." "been "
msgstr "" msgstr ""
"Àn érrör öççürréd whïlé ättémptïng tö pröçéss ýöür päýmént. Ýöü hävé nöt " "Àn érrör öççürréd whïlé ättémptïng tö pröçéss ýöür päýmént. Ýöü hävé nöt "
"ßéén çhärgéd. Pléäsé çhéçk ýöür päýmént détäïls, änd trý ägäïn. Ⱡ'σяєм ιρѕυм" "ßéén Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєт#"
" ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя "
"ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαgηα αłιqυα. υт єηιм α∂ мιηιм νєηιαм, qυιѕ "
"ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ ηιѕι υт αłιqυιρ єχ єα ¢σммσ∂σ "
"¢σηѕєqυαт. ∂υιѕ αυтє ιяυяє ∂σłσя ιη яєρяєнєη∂єяιт ιη νσłυρтαтє νєłιт єѕѕє "
"¢ιłłυм ∂σłσяє єυ ƒυgιαт ηυłłα ραяιαтυя. єχ¢єρтєυя ѕιηт σ¢¢αє¢αт ¢υρι∂αтαт "
"ηση ρяσι∂єηт, ѕυηт ιη ¢υłρα qυι σƒƒι¢ια ∂єѕєяυηт мσłłιт αηιм ι∂ єѕт łαвσя#"
#: ecommerce/static/js/payment_processors/stripe.js #: ecommerce/static/js/payment_processors/stripe.js
msgid "An error occurred while processing your payment. Please try again." msgid "An error occurred while processing your payment. "
msgstr "" msgstr ""
"Àn érrör öççürréd whïlé pröçéssïng ýöür päýmént. Pléäsé trý ägäïn. Ⱡ'σяєм " "Àn érrör öççürréd whïlé pröçéssïng ýöür päýmént. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт "
"ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #" "αмєт, ¢σηѕє¢тєтυя α#"
#: ecommerce/static/js/utils/utils.js #: ecommerce/static/js/utils/utils.js
msgid "Trailing comma not allowed." msgid "Trailing comma not allowed."
msgstr "Träïlïng çömmä nöt ällöwéd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#" msgstr "Träïlïng çömmä nöt ällöwéd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#"
#: ecommerce/static/js/utils/validation_patterns.js
msgid "The course ID is invalid."
msgstr "Thé çöürsé ÌD ïs ïnvälïd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#"
#: ecommerce/static/js/utils/validation_patterns.js
msgid "The product name cannot contain HTML."
msgstr ""
"Thé prödüçt nämé çännöt çöntäïn HTML. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, "
"¢σηѕє¢тєтυ#"
#: ecommerce/static/js/views/coupon_detail_view.js #: ecommerce/static/js/views/coupon_detail_view.js
#: ecommerce/static/js/views/coupon_form_view.js #: ecommerce/static/js/views/coupon_form_view.js
msgid "Can be used once by one customer" msgid "Can be used once by one customer"
...@@ -425,20 +428,17 @@ msgid "Professional Education" ...@@ -425,20 +428,17 @@ msgid "Professional Education"
msgstr "Pröféssïönäl Édüçätïön Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢#" msgstr "Pröféssïönäl Édüçätïön Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢#"
#: ecommerce/static/js/views/course_form_view.js #: ecommerce/static/js/views/course_form_view.js
msgid "" msgid "Paid certificate track with initial verification and Professional "
"Paid certificate track with initial verification and Professional Education "
"Certificate"
msgstr "" msgstr ""
"Päïd çértïfïçäté träçk wïth ïnïtïäl vérïfïçätïön änd Pröféssïönäl Édüçätïön " "Päïd çértïfïçäté träçk wïth ïnïtïäl vérïfïçätïön änd Pröféssïönäl Ⱡ'σяєм "
"Çértïfïçäté Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#" "ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #"
#: ecommerce/static/js/views/course_form_view.js #: ecommerce/static/js/views/course_form_view.js
msgid "" msgid ""
"Paid certificate track with initial verification and Verified Certificate, " "Paid certificate track with initial verification and Verified Certificate, "
"and option to purchase credit"
msgstr "" msgstr ""
"Päïd çértïfïçäté träçk wïth ïnïtïäl vérïfïçätïön änd Vérïfïéd Çértïfïçäté, " "Päïd çértïfïçäté träçk wïth ïnïtïäl vérïfïçätïön änd Vérïfïéd Çértïfïçäté, "
"änd öptïön tö pürçhäsé çrédït Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#" "Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυ#"
#. Translators: _START_, _END_, and _TOTAL_ are placeholders. Do NOT translate #. Translators: _START_, _END_, and _TOTAL_ are placeholders. Do NOT translate
#. them. #. them.
......
...@@ -21,6 +21,7 @@ Basket = get_model('basket', 'Basket') ...@@ -21,6 +21,7 @@ Basket = get_model('basket', 'Basket')
BasketAttribute = get_model('basket', 'BasketAttribute') BasketAttribute = get_model('basket', 'BasketAttribute')
BasketAttributeType = get_model('basket', 'BasketAttributeType') BasketAttributeType = get_model('basket', 'BasketAttributeType')
BUNDLE = 'bundle_identifier' BUNDLE = 'bundle_identifier'
ORGANIZATION_ATTRIBUTE_TYPE = 'organization'
StockRecord = get_model('partner', 'StockRecord') StockRecord = get_model('partner', 'StockRecord')
OrderLine = get_model('order', 'Line') OrderLine = get_model('order', 'Line')
Refund = get_model('refund', 'Refund') Refund = get_model('refund', 'Refund')
...@@ -221,3 +222,25 @@ def _record_utm_basket_attribution(referral, request): ...@@ -221,3 +222,25 @@ def _record_utm_basket_attribution(referral, request):
created_at_datetime = None created_at_datetime = None
referral.utm_created_at = created_at_datetime referral.utm_created_at = created_at_datetime
def basket_add_organization_attribute(basket, request_data):
"""
Add organization attribute on basket, if organization value is provided
in basket data.
Arguments:
basket(Basket): order basket
request_data (dict): HttpRequest data
"""
# Name of business client is being passed as "organization" from basket page
business_client = request_data.get(ORGANIZATION_ATTRIBUTE_TYPE)
if business_client:
organization_attribute, __ = BasketAttributeType.objects.get_or_create(name=ORGANIZATION_ATTRIBUTE_TYPE)
BasketAttribute.objects.get_or_create(
basket=basket,
attribute_type=organization_attribute,
value_text=business_client.strip()
)
...@@ -10,14 +10,20 @@ from ecommerce_worker.fulfillment.v1.tasks import fulfill_order ...@@ -10,14 +10,20 @@ from ecommerce_worker.fulfillment.v1.tasks import fulfill_order
from oscar.apps.checkout.mixins import OrderPlacementMixin from oscar.apps.checkout.mixins import OrderPlacementMixin
from oscar.core.loading import get_class, get_model from oscar.core.loading import get_class, get_model
from ecommerce.core.models import BusinessClient
from ecommerce.extensions.analytics.utils import audit_log, track_segment_event from ecommerce.extensions.analytics.utils import audit_log, track_segment_event
from ecommerce.extensions.api import data as data_api from ecommerce.extensions.api import data as data_api
from ecommerce.extensions.basket.utils import ORGANIZATION_ATTRIBUTE_TYPE
from ecommerce.extensions.checkout.exceptions import BasketNotFreeError from ecommerce.extensions.checkout.exceptions import BasketNotFreeError
from ecommerce.extensions.customer.utils import Dispatcher from ecommerce.extensions.customer.utils import Dispatcher
from ecommerce.extensions.order.constants import PaymentEventTypeName from ecommerce.extensions.order.constants import PaymentEventTypeName
from ecommerce.invoice.models import Invoice
CommunicationEventType = get_model('customer', 'CommunicationEventType') CommunicationEventType = get_model('customer', 'CommunicationEventType')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
Basket = get_model('basket', 'Basket')
BasketAttribute = get_model('basket', 'BasketAttribute')
BasketAttributeType = get_model('basket', 'BasketAttributeType')
Order = get_model('order', 'Order') Order = get_model('order', 'Order')
post_checkout = get_class('checkout.signals', 'post_checkout') post_checkout = get_class('checkout.signals', 'post_checkout')
PaymentEvent = get_model('order', 'PaymentEvent') PaymentEvent = get_model('order', 'PaymentEvent')
...@@ -220,3 +226,32 @@ class EdxOrderPlacementMixin(OrderPlacementMixin): ...@@ -220,3 +226,32 @@ class EdxOrderPlacementMixin(OrderPlacementMixin):
else: else:
logger.warning("Order #%s - no %s communication event type", logger.warning("Order #%s - no %s communication event type",
order.number, code) order.number, code)
def handle_post_order(self, order):
"""
Handle extra processing of order after its placed.
This method links the provided order with the BusinessClient for bulk
purchase through Invoice model.
Arguments:
order (Order): Order object
"""
basket_has_enrollment_code_product = any(
line.product.is_enrollment_code_product for line in order.basket.all_lines()
)
organization_attribute = BasketAttributeType.objects.filter(name=ORGANIZATION_ATTRIBUTE_TYPE).first()
if not organization_attribute:
return None
business_client = BasketAttribute.objects.filter(
basket=order.basket,
attribute_type=organization_attribute,
).first()
if basket_has_enrollment_code_product and business_client:
client, __ = BusinessClient.objects.get_or_create(name=business_client.value_text)
Invoice.objects.create(
order=order, business_client=client, type=Invoice.BULK_PURCHASE, state=Invoice.PAID
)
...@@ -5,12 +5,16 @@ import mock ...@@ -5,12 +5,16 @@ import mock
from django.core import mail from django.core import mail
from django.test import RequestFactory from django.test import RequestFactory
from oscar.core.loading import get_class, get_model from oscar.core.loading import get_class, get_model
from oscar.test.factories import ProductFactory, UserFactory from oscar.test.factories import BasketFactory, ProductFactory, UserFactory
from testfixtures import LogCapture from testfixtures import LogCapture
from waffle.models import Sample from waffle.models import Sample
from ecommerce.core.models import SegmentClient from ecommerce.core.constants import ENROLLMENT_CODE_PRODUCT_CLASS_NAME, ENROLLMENT_CODE_SWITCH
from ecommerce.core.models import BusinessClient, SegmentClient
from ecommerce.core.tests import toggle_switch
from ecommerce.courses.tests.factories import CourseFactory
from ecommerce.extensions.analytics.utils import parse_tracking_context, translate_basket_line_for_segment from ecommerce.extensions.analytics.utils import parse_tracking_context, translate_basket_line_for_segment
from ecommerce.extensions.basket.utils import basket_add_organization_attribute
from ecommerce.extensions.checkout.exceptions import BasketNotFreeError from ecommerce.extensions.checkout.exceptions import BasketNotFreeError
from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin
from ecommerce.extensions.fulfillment.status import ORDER from ecommerce.extensions.fulfillment.status import ORDER
...@@ -18,6 +22,7 @@ from ecommerce.extensions.payment.tests.mixins import PaymentEventsMixin ...@@ -18,6 +22,7 @@ from ecommerce.extensions.payment.tests.mixins import PaymentEventsMixin
from ecommerce.extensions.payment.tests.processors import DummyProcessor from ecommerce.extensions.payment.tests.processors import DummyProcessor
from ecommerce.extensions.refund.tests.mixins import RefundTestMixin from ecommerce.extensions.refund.tests.mixins import RefundTestMixin
from ecommerce.extensions.test.factories import create_basket, create_order from ecommerce.extensions.test.factories import create_basket, create_order
from ecommerce.invoice.models import Invoice
from ecommerce.tests.factories import SiteConfigurationFactory from ecommerce.tests.factories import SiteConfigurationFactory
from ecommerce.tests.mixins import BusinessIntelligenceMixin from ecommerce.tests.mixins import BusinessIntelligenceMixin
from ecommerce.tests.testcases import TestCase from ecommerce.tests.testcases import TestCase
...@@ -28,6 +33,7 @@ NoShippingRequired = get_class('shipping.methods', 'NoShippingRequired') ...@@ -28,6 +33,7 @@ NoShippingRequired = get_class('shipping.methods', 'NoShippingRequired')
OrderTotalCalculator = get_class('checkout.calculators', 'OrderTotalCalculator') OrderTotalCalculator = get_class('checkout.calculators', 'OrderTotalCalculator')
PaymentEventType = get_model('order', 'PaymentEventType') PaymentEventType = get_model('order', 'PaymentEventType')
SourceType = get_model('payment', 'SourceType') SourceType = get_model('payment', 'SourceType')
Product = get_model('catalogue', 'Product')
@mock.patch.object(SegmentClient, 'track') @mock.patch.object(SegmentClient, 'track')
...@@ -149,6 +155,54 @@ class EdxOrderPlacementMixinTests(BusinessIntelligenceMixin, PaymentEventsMixin, ...@@ -149,6 +155,54 @@ class EdxOrderPlacementMixinTests(BusinessIntelligenceMixin, PaymentEventsMixin,
) )
) )
def test_handle_post_order_for_bulk_purchase(self, __):
"""
Ensure that the bulk purchase order is linked to the provided business
client when the method `handle_post_order` is invoked.
"""
toggle_switch(ENROLLMENT_CODE_SWITCH, True)
course = CourseFactory()
course.create_or_update_seat('verified', True, 50, self.partner, create_enrollment_code=True)
enrollment_code = Product.objects.get(product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME)
user = UserFactory()
basket = BasketFactory(owner=user, site=self.site)
basket.add_product(enrollment_code, quantity=1)
order = create_order(number=1, basket=basket, user=user)
request_data = {'organization': 'Dummy Business Client'}
# Manually add organization attribute on the basket for testing
basket_add_organization_attribute(basket, request_data)
EdxOrderPlacementMixin().handle_post_order(order)
# Now verify that a new business client has been created in current
# order is now linked with that client through Invoice model.
business_client = BusinessClient.objects.get(name=request_data['organization'])
assert Invoice.objects.get(order=order).business_client == business_client
def test_handle_post_order_for_seat_purchase(self, __):
"""
Ensure that the single seat purchase order is not linked any business
client when the method `handle_post_order` is invoked.
"""
toggle_switch(ENROLLMENT_CODE_SWITCH, False)
course = CourseFactory()
verified_product = course.create_or_update_seat('verified', True, 50, self.partner)
user = UserFactory()
basket = BasketFactory(owner=user, site=self.site)
basket.add_product(verified_product, quantity=1)
order = create_order(number=1, basket=basket, user=user)
request_data = {'organization': 'Dummy Business Client'}
# Manually add organization attribute on the basket for testing
basket_add_organization_attribute(basket, request_data)
EdxOrderPlacementMixin().handle_post_order(order)
# Now verify that the single seat order is not linked to business
# client by checking that there is no record for BusinessClient.
assert not BusinessClient.objects.all()
def test_handle_successful_order_no_context(self, mock_track): def test_handle_successful_order_no_context(self, mock_track):
""" """
Ensure that expected values are substituted when no tracking_context Ensure that expected values are substituted when no tracking_context
......
...@@ -13,14 +13,20 @@ from oscar.apps.payment.exceptions import TransactionDeclined ...@@ -13,14 +13,20 @@ from oscar.apps.payment.exceptions import TransactionDeclined
from oscar.core.loading import get_class, get_model from oscar.core.loading import get_class, get_model
from oscar.test import factories from oscar.test import factories
from ecommerce.core.constants import ENROLLMENT_CODE_PRODUCT_CLASS_NAME, ENROLLMENT_CODE_SWITCH
from ecommerce.core.models import BusinessClient
from ecommerce.core.tests import toggle_switch
from ecommerce.core.url_utils import get_lms_url from ecommerce.core.url_utils import get_lms_url
from ecommerce.courses.tests.factories import CourseFactory
from ecommerce.extensions.api.serializers import OrderSerializer from ecommerce.extensions.api.serializers import OrderSerializer
from ecommerce.extensions.basket.utils import basket_add_organization_attribute
from ecommerce.extensions.order.constants import PaymentEventTypeName from ecommerce.extensions.order.constants import PaymentEventTypeName
from ecommerce.extensions.payment.exceptions import InvalidBasketError, InvalidSignatureError from ecommerce.extensions.payment.exceptions import InvalidBasketError, InvalidSignatureError
from ecommerce.extensions.payment.processors.cybersource import Cybersource from ecommerce.extensions.payment.processors.cybersource import Cybersource
from ecommerce.extensions.payment.tests.mixins import CybersourceMixin, CybersourceNotificationTestsMixin from ecommerce.extensions.payment.tests.mixins import CybersourceMixin, CybersourceNotificationTestsMixin
from ecommerce.extensions.payment.views.cybersource import CybersourceInterstitialView from ecommerce.extensions.payment.views.cybersource import CybersourceInterstitialView
from ecommerce.extensions.test.factories import create_basket from ecommerce.extensions.test.factories import create_basket
from ecommerce.invoice.models import Invoice
from ecommerce.tests.testcases import TestCase from ecommerce.tests.testcases import TestCase
JSON = 'application/json' JSON = 'application/json'
...@@ -32,6 +38,7 @@ PaymentEvent = get_model('order', 'PaymentEvent') ...@@ -32,6 +38,7 @@ PaymentEvent = get_model('order', 'PaymentEvent')
PaymentProcessorResponse = get_model('payment', 'PaymentProcessorResponse') PaymentProcessorResponse = get_model('payment', 'PaymentProcessorResponse')
Selector = get_class('partner.strategy', 'Selector') Selector = get_class('partner.strategy', 'Selector')
Source = get_model('payment', 'Source') Source = get_model('payment', 'Source')
Product = get_model('catalogue', 'Product')
post_checkout = get_class('checkout.signals', 'post_checkout') post_checkout = get_class('checkout.signals', 'post_checkout')
...@@ -246,6 +253,41 @@ class CybersourceInterstitialViewTests(CybersourceNotificationTestsMixin, TestCa ...@@ -246,6 +253,41 @@ class CybersourceInterstitialViewTests(CybersourceNotificationTestsMixin, TestCa
self.assertTrue(Order.objects.filter(basket=self.basket).exists()) self.assertTrue(Order.objects.filter(basket=self.basket).exists())
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
def test_successful_order_for_bulk_purchase(self):
"""
Verify the view redirects to the Receipt page when the Order has been
successfully placed for bulk purchase and also that the order is linked
to the provided business client.
"""
toggle_switch(ENROLLMENT_CODE_SWITCH, True)
course = CourseFactory()
course.create_or_update_seat('verified', True, 50, self.partner, create_enrollment_code=True)
enrollment_code = Product.objects.get(product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME)
self.basket = create_basket(owner=self.user, site=self.site)
self.basket.add_product(enrollment_code, quantity=1)
# The basket should not have an associated order if no payment was made.
self.assertFalse(Order.objects.filter(basket=self.basket).exists())
request_data = self.generate_notification(
self.basket,
billing_address=self.billing_address,
)
request_data.update({'organization': 'Dummy Business Client'})
# Manually add organization attribute on the basket for testing
basket_add_organization_attribute(self.basket, request_data)
response = self.client.post(self.path, request_data)
self.assertTrue(Order.objects.filter(basket=self.basket).exists())
self.assertEqual(response.status_code, 302)
# Now verify that a new business client has been created and current
# order is now linked with that client through Invoice model.
order = Order.objects.filter(basket=self.basket).first()
business_client = BusinessClient.objects.get(name=request_data['organization'])
assert Invoice.objects.get(order=order).business_client == business_client
def test_order_creation_error(self): def test_order_creation_error(self):
""" Verify the view redirects to the Payment error page when an error occurred during Order creation. """ """ Verify the view redirects to the Payment error page when an error occurred during Order creation. """
notification = self.generate_notification( notification = self.generate_notification(
......
...@@ -12,11 +12,17 @@ from oscar.core.loading import get_class, get_model ...@@ -12,11 +12,17 @@ from oscar.core.loading import get_class, get_model
from oscar.test import factories from oscar.test import factories
from testfixtures import LogCapture from testfixtures import LogCapture
from ecommerce.core.constants import ENROLLMENT_CODE_PRODUCT_CLASS_NAME, ENROLLMENT_CODE_SWITCH
from ecommerce.core.models import BusinessClient
from ecommerce.core.tests import toggle_switch
from ecommerce.courses.tests.factories import CourseFactory
from ecommerce.extensions.basket.utils import basket_add_organization_attribute
from ecommerce.extensions.checkout.utils import get_receipt_page_url from ecommerce.extensions.checkout.utils import get_receipt_page_url
from ecommerce.extensions.payment.processors.paypal import Paypal from ecommerce.extensions.payment.processors.paypal import Paypal
from ecommerce.extensions.payment.tests.mixins import PaymentEventsMixin, PaypalMixin from ecommerce.extensions.payment.tests.mixins import PaymentEventsMixin, PaypalMixin
from ecommerce.extensions.payment.views.paypal import PaypalPaymentExecutionView from ecommerce.extensions.payment.views.paypal import PaypalPaymentExecutionView
from ecommerce.extensions.test.factories import create_basket from ecommerce.extensions.test.factories import create_basket
from ecommerce.invoice.models import Invoice
from ecommerce.tests.testcases import TestCase from ecommerce.tests.testcases import TestCase
JSON = 'application/json' JSON = 'application/json'
...@@ -28,6 +34,7 @@ PaymentEventType = get_model('order', 'PaymentEventType') ...@@ -28,6 +34,7 @@ PaymentEventType = get_model('order', 'PaymentEventType')
PaymentProcessorResponse = get_model('payment', 'PaymentProcessorResponse') PaymentProcessorResponse = get_model('payment', 'PaymentProcessorResponse')
Selector = get_class('partner.strategy', 'Selector') Selector = get_class('partner.strategy', 'Selector')
SourceType = get_model('payment', 'SourceType') SourceType = get_model('payment', 'SourceType')
Product = get_model('catalogue', 'Product')
post_checkout = get_class('checkout.signals', 'post_checkout') post_checkout = get_class('checkout.signals', 'post_checkout')
...@@ -121,6 +128,49 @@ class PaypalPaymentExecutionViewTests(PaypalMixin, PaymentEventsMixin, TestCase) ...@@ -121,6 +128,49 @@ class PaypalPaymentExecutionViewTests(PaypalMixin, PaymentEventsMixin, TestCase)
fetch_redirect_response=False fetch_redirect_response=False
) )
@responses.activate
def test_execution_for_bulk_purchase(self):
"""
Verify redirection to LMS receipt page after attempted payment
execution if the Otto receipt page is disabled for bulk purchase and
also that the order is linked to the provided business client..
"""
toggle_switch(ENROLLMENT_CODE_SWITCH, True)
self.mock_oauth2_response()
course = CourseFactory()
course.create_or_update_seat('verified', True, 50, self.partner, create_enrollment_code=True)
self.basket = create_basket(owner=factories.UserFactory(), site=self.site)
enrollment_code = Product.objects.get(product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME)
factories.create_stockrecord(enrollment_code, num_in_stock=2, price_excl_tax='10.00')
self.basket.add_product(enrollment_code, quantity=1)
# Create a payment record the view can use to retrieve a basket
self.mock_payment_creation_response(self.basket)
self.processor.get_transaction_parameters(self.basket, request=self.request)
self.mock_payment_execution_response(self.basket)
self.mock_payment_creation_response(self.basket, find=True)
# Manually add organization attribute on the basket for testing
self.RETURN_DATA.update({'organization': 'Dummy Business Client'})
basket_add_organization_attribute(self.basket, self.RETURN_DATA)
response = self.client.get(reverse('paypal:execute'), self.RETURN_DATA)
self.assertRedirects(
response,
get_receipt_page_url(
order_number=self.basket.order_number,
site_configuration=self.basket.site.siteconfiguration
),
fetch_redirect_response=False
)
# Now verify that a new business client has been created and current
# order is now linked with that client through Invoice model.
order = Order.objects.filter(basket=self.basket).first()
business_client = BusinessClient.objects.get(name=self.RETURN_DATA['organization'])
assert Invoice.objects.get(order=order).business_client == business_client
@ddt.data( @ddt.data(
None, # falls back to PaypalMixin.PAYER_INFO, a fully-populated payer_info object None, # falls back to PaypalMixin.PAYER_INFO, a fully-populated payer_info object
{"shipping_address": None}, # minimal data, which may be sent in some Paypal execution responses {"shipping_address": None}, # minimal data, which may be sent in some Paypal execution responses
......
...@@ -7,12 +7,18 @@ from mock import mock ...@@ -7,12 +7,18 @@ from mock import mock
from oscar.core.loading import get_class, get_model from oscar.core.loading import get_class, get_model
from oscar.test.factories import BillingAddressFactory from oscar.test.factories import BillingAddressFactory
from ecommerce.core.constants import ENROLLMENT_CODE_PRODUCT_CLASS_NAME, ENROLLMENT_CODE_SWITCH
from ecommerce.core.models import BusinessClient
from ecommerce.core.tests import toggle_switch
from ecommerce.courses.tests.factories import CourseFactory
from ecommerce.extensions.basket.utils import basket_add_organization_attribute
from ecommerce.extensions.checkout.utils import get_receipt_page_url from ecommerce.extensions.checkout.utils import get_receipt_page_url
from ecommerce.extensions.order.constants import PaymentEventTypeName from ecommerce.extensions.order.constants import PaymentEventTypeName
from ecommerce.extensions.payment.constants import STRIPE_CARD_TYPE_MAP from ecommerce.extensions.payment.constants import STRIPE_CARD_TYPE_MAP
from ecommerce.extensions.payment.processors.stripe import Stripe from ecommerce.extensions.payment.processors.stripe import Stripe
from ecommerce.extensions.payment.tests.mixins import PaymentEventsMixin from ecommerce.extensions.payment.tests.mixins import PaymentEventsMixin
from ecommerce.extensions.test.factories import create_basket from ecommerce.extensions.test.factories import create_basket
from ecommerce.invoice.models import Invoice
from ecommerce.tests.testcases import TestCase from ecommerce.tests.testcases import TestCase
Country = get_model('address', 'Country') Country = get_model('address', 'Country')
...@@ -20,6 +26,7 @@ Order = get_model('order', 'Order') ...@@ -20,6 +26,7 @@ Order = get_model('order', 'Order')
PaymentEvent = get_model('order', 'PaymentEvent') PaymentEvent = get_model('order', 'PaymentEvent')
Selector = get_class('partner.strategy', 'Selector') Selector = get_class('partner.strategy', 'Selector')
Source = get_model('payment', 'Source') Source = get_model('payment', 'Source')
Product = get_model('catalogue', 'Product')
class StripeSubmitViewTests(PaymentEventsMixin, TestCase): class StripeSubmitViewTests(PaymentEventsMixin, TestCase):
...@@ -134,3 +141,52 @@ class StripeSubmitViewTests(PaymentEventsMixin, TestCase): ...@@ -134,3 +141,52 @@ class StripeSubmitViewTests(PaymentEventsMixin, TestCase):
self.assert_successful_order_response(response, basket.order_number) self.assert_successful_order_response(response, basket.order_number)
self.assert_order_created(basket, billing_address, card_type, label) self.assert_order_created(basket, billing_address, card_type, label)
def test_successful_payment_for_bulk_purchase(self):
"""
Verify that when a Order has been successfully placed for bulk
purchase then that order is linked to the provided business client.
"""
toggle_switch(ENROLLMENT_CODE_SWITCH, True)
course = CourseFactory()
course.create_or_update_seat('verified', True, 50, self.partner, create_enrollment_code=True)
basket = create_basket(owner=self.user, site=self.site)
enrollment_code = Product.objects.get(product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME)
basket.add_product(enrollment_code, quantity=1)
basket.strategy = Selector().strategy()
data = self.generate_form_data(basket.id)
data.update({'organization': 'Dummy Business Client'})
# Manually add organization attribute on the basket for testing
basket_add_organization_attribute(basket, data)
card_type = 'American Express'
label = '1986'
charge = stripe.Charge.construct_from({
'id': '2404',
'source': {
'brand': card_type,
'last4': label,
},
}, 'fake-key')
billing_address = BillingAddressFactory()
with mock.patch.object(Stripe, 'get_address_from_token') as address_mock:
address_mock.return_value = billing_address
with mock.patch.object(stripe.Charge, 'create') as charge_mock:
charge_mock.return_value = charge
response = self.client.post(self.path, data)
address_mock.assert_called_once_with(data['stripe_token'])
self.assert_successful_order_response(response, basket.order_number)
self.assert_order_created(basket, billing_address, card_type, label)
# Now verify that a new business client has been created and current
# order is now linked with that client through Invoice model.
order = Order.objects.filter(basket=basket).first()
business_client = BusinessClient.objects.get(name=data['organization'])
assert Invoice.objects.get(order=order).business_client == business_client
...@@ -26,6 +26,7 @@ from rest_framework.response import Response ...@@ -26,6 +26,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from ecommerce.extensions.api.serializers import OrderSerializer from ecommerce.extensions.api.serializers import OrderSerializer
from ecommerce.extensions.basket.utils import basket_add_organization_attribute
from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin
from ecommerce.extensions.checkout.utils import get_receipt_page_url from ecommerce.extensions.checkout.utils import get_receipt_page_url
from ecommerce.extensions.payment.exceptions import DuplicateReferenceNumber, InvalidBasketError, InvalidSignatureError from ecommerce.extensions.payment.exceptions import DuplicateReferenceNumber, InvalidBasketError, InvalidSignatureError
...@@ -136,6 +137,8 @@ class CybersourceSubmitView(BasePaymentSubmitView): ...@@ -136,6 +137,8 @@ class CybersourceSubmitView(BasePaymentSubmitView):
# don't have to deal with thawing the basket in the event of an error. # don't have to deal with thawing the basket in the event of an error.
response = JsonResponse({'form_fields': parameters}) response = JsonResponse({'form_fields': parameters})
basket_add_organization_attribute(basket, data)
# Freeze the basket since the user is paying for it now. # Freeze the basket since the user is paying for it now.
basket.freeze() basket.freeze()
...@@ -320,7 +323,8 @@ class CybersourceInterstitialView(CybersourceNotificationMixin, View): ...@@ -320,7 +323,8 @@ class CybersourceInterstitialView(CybersourceNotificationMixin, View):
return redirect(reverse('payment_error')) return redirect(reverse('payment_error'))
try: try:
self.create_order(request, basket, self._get_billing_address(notification)) order = self.create_order(request, basket, self._get_billing_address(notification))
self.handle_post_order(order)
return self.redirect_to_receipt_page(notification) return self.redirect_to_receipt_page(notification)
except: # pylint: disable=bare-except except: # pylint: disable=bare-except
......
...@@ -16,6 +16,7 @@ from oscar.apps.partner import strategy ...@@ -16,6 +16,7 @@ from oscar.apps.partner import strategy
from oscar.apps.payment.exceptions import PaymentError from oscar.apps.payment.exceptions import PaymentError
from oscar.core.loading import get_class, get_model from oscar.core.loading import get_class, get_model
from ecommerce.extensions.basket.utils import basket_add_organization_attribute
from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin
from ecommerce.extensions.checkout.utils import get_receipt_page_url from ecommerce.extensions.checkout.utils import get_receipt_page_url
from ecommerce.extensions.payment.processors.paypal import Paypal from ecommerce.extensions.payment.processors.paypal import Paypal
...@@ -66,6 +67,8 @@ class PaypalPaymentExecutionView(EdxOrderPlacementMixin, View): ...@@ -66,6 +67,8 @@ class PaypalPaymentExecutionView(EdxOrderPlacementMixin, View):
).basket ).basket
basket.strategy = strategy.Default() basket.strategy = strategy.Default()
Applicator().apply(basket, basket.owner, self.request) Applicator().apply(basket, basket.owner, self.request)
basket_add_organization_attribute(basket, self.request.GET)
return basket return basket
except MultipleObjectsReturned: except MultipleObjectsReturned:
logger.warning(u"Duplicate payment ID [%s] received from PayPal.", payment_id) logger.warning(u"Duplicate payment ID [%s] received from PayPal.", payment_id)
...@@ -112,7 +115,7 @@ class PaypalPaymentExecutionView(EdxOrderPlacementMixin, View): ...@@ -112,7 +115,7 @@ class PaypalPaymentExecutionView(EdxOrderPlacementMixin, View):
# than to retrieve an invoice number from PayPal. # than to retrieve an invoice number from PayPal.
order_number = basket.order_number order_number = basket.order_number
self.handle_order_placement( order = self.handle_order_placement(
order_number=order_number, order_number=order_number,
user=user, user=user,
basket=basket, basket=basket,
...@@ -123,6 +126,7 @@ class PaypalPaymentExecutionView(EdxOrderPlacementMixin, View): ...@@ -123,6 +126,7 @@ class PaypalPaymentExecutionView(EdxOrderPlacementMixin, View):
order_total=order_total, order_total=order_total,
request=request request=request
) )
self.handle_post_order(order)
return redirect(receipt_url) return redirect(receipt_url)
except: # pylint: disable=bare-except except: # pylint: disable=bare-except
......
...@@ -3,6 +3,7 @@ import logging ...@@ -3,6 +3,7 @@ import logging
from django.http import JsonResponse from django.http import JsonResponse
from oscar.core.loading import get_class, get_model from oscar.core.loading import get_class, get_model
from ecommerce.extensions.basket.utils import basket_add_organization_attribute
from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin
from ecommerce.extensions.checkout.utils import get_receipt_page_url from ecommerce.extensions.checkout.utils import get_receipt_page_url
from ecommerce.extensions.payment.forms import StripeSubmitForm from ecommerce.extensions.payment.forms import StripeSubmitForm
...@@ -36,6 +37,8 @@ class StripeSubmitView(EdxOrderPlacementMixin, BasePaymentSubmitView): ...@@ -36,6 +37,8 @@ class StripeSubmitView(EdxOrderPlacementMixin, BasePaymentSubmitView):
token = form_data['stripe_token'] token = form_data['stripe_token']
order_number = basket.order_number order_number = basket.order_number
basket_add_organization_attribute(basket, self.request.POST)
try: try:
billing_address = self.payment_processor.get_address_from_token(token) billing_address = self.payment_processor.get_address_from_token(token)
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
...@@ -56,7 +59,7 @@ class StripeSubmitView(EdxOrderPlacementMixin, BasePaymentSubmitView): ...@@ -56,7 +59,7 @@ class StripeSubmitView(EdxOrderPlacementMixin, BasePaymentSubmitView):
shipping_charge = shipping_method.calculate(basket) shipping_charge = shipping_method.calculate(basket)
order_total = OrderTotalCalculator().calculate(basket, shipping_charge) order_total = OrderTotalCalculator().calculate(basket, shipping_charge)
self.handle_order_placement( order = self.handle_order_placement(
order_number=order_number, order_number=order_number,
user=basket.owner, user=basket.owner,
basket=basket, basket=basket,
...@@ -67,6 +70,7 @@ class StripeSubmitView(EdxOrderPlacementMixin, BasePaymentSubmitView): ...@@ -67,6 +70,7 @@ class StripeSubmitView(EdxOrderPlacementMixin, BasePaymentSubmitView):
order_total=order_total, order_total=order_total,
request=self.request request=self.request
) )
self.handle_post_order(order)
receipt_url = get_receipt_page_url( receipt_url = get_receipt_page_url(
site_configuration=self.request.site.siteconfiguration, site_configuration=self.request.site.siteconfiguration,
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.9 on 2018-02-28 10:57
from __future__ import unicode_literals
import django_extensions.db.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('invoice', '0005_auto_20180119_0903'),
]
operations = [
migrations.AlterField(
model_name='invoice',
name='created',
field=django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created'),
),
migrations.AlterField(
model_name='invoice',
name='modified',
field=django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified'),
),
migrations.AlterField(
model_name='invoice',
name='type',
field=models.CharField(blank=True, choices=[(b'Prepaid', 'Prepaid'), (b'Postpaid', 'Postpaid'), (b'Bulk purchase', 'Bulk purchase'), (b'Not applicable', 'Not applicable')], default=b'Prepaid', max_length=255, null=True),
),
]
...@@ -10,10 +10,11 @@ class Invoice(TimeStampedModel): ...@@ -10,10 +10,11 @@ class Invoice(TimeStampedModel):
(NOT_PAID, _('Not Paid')), (NOT_PAID, _('Not Paid')),
(PAID, _('Paid')), (PAID, _('Paid')),
) )
PREPAID, POSTPAID, NA = 'Prepaid', 'Postpaid', 'Not applicable' PREPAID, POSTPAID, BULK_PURCHASE, NA = 'Prepaid', 'Postpaid', 'Bulk purchase', 'Not applicable'
type_choices = ( type_choices = (
(PREPAID, _('Prepaid')), (PREPAID, _('Prepaid')),
(POSTPAID, _('Postpaid')), (POSTPAID, _('Postpaid')),
(BULK_PURCHASE, _('Bulk purchase')),
(NA, _('Not applicable')) (NA, _('Not applicable'))
) )
PERCENTAGE, FIXED = 'Percentage', 'Fixed' PERCENTAGE, FIXED = 'Percentage', 'Fixed'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment