From da486eeebdfd1eda94ebcae8edfcc388e0ceaec9 Mon Sep 17 00:00:00 2001 From: Manas Solanki Date: Fri, 6 Jul 2018 12:36:57 +0530 Subject: [PATCH] Loyalty Program (#12631) * First Cut for the Loyalty Program * finished the collection part * redmeption for the loyalty point update the loyalty point entry in the FIFO style make the accounting entry knocking the debtors account against the expense account selected in the loyalty program update the outstanding balance in the client side * completed for the desk viewe * wrap up for the desk and shopping cart * pos * fix and test the travis --- .../doctype/loyalty_point_entry/__init__.py | 0 .../loyalty_point_entry.js | 8 + .../loyalty_point_entry.json | 419 +++++++++++ .../loyalty_point_entry.py | 27 + .../test_loyalty_point_entry.js | 23 + .../test_loyalty_point_entry.py | 10 + .../__init__.py | 0 .../loyalty_point_entry_redemption.json | 134 ++++ .../loyalty_point_entry_redemption.py | 10 + .../doctype/loyalty_program/__init__.py | 0 .../loyalty_program/loyalty_program.js | 65 ++ .../loyalty_program/loyalty_program.json | 687 ++++++++++++++++++ .../loyalty_program/loyalty_program.py | 125 ++++ .../loyalty_program/test_loyalty_program.js | 23 + .../loyalty_program/test_loyalty_program.py | 10 + .../loyalty_program_collection/__init__.py | 0 .../loyalty_program_collection.json | 161 ++++ .../loyalty_program_collection.py | 10 + .../payment_request/payment_request.py | 9 + .../doctype/sales_invoice/sales_invoice.js | 90 ++- .../doctype/sales_invoice/sales_invoice.json | 262 ++++++- .../doctype/sales_invoice/sales_invoice.py | 113 ++- erpnext/controllers/taxes_and_totals.py | 8 +- .../public/js/controllers/taxes_and_totals.js | 15 +- erpnext/selling/doctype/customer/customer.js | 5 + .../selling/doctype/customer/customer.json | 93 ++- erpnext/selling/doctype/customer/customer.py | 20 + .../doctype/sales_order/sales_order.json | 93 +++ .../doctype/sales_order/sales_order.py | 4 + .../page/point_of_sale/point_of_sale.js | 154 +++- .../delivery_note/test_delivery_note.py | 1 + erpnext/templates/pages/order.html | 171 +++-- erpnext/templates/pages/order.js | 40 + erpnext/templates/pages/order.py | 7 + 34 files changed, 2706 insertions(+), 91 deletions(-) create mode 100644 erpnext/accounts/doctype/loyalty_point_entry/__init__.py create mode 100644 erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.js create mode 100644 erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.json create mode 100644 erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py create mode 100644 erpnext/accounts/doctype/loyalty_point_entry/test_loyalty_point_entry.js create mode 100644 erpnext/accounts/doctype/loyalty_point_entry/test_loyalty_point_entry.py create mode 100644 erpnext/accounts/doctype/loyalty_point_entry_redemption/__init__.py create mode 100644 erpnext/accounts/doctype/loyalty_point_entry_redemption/loyalty_point_entry_redemption.json create mode 100644 erpnext/accounts/doctype/loyalty_point_entry_redemption/loyalty_point_entry_redemption.py create mode 100644 erpnext/accounts/doctype/loyalty_program/__init__.py create mode 100644 erpnext/accounts/doctype/loyalty_program/loyalty_program.js create mode 100644 erpnext/accounts/doctype/loyalty_program/loyalty_program.json create mode 100644 erpnext/accounts/doctype/loyalty_program/loyalty_program.py create mode 100644 erpnext/accounts/doctype/loyalty_program/test_loyalty_program.js create mode 100644 erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py create mode 100644 erpnext/accounts/doctype/loyalty_program_collection/__init__.py create mode 100644 erpnext/accounts/doctype/loyalty_program_collection/loyalty_program_collection.json create mode 100644 erpnext/accounts/doctype/loyalty_program_collection/loyalty_program_collection.py create mode 100644 erpnext/templates/pages/order.js diff --git a/erpnext/accounts/doctype/loyalty_point_entry/__init__.py b/erpnext/accounts/doctype/loyalty_point_entry/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.js b/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.js new file mode 100644 index 0000000000..d7dc7f3f3c --- /dev/null +++ b/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.js @@ -0,0 +1,8 @@ +// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Loyalty Point Entry', { + refresh: function(frm) { + + } +}); diff --git a/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.json b/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.json new file mode 100644 index 0000000000..be95f009d3 --- /dev/null +++ b/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.json @@ -0,0 +1,419 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "LPE.####", + "beta": 0, + "creation": "2018-01-23 05:40:18.117583", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "loyalty_program", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Loyalty Program", + "length": 0, + "no_copy": 0, + "options": "Loyalty Program", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "loyalty_program_tier", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Loyalty Program Tier", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "customer", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Customer", + "length": 0, + "no_copy": 0, + "options": "Customer", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sales_invoice", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Sales Invoice", + "length": 0, + "no_copy": 0, + "options": "Sales Invoice", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "redeem_against", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Redeem Against", + "length": 0, + "no_copy": 0, + "options": "Loyalty Point Entry", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "loyalty_points", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Loyalty Points", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "purchase_amount", + "fieldtype": "Currency", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Purchase Amount", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "expiry_date", + "fieldtype": "Date", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Expiry Date", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "posting_date", + "fieldtype": "Date", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Posting Date", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "company", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Company", + "length": 0, + "no_copy": 0, + "options": "Company", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2018-03-26 08:52:09.468010", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Loyalty Point Entry", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Auditor", + "set_user_permissions": 0, + "share": 0, + "submit": 0, + "write": 0 + }, + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "set_user_permissions": 0, + "share": 0, + "submit": 0, + "write": 0 + }, + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "set_user_permissions": 0, + "share": 0, + "submit": 0, + "write": 0 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "customer", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py b/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py new file mode 100644 index 0000000000..37fce0b856 --- /dev/null +++ b/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +from frappe.utils import today + +exclude_from_linked_with = True + +class LoyaltyPointEntry(Document): + pass + + +def get_loyalty_point_entries(customer, loyalty_program, expiry_date=None, company=None): + if not expiry_date: + date = today() + args_list = [customer, loyalty_program, expiry_date] + condition = '' + if company: + condition = " and company=%s " + args_list.append(company) + loyalty_point_details = frappe.db.sql('''select name, loyalty_points, expiry_date, loyalty_program_tier + from `tabLoyalty Point Entry` where customer=%s and loyalty_program=%s and expiry_date>=%s and loyalty_points>0 + {condition} order by expiry_date'''.format(condition=condition), tuple(args_list), as_dict=1) + return loyalty_point_details diff --git a/erpnext/accounts/doctype/loyalty_point_entry/test_loyalty_point_entry.js b/erpnext/accounts/doctype/loyalty_point_entry/test_loyalty_point_entry.js new file mode 100644 index 0000000000..a916b67522 --- /dev/null +++ b/erpnext/accounts/doctype/loyalty_point_entry/test_loyalty_point_entry.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Loyalty Point Entry", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Loyalty Point Entry + () => frappe.tests.make('Loyalty Point Entry', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/erpnext/accounts/doctype/loyalty_point_entry/test_loyalty_point_entry.py b/erpnext/accounts/doctype/loyalty_point_entry/test_loyalty_point_entry.py new file mode 100644 index 0000000000..b6e2d57b9f --- /dev/null +++ b/erpnext/accounts/doctype/loyalty_point_entry/test_loyalty_point_entry.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +class TestLoyaltyPointEntry(unittest.TestCase): + pass diff --git a/erpnext/accounts/doctype/loyalty_point_entry_redemption/__init__.py b/erpnext/accounts/doctype/loyalty_point_entry_redemption/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/loyalty_point_entry_redemption/loyalty_point_entry_redemption.json b/erpnext/accounts/doctype/loyalty_point_entry_redemption/loyalty_point_entry_redemption.json new file mode 100644 index 0000000000..8a8dfbb522 --- /dev/null +++ b/erpnext/accounts/doctype/loyalty_point_entry_redemption/loyalty_point_entry_redemption.json @@ -0,0 +1,134 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2018-03-26 01:32:10.108450", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sales_invoice", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Sales Invoice", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "redemption_date", + "fieldtype": "Date", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Redemption Date", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "redeemed_points", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Redeemed Points", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2018-03-26 03:12:59.173071", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Loyalty Point Entry Redemption", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/loyalty_point_entry_redemption/loyalty_point_entry_redemption.py b/erpnext/accounts/doctype/loyalty_point_entry_redemption/loyalty_point_entry_redemption.py new file mode 100644 index 0000000000..e4382b6c78 --- /dev/null +++ b/erpnext/accounts/doctype/loyalty_point_entry_redemption/loyalty_point_entry_redemption.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class LoyaltyPointEntryRedemption(Document): + pass diff --git a/erpnext/accounts/doctype/loyalty_program/__init__.py b/erpnext/accounts/doctype/loyalty_program/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js new file mode 100644 index 0000000000..524a671801 --- /dev/null +++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js @@ -0,0 +1,65 @@ +// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Loyalty Program', { + setup: function(frm) { + var help_content = + ` + +
+

+ + ${__('Notes')} +

+
    +
  • + ${__("Loyalty Points will be calculated from the spent done (via the Sales Invoice), based on collection factor mentioned.")} +
  • +
  • + ${__("There can be multiple tiered collection factor based on the total spent. But the conversion factor for redemption will always be same for all the tier.")} +
  • +
  • + ${__("In the case of multi-tier program, Customers will be auto assigned to the concerned tier as per their spent")} +
  • +
  • + ${__("If unlimited expiry for the Loyalty Points, keep the Expiry Duration empty or 0.")} +
  • +
  • + ${__("If Auto Opt In is checked, then the customers will be automatically linked with the concerned Loyalty Program (on save)")} +
  • +
  • + ${__("One customer can be part of only single Loyalty Program.")} +
  • +
+
`; + set_field_options("loyalty_program_help", help_content); + }, + + onload: function(frm) { + frm.set_query("expense_account", function(doc) { + return { + filters: { + "root_type": "Expense", + 'is_group': 0, + 'company': doc.company + } + }; + }); + + frm.set_query("cost_center", function() { + return { + filters: { + company: frm.doc.company + } + }; + }); + + frm.set_value("company", frappe.defaults.get_user_default("Company")); + }, + + refresh: function(frm) { + if (frm.doc.loyalty_program_type === "Single Tier Program" && frm.doc.collection_rules.length > 1) { + frappe.throw(__("Please select the Multiple Tier Program type for more than one collection rules.")); + } + } +}); diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.json b/erpnext/accounts/doctype/loyalty_program/loyalty_program.json new file mode 100644 index 0000000000..4536a7a289 --- /dev/null +++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.json @@ -0,0 +1,687 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "field:loyalty_program_name", + "beta": 0, + "creation": "2018-01-23 06:23:05.731431", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "disabled", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Disabled", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_2", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "auto_opt_in", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Auto Opt In (For all customers)", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_2", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "loyalty_program_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Loyalty Program Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "loyalty_program_type", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Loyalty Program Type", + "length": 0, + "no_copy": 0, + "options": "Single Tier Program\nMultiple Tier Program", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_7", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "customer_group", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Customer Group", + "length": 0, + "no_copy": 0, + "options": "Customer Group", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "customer_territory", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Customer Territory", + "length": 0, + "no_copy": 0, + "options": "Territory", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "rules", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Collection Tier", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "collection_rules", + "fieldtype": "Table", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Collection Rules", + "length": 0, + "no_copy": 0, + "options": "Loyalty Program Collection", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "redemption", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Redemption", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "1 Loyalty Points = How much base currency?", + "fieldname": "conversion_factor", + "fieldtype": "Float", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Conversion Factor", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "expiry_duration", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Expiry Duration (in days)", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_10", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "expense_account", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Expense Account", + "length": 0, + "no_copy": 0, + "options": "Account", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cost_center", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Cost Center", + "length": 0, + "no_copy": 0, + "options": "Cost Center", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "company", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Company", + "length": 0, + "no_copy": 0, + "options": "Company", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "help_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Help Section", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "loyalty_program_help", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Loyalty Program Help", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2018-03-21 10:20:25.468206", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Loyalty Program", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py new file mode 100644 index 0000000000..593e426531 --- /dev/null +++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe + +from frappe import _ +from frappe.model.document import Document +from frappe.utils import today, flt + +class LoyaltyProgram(Document): + pass + + +def get_loyalty_details(customer, loyalty_program, expiry_date=None, company=None): + if not expiry_date: + expiry_date = today() + args_list = [customer, loyalty_program, expiry_date] + condition = '' + if company: + condition = " and company=%s " + args_list.append(company) + loyalty_point_details = frappe.db.sql('''select sum(loyalty_points) as loyalty_points, + sum(purchase_amount) as total_spent from `tabLoyalty Point Entry` + where customer=%s and loyalty_program=%s and (expiry_date>=%s) {condition} + group by customer'''.format(condition=condition), tuple(args_list), as_dict=1) + if loyalty_point_details: + return loyalty_point_details[0] + else: + return {"loyalty_points": 0, "total_spent": 0} + +@frappe.whitelist() +def get_loyalty_program_details(customer, loyalty_program=None, expiry_date=None, company=None, silent=False): + lp_details = frappe._dict() + customer_loyalty_program = frappe.db.get_value("Customer", customer, "loyalty_program") + + if not (customer_loyalty_program or silent): + frappe.throw(_("Customer isn't enrolled in any Loyalty Program")) + elif silent and not customer_loyalty_program: + return frappe._dict({"loyalty_program": None}) + + if loyalty_program and loyalty_program != customer_loyalty_program: + frappe.throw(_("Customer isn't enrolled in this Loyalty Program")) + + if not loyalty_program: + loyalty_program = customer_loyalty_program + if not company: + company = frappe.db.get_default("company") or frappe.get_all("Company")[0].name + + lp_details.update(get_loyalty_details(customer, loyalty_program, expiry_date, company)) + + lp_details.update({"loyalty_program": loyalty_program}) + loyalty_program = frappe.get_doc("Loyalty Program", lp_details.loyalty_program) + + lp_details.expiry_duration = loyalty_program.expiry_duration + lp_details.conversion_factor = loyalty_program.conversion_factor + lp_details.expense_account = loyalty_program.expense_account + lp_details.cost_center = loyalty_program.cost_center + lp_details.company = loyalty_program.company + + tier_spent_level = sorted([d.as_dict() for d in loyalty_program.collection_rules], key=lambda rule:rule.min_spent, reverse=True) + for i, d in enumerate(tier_spent_level): + if i==0 or lp_details.total_spent < d.min_spent: + lp_details.tier_name = d.tier_name + lp_details.collection_factor = d.collection_factor + else: + break + return lp_details + +@frappe.whitelist() +def get_redeemption_factor(loyalty_program=None, customer=None): + customer_loyalty_program = None + if not loyalty_program: + customer_loyalty_program = frappe.db.get_value("Customer", customer, "loyalty_program") + loyalty_program = customer_loyalty_program + if loyalty_program: + return frappe.db.get_value("Loyalty Program", loyalty_program, "conversion_factor") + else: + frappe.throw(_("Customer isn't enrolled in any Loyalty Program")) + + +def validate_loyalty_points(ref_doc, points_to_redeem): + loyalty_program = None + posting_date = None + + if ref_doc.doctype == "Sales Invoice": + posting_date = ref_doc.posting_date + else: + posting_date = today() + + if hasattr(ref_doc, "loyalty_program") and ref_doc.loyalty_program: + loyalty_program = ref_doc.loyalty_program + else: + loyalty_program = frappe.db.get_value("Customer", ref_doc.customer, ["loyalty_program"]) + + if loyalty_program and frappe.db.get_value("Loyalty Program", loyalty_program, ["company"]) !=\ + ref_doc.company: + frappe.throw(_("The Loyalty Program isn't valid for the selected company")) + + if loyalty_program and points_to_redeem: + loyalty_program_details = get_loyalty_program_details(ref_doc.customer, loyalty_program, + posting_date, ref_doc.company) + + if points_to_redeem > loyalty_program_details.loyalty_points: + frappe.throw(_("You don't have enought Loyalty Points to redeem")) + + loyalty_amount = flt(points_to_redeem * loyalty_program_details.conversion_factor) + + if loyalty_amount > ref_doc.grand_total: + frappe.throw(_("You can't redeem Loyalty Points having more value than the Grand Total.")) + + if not ref_doc.loyalty_amount and ref_doc.loyalty_amount != loyalty_amount: + ref_doc.loyalty_amount = loyalty_amount + + if ref_doc.doctype == "Sales Invoice": + ref_doc.loyalty_program = loyalty_program + if not ref_doc.loyalty_redemption_account: + ref_doc.loyalty_redemption_account = loyalty_program_details.expense_account + + if not ref_doc.loyalty_redemption_cost_center: + ref_doc.loyalty_redemption_cost_center = loyalty_program_details.cost_center + + elif ref_doc.doctype == "Sales Order": + return loyalty_amount \ No newline at end of file diff --git a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.js b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.js new file mode 100644 index 0000000000..9321c14e1f --- /dev/null +++ b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Loyalty Program", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Loyalty Program + () => frappe.tests.make('Loyalty Program', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py new file mode 100644 index 0000000000..a5cf765599 --- /dev/null +++ b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +class TestLoyaltyProgram(unittest.TestCase): + pass diff --git a/erpnext/accounts/doctype/loyalty_program_collection/__init__.py b/erpnext/accounts/doctype/loyalty_program_collection/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/loyalty_program_collection/loyalty_program_collection.json b/erpnext/accounts/doctype/loyalty_program_collection/loyalty_program_collection.json new file mode 100644 index 0000000000..6ea52169b5 --- /dev/null +++ b/erpnext/accounts/doctype/loyalty_program_collection/loyalty_program_collection.json @@ -0,0 +1,161 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2018-01-23 06:56:37.163859", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 0, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "tier_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Tier Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "min_spent", + "fieldtype": "Currency", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Minimum Total Spent", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_3", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "For how much spent = 1 Loyalty Point", + "fieldname": "collection_factor", + "fieldtype": "Currency", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Collection Factor (=1 LP)", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2018-01-23 07:19:10.316392", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Loyalty Program Collection", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/loyalty_program_collection/loyalty_program_collection.py b/erpnext/accounts/doctype/loyalty_program_collection/loyalty_program_collection.py new file mode 100644 index 0000000000..42cc38cb4c --- /dev/null +++ b/erpnext/accounts/doctype/loyalty_program_collection/loyalty_program_collection.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class LoyaltyProgramCollection(Document): + pass diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index fd48faa3a2..a633cc31a6 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -240,14 +240,23 @@ def make_payment_request(**args): """Make payment request""" args = frappe._dict(args) + ref_doc = frappe.get_doc(args.dt, args.dn) grand_total = get_amount(ref_doc, args.dt) + if args.loyalty_points and args.dt == "Sales Order": + from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points + loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points)) + frappe.db.set_value("Sales Order", args.dn, "loyalty_points", int(args.loyalty_points), update_modified=False) + frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False) + grand_total = grand_total - loyalty_amount + gateway_account = get_gateway_details(args) or frappe._dict() existing_payment_request = frappe.db.get_value("Payment Request", {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ["!=", 2]}) if existing_payment_request: + frappe.db.set_value("Payment Request", existing_payment_request, "grand_total", grand_total, update_modified=False) pr = frappe.get_doc("Payment Request", existing_payment_request) else: diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 8f57352e78..f2b9ba295c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -17,6 +17,8 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte onload: function() { var me = this; this._super(); + console.log("class erpnext.accounts.SalesInvoiceController, onload this->", this); + if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { // show debit_to in print format @@ -36,6 +38,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte }, refresh: function(doc, dt, dn) { + console.log("triggered the SalesInvoiceController"); this._super(); if(cur_frm.msgbox && cur_frm.msgbox.$wrapper.is(":visible")) { // hide new msgbox @@ -228,7 +231,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte price_list: this.frm.doc.selling_price_list, }, function() { me.apply_pricing_rule(); - }) + }); }, make_inter_company_invoice: function() { @@ -355,7 +358,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte }); } } - else this.frm.trigger("refresh") + else this.frm.trigger("refresh"); }, amount: function(){ @@ -364,13 +367,21 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte change_amount: function(){ if(this.frm.doc.paid_amount > this.frm.doc.grand_total){ - this.calculate_write_off_amount() + this.calculate_write_off_amount(); }else { - this.frm.set_value("change_amount", 0.0) - this.frm.set_value("base_change_amount", 0.0) + this.frm.set_value("change_amount", 0.0); + this.frm.set_value("base_change_amount", 0.0); } this.frm.refresh_fields(); + }, + + loyalty_amount: function(){ + console.log("triggered the loyalty amount"); + this.calculate_outstanding_amount(); + this.frm.refresh_field("outstanding_amount"); + this.frm.refresh_field("paid_amount"); + this.frm.refresh_field("base_paid_amount"); } }); @@ -521,7 +532,12 @@ cur_frm.set_query("asset", "items", function(doc, cdt, cdn) { }); frappe.ui.form.on('Sales Invoice', { + refresh: function(frm) { + frm.add_fetch('customer', 'loyalty_program', 'loyalty_program'); + }, + setup: function(frm){ + frm.custom_make_buttons = { 'Delivery Note': 'Delivery', 'Sales Invoice': 'Sales Return', @@ -612,7 +628,71 @@ frappe.ui.form.on('Sales Invoice', { refresh_field(['timesheets']) } }) + }, + + onload: function(frm) { + frm.redemption_conversion_factor = null; + }, + + redeem_loyalty_points: function(frm) { + frm.events.get_loyalty_details(frm); + }, + + loyalty_points: function(frm) { + if (frm.redemption_conversion_factor) { + frm.events.set_loyalty_points(frm); + } else { + frappe.call({ + method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_redeemption_factor", + args: { + "loyalty_program": frm.doc.loyalty_program + }, + callback: function(r) { + if (r) { + frm.redemption_conversion_factor = r.message; + frm.events.set_loyalty_points(frm); + } + } + }); + } + }, + + get_loyalty_details: function(frm) { + if (frm.doc.customer && frm.doc.redeem_loyalty_points) { + frappe.call({ + method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_program_details", + args: { + "customer": frm.doc.customer, + "till_date": frm.doc.posting_date, + "company": frm.doc.company + }, + callback: function(r) { + if (r) { + frm.set_value("loyalty_program", r.message.loyalty_program); + frm.set_value("loyalty_redemption_account", r.message.expense_account); + frm.set_value("loyalty_redemption_cost_center", r.message.cost_center); + frm.redemption_conversion_factor = r.message.conversion_factor; + // let max_loyalty_points = parseInt((frm.doc.grand_total-frm.doc.total_advance)/r.message.conversion_factor); + // let redeemable_points = max_loyalty_points > r.message.loyalty_points ? r.message.loyalty_points : max_loyalty_points; + // frm.set_value("loyalty_points", redeemable_points); + } + } + }); + } + }, + + set_loyalty_points: function(frm) { + if (frm.redemption_conversion_factor) { + let loyalty_amount = flt(frm.redemption_conversion_factor*flt(frm.doc.loyalty_points), precision("loyalty_amount")); + var remaining_amount = flt(frm.doc.grand_total - frm.doc.total_advance) + if (frm.doc.grand_total && (remaining_amount < loyalty_amount)) { + let redeemable_amount = parseInt(remaining_amount/frm.redemption_conversion_factor); + frappe.throw(__("You can only redeem max {0} points in this order.",[redeemable_amount])); + } + frm.set_value("loyalty_amount", loyalty_amount); + } } + }) frappe.ui.form.on('Sales Invoice Timesheet', { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 89bfc8f0d2..663042fc59 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -381,6 +381,38 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "redeem_loyalty_points", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Redeem Loyalty Points", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -2423,6 +2455,234 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": "", + "columns": 0, + "depends_on": "redeem_loyalty_points", + "fieldname": "loyalty_points_redemption", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Loyalty Points Redemption", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "loyalty_points", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Loyalty Points", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "loyalty_amount", + "fieldtype": "Currency", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Loyalty Amount", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_77", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "loyalty_program", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Loyalty Program", + "length": 0, + "no_copy": 0, + "options": "Loyalty Program", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "loyalty_redemption_account", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Redemption Account", + "length": 0, + "no_copy": 0, + "options": "Account", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "loyalty_redemption_cost_center", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Redemption Cost Center", + "length": 0, + "no_copy": 0, + "options": "Cost Center", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -3426,7 +3686,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "depends_on": "is_pos", + "depends_on": "eval: doc.is_pos || doc.redeem_loyalty_points", "fieldname": "paid_amount", "fieldtype": "Currency", "hidden": 0, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 4c617b457a..7f11f3563e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe, erpnext import frappe.defaults -from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days +from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr from frappe import _, msgprint, throw from erpnext.accounts.party import get_party_account, get_due_date from erpnext.controllers.stock_controller import update_gl_entries_after @@ -21,6 +21,8 @@ from erpnext.stock.doctype.batch.batch import set_batch_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_delivery_note_serial_no from erpnext.setup.doctype.company.company import update_company_current_month_sales from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center +from erpnext.accounts.doctype.loyalty_program.loyalty_program import \ + get_loyalty_program_details, get_loyalty_details, validate_loyalty_points from six import iteritems @@ -106,6 +108,9 @@ class SalesInvoice(SellingController): self.set_status() if self.is_pos and not self.is_return: self.verify_payment_amount_is_positive() + if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points: + validate_loyalty_points(self, self.loyalty_points) + def before_save(self): set_account_for_mode_of_payment(self) @@ -152,6 +157,15 @@ class SalesInvoice(SellingController): self.update_project() update_linked_invoice(self.doctype, self.name, self.inter_company_invoice_reference) + # create the loyalty point ledger entry if the customer is enrolled in any loyalty program + if not self.is_return and self.loyalty_program: + self.make_loyalty_point_entry() + elif self.is_return and self.return_against and self.loyalty_program: + against_si_doc = frappe.get_doc("Sales Invoice", self.return_against) + against_si_doc.delete_loyalty_point_entry() + if self.redeem_loyalty_points and self.loyalty_points: + self.apply_loyalty_points() + def validate_pos_paid_amount(self): if len(self.payments) == 0 and self.is_pos: frappe.throw(_("At least one mode of payment is required for POS invoice.")) @@ -191,6 +205,11 @@ class SalesInvoice(SellingController): if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction": update_company_current_month_sales(self.company) self.update_project() + if not self.is_return and self.loyalty_program: + self.delete_loyalty_point_entry() + elif self.is_return and self.return_against and self.loyalty_program: + against_si_doc = frappe.get_doc("Sales Invoice", self.return_against) + against_si_doc.make_loyalty_point_entry() unlink_inter_company_invoice(self.doctype, self.name, self.inter_company_invoice_reference) @@ -607,7 +626,8 @@ class SalesInvoice(SellingController): from erpnext.accounts.general_ledger import make_gl_entries # if POS and amount is written off, updating outstanding amt after posting all gl entries - update_outstanding = "No" if (cint(self.is_pos) or self.write_off_account) else "Yes" + update_outstanding = "No" if (cint(self.is_pos) or self.write_off_account or + cint(self.redeem_loyalty_points)) else "Yes" make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding=update_outstanding, merge_entries=False) @@ -640,6 +660,7 @@ class SalesInvoice(SellingController): # merge gl entries before adding pos entries gl_entries = merge_similar_entries(gl_entries) + self.make_loyalty_point_redemption_gle(gl_entries) self.make_pos_gl_entries(gl_entries) self.make_gle_for_change_amount(gl_entries) @@ -716,6 +737,29 @@ class SalesInvoice(SellingController): erpnext.is_perpetual_inventory_enabled(self.company): gl_entries += super(SalesInvoice, self).get_gl_entries() + def make_loyalty_point_redemption_gle(self, gl_entries): + if cint(self.redeem_loyalty_points): + gl_entries.append( + self.get_gl_dict({ + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "against": "Expense account - " + cstr(self.loyalty_redemption_account) + " for the Loyalty Program", + "credit": self.loyalty_amount, + "against_voucher": self.return_against if cint(self.is_return) else self.name, + "against_voucher_type": self.doctype + }) + ) + gl_entries.append( + self.get_gl_dict({ + "account": self.loyalty_redemption_account, + "cost_center": self.loyalty_redemption_cost_center, + "against": self.customer, + "debit": self.loyalty_amount, + "remark": "Loyalty Points redeemed by the customer" + }) + ) + def make_pos_gl_entries(self, gl_entries): if cint(self.is_pos): for payment_mode in self.payments: @@ -920,6 +964,71 @@ class SalesInvoice(SellingController): if entry.amount < 0: frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx)) + # collection of the loyalty points, create the ledger entry for that. + def make_loyalty_point_entry(self): + loyalty_program_details = get_loyalty_program_details(self.customer, company=self.company) + if loyalty_program_details: + points_earned = int(self.grand_total/loyalty_program_details.collection_factor) + doc = frappe.get_doc({ + "doctype": "Loyalty Point Entry", + "company": self.company, + "loyalty_program": loyalty_program_details.loyalty_program, + "loyalty_program_tier": loyalty_program_details.tier_name, + "customer": self.customer, + "sales_invoice": self.name, + "loyalty_points": points_earned, + "purchase_amount": self.grand_total, + "expiry_date": add_days(self.posting_date, loyalty_program_details.expiry_duration), + "posting_date": self.posting_date + }) + doc.flags.ignore_permissions = 1 + doc.save() + # frappe.db.set_value("Customer", self.customer, "loyalty_program_tier", loyalty_program_details.tier_name) + + # valdite the redemption and then delete the loyalty points earned on cancel of the invoice + def delete_loyalty_point_entry(self): + lp_entry = frappe.db.sql("select name from `tabLoyalty Point Entry` where sales_invoice=%s", + (self.name), as_dict=1)[0] + against_lp_entry = frappe.db.sql('''select name, sales_invoice from `tabLoyalty Point Entry` + where redeem_against=%s''', (lp_entry.name), as_dict=1) + if against_lp_entry: + invoice_list = ", ".join([d.sales_invoice for d in against_lp_entry]) + frappe.throw(_('''Sales Invoice can't be cancelled since the Loyalty Points earned has been redeemed. + First cancel the Sales Invoice No {0}''').format(invoice_list)) + else: + frappe.db.sql('''delete from `tabLoyalty Point Entry` where sales_invoice=%s''', (self.name)) + + # redeem the loyalty points. + def apply_loyalty_points(self): + from erpnext.accounts.doctype.loyalty_point_entry.loyalty_point_entry \ + import get_loyalty_point_entries + loyalty_point_entries = get_loyalty_point_entries(self.customer, self.loyalty_program, self.posting_date, self.company) + + points_to_redeem = self.loyalty_amount + for lp_entry in loyalty_point_entries: + if lp_entry.loyalty_points > points_to_redeem: + redeemed_points = points_to_redeem + else: + redeemed_points = lp_entry.loyalty_points + doc = frappe.get_doc({ + "doctype": "Loyalty Point Entry", + "company": self.company, + "loyalty_program": self.loyalty_program, + "loyalty_program_tier": lp_entry.loyalty_program_tier, + "customer": self.customer, + "sales_invoice": self.name, + "redeem_against": lp_entry.name, + "loyalty_points": -(redeemed_points), + "purchase_amount": self.grand_total, + "expiry_date": lp_entry.expiry_date, + "posting_date": self.posting_date + }) + doc.flags.ignore_permissions = 1 + doc.save() + points_to_redeem -= redeemed_points + if points_to_redeem < 1: # since points_to_redeem is integer + break + def book_income_for_deferred_revenue(self): # book the income on the last day, but it will be trigger on the 1st of month at 12:00 AM # start_date: 1st of the last month or the start date diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 6f9db34416..bf1c13954c 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -462,7 +462,7 @@ class calculate_taxes_and_totals(object): if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]: grand_total = self.doc.rounded_total or self.doc.grand_total if self.doc.party_account_currency == self.doc.currency: - total_amount_to_pay = flt(grand_total - self.doc.total_advance + total_amount_to_pay = flt(grand_total - self.doc.total_advance - flt(self.doc.write_off_amount), self.doc.precision("grand_total")) else: total_amount_to_pay = flt(flt(grand_total * @@ -481,11 +481,11 @@ class calculate_taxes_and_totals(object): paid_amount = self.doc.paid_amount \ if self.doc.party_account_currency == self.doc.currency else self.doc.base_paid_amount - self.doc.outstanding_amount = flt(total_amount_to_pay - flt(paid_amount) + flt(change_amount), self.doc.precision("outstanding_amount")) def calculate_paid_amount(self): + paid_amount = base_paid_amount = 0.0 if self.doc.is_pos: @@ -497,6 +497,10 @@ class calculate_taxes_and_totals(object): elif not self.doc.is_return: self.doc.set('payments', []) + if self.doc.redeem_loyalty_points and self.doc.loyalty_amount: + base_paid_amount += self.doc.loyalty_amount + paid_amount += (self.doc.loyalty_amount / flt(self.doc.conversion_rate)) + self.doc.paid_amount = flt(paid_amount, self.doc.precision("paid_amount")) self.doc.base_paid_amount = flt(base_paid_amount, self.doc.precision("base_paid_amount")) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index c24945ea98..303d21f4db 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -568,9 +568,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ calculate_outstanding_amount: function(update_paid_amount) { // NOTE: - // paid_amount and write_off_amount is only for POS Invoice + // paid_amount and write_off_amount is only for POS/Loyalty Point Redemption Invoice // total_advance is only for non POS Invoice - if(this.frm.doc.doctype == "Sales Invoice" && this.frm.doc.is_return){ this.calculate_paid_amount(); } @@ -602,16 +601,19 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } if(this.frm.doc.doctype == "Sales Invoice") { - this.set_default_payment(total_amount_to_pay, update_paid_amount); + let total_amount_for_payment = (this.frm.doc.redeem_loyalty_points && this.frm.doc.loyalty_amount) + ? flt(total_amount_to_pay - this.frm.doc.loyalty_amount, precision("base_grand_total")) + : total_amount_to_pay; + this.set_default_payment(total_amount_for_payment, update_paid_amount); this.calculate_paid_amount(); } this.calculate_change_amount(); var paid_amount = (this.frm.doc.party_account_currency == this.frm.doc.currency) ? this.frm.doc.paid_amount : this.frm.doc.base_paid_amount; - this.frm.doc.outstanding_amount = flt(total_amount_to_pay - flt(paid_amount) + flt(this.frm.doc.change_amount * this.frm.doc.conversion_rate), precision("outstanding_amount")); + console.log("set the outstanding amount"); } }, @@ -644,9 +646,14 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } else if(!this.frm.doc.is_return){ this.frm.doc.payments = []; } + if (this.frm.doc.redeem_loyalty_points && this.frm.doc.loyalty_amount) { + base_paid_amount += this.frm.doc.loyalty_amount; + paid_amount += flt(this.frm.doc.loyalty_amount / me.frm.doc.conversion_rate, precision("paid_amount")); + } this.frm.doc.paid_amount = flt(paid_amount, precision("paid_amount")); this.frm.doc.base_paid_amount = flt(base_paid_amount, precision("base_paid_amount")); + console.log("paid amount set as -> ", paid_amount, base_paid_amount); }, calculate_change_amount: function(){ diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 88e66ca668..5499ab0791 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -103,6 +103,11 @@ frappe.ui.form.on("Customer", { // indicator erpnext.utils.set_party_dashboard_indicators(frm); + // + if (frm.doc.__onload.dashboard_info.loyalty_point) { + frm.dashboard.add_indicator(__('Loyalty Point: {0}', [frm.doc.__onload.dashboard_info.loyalty_point]), 'blue'); + } + } else { frappe.contacts.clear_address_and_contact(frm); } diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 3fdf6cff39..5215854522 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -1470,6 +1470,97 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 1, + "columns": 0, + "fieldname": "column_break_38", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Loyalty Points", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "loyalty_program", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Loyalty Program", + "length": 0, + "no_copy": 0, + "options": "Loyalty Program", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "loyalty_program_tier", + "fieldtype": "Read Only", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Loyalty Program Tier", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, "collapsible_depends_on": "default_sales_partner", "columns": 0, "fieldname": "sales_team_section_break", @@ -1677,7 +1768,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-06-26 13:12:30.677834", + "modified": "2018-06-27 12:12:30.677834", "modified_by": "Administrator", "module": "Selling", "name": "Customer", diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index b5961a9ab8..1e63e592b1 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -24,6 +24,9 @@ class Customer(TransactionBase): def load_dashboard_info(self): info = get_dashboard_info(self.doctype, self.name) + loyalty_point_details = self.get_loyalty_points() + if loyalty_point_details and loyalty_point_details.get("loyalty_points"): + info["loyalty_point"] = loyalty_point_details.loyalty_points self.set_onload('dashboard_info', info) def autoname(self): @@ -33,6 +36,11 @@ class Customer(TransactionBase): else: set_name_by_naming_series(self) + def get_loyalty_points(self): + if self.loyalty_program: + from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_details + return get_loyalty_details(self.name, self.loyalty_program) + def get_customer_name(self): if frappe.db.get_value("Customer", self.customer_name): count = frappe.db.sql("""select ifnull(MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)), 0) from tabCustomer @@ -51,6 +59,7 @@ class Customer(TransactionBase): self.flags.old_lead = self.lead_name validate_party_accounts(self) self.validate_credit_limit_on_change() + self.set_loyalty_program() self.check_customer_group_change() def check_customer_group_change(self): @@ -178,6 +187,17 @@ class Customer(TransactionBase): if frappe.defaults.get_global_default('cust_master_name') == 'Customer Name': frappe.db.set(self, "customer_name", newdn) + def set_loyalty_program(self): + if not self.loyalty_program: + loyalty_programs = frappe.get_all("Loyalty Program", fields=["name", "customer_group", + "customer_territory"], filters={"auto_opt_in": 1, "disabled": 0}) + from frappe.desk.treeview import get_children + for loyalty_program in loyalty_programs: + customer_groups = get_children("Customer Group", loyalty_program.customer_group, ) + if self.customer_group in customer_groups and\ + self.territory in get_children("Territory", loyalty_program.customer_territory): + self.loyalty_program = loyalty_program.name + def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): if frappe.db.get_default("cust_master_name") == "Customer Name": diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index c34f26bf7e..9223adb470 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1910,6 +1910,99 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "loyalty_points_redemption", + "fieldtype": "Section Break", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Loyalty Points Redemption", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "loyalty_points", + "fieldtype": "Int", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Loyalty Points", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "loyalty_amount", + "fieldtype": "Currency", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Loyalty Amount", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 6fbe587593..ee6b1c7e2b 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -558,6 +558,10 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): if target.company_address: target.update(get_fetch_values("Sales Invoice", 'company_address', target.company_address)) + # set the redeem loyalty points if provided via shopping cart + if source.loyalty_points and source.order_type == "Shopping Cart": + target.redeem_loyalty_points = 1 + def update_item(source, target, source_parent): target.amount = flt(source.amount) - flt(source.billed_amt) target.base_amount = target.amount * flt(source_parent.conversion_rate) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 7893b007d5..294b2bad7c 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -95,7 +95,9 @@ erpnext.pos.PointOfSale = class PointOfSale { frm: this.frm, wrapper: this.wrapper.find('.cart-container'), events: { - on_customer_change: (customer) => this.frm.set_value('customer', customer), + on_customer_change: (customer) => { + this.frm.set_value('customer', customer); + }, on_field_change: (item_code, field, value, batch_no) => { this.update_item_in_cart(item_code, field, value, batch_no); }, @@ -119,6 +121,46 @@ erpnext.pos.PointOfSale = class PointOfSale { }, get_item_details: (item_code) => { return this.items.get(item_code); + }, + get_loyalty_details: () => { + var me = this; + if (this.frm.doc.customer) { + frappe.call({ + method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_program_details", + args: { + "customer": me.frm.doc.customer, + "till_date": me.frm.doc.posting_date, + "company": me.frm.doc.company, + "silent": true + }, + callback: function(r) { + if (r.message.loyalty_program && r.message.loyalty_points) { + me.cart.events.set_loyalty_details(r.message, true); + } + if (!r.message.loyalty_program) { + var loyalty_details = { + loyalty_points: 0, + loyalty_program: '', + expense_account: '', + cost_center: '' + } + me.cart.events.set_loyalty_details(loyalty_details, false); + } + } + }); + } + }, + set_loyalty_details: (details, view_status) => { + if (view_status) { + this.cart.available_loyalty_points.$wrapper.removeClass("hide"); + } else { + this.cart.available_loyalty_points.$wrapper.addClass("hide"); + } + this.cart.available_loyalty_points.set_value(details.loyalty_points); + this.cart.available_loyalty_points.refresh_input(); + this.frm.set_value("loyalty_program", details.loyalty_program); + this.frm.set_value("loyalty_redemption_account", details.expense_account); + this.frm.set_value("loyalty_redemption_cost_center", details.cost_center); } } }); @@ -563,6 +605,7 @@ class POSCart { make() { this.make_dom(); this.make_customer_field(); + this.make_loyalty_points(); this.make_numpad(); } @@ -598,16 +641,27 @@ class POSCart { -
+
+
+
+
+
`); + + this.$cart_items = this.wrapper.find('.cart-items'); this.$empty_state = this.wrapper.find('.cart-items .empty-state'); this.$taxes_and_totals = this.wrapper.find('.taxes-and-totals'); this.$discount_amount = this.wrapper.find('.discount-amount'); this.$grand_total = this.wrapper.find('.grand-total'); this.$qty_total = this.wrapper.find('.quantity-total'); + // this.$loyalty_button = this.wrapper.find('.loyalty-button'); + + // this.$loyalty_button.on('click', () => { + // this.loyalty_button.show(); + // }) this.toggle_taxes_and_totals(false); this.$grand_total.on('click', () => { @@ -765,6 +819,7 @@ class POSCart { }, onchange: () => { this.events.on_customer_change(this.customer_field.get_value()); + this.events.get_loyalty_details(); } }, parent: this.wrapper.find('.customer-field'), @@ -774,6 +829,21 @@ class POSCart { this.customer_field.set_value(this.frm.doc.customer); } + + make_loyalty_points() { + this.available_loyalty_points = frappe.ui.form.make_control({ + df: { + fieldtype: 'Int', + label: 'Available Loyalty Points', + read_only: 1, + fieldname: 'available_loyalty_points' + }, + parent: this.wrapper.find('.loyalty-program-field') + }); + this.available_loyalty_points.set_value(this.frm.doc.loyalty_points); + } + + disable_numpad_control() { let disabled_btns = []; if(!this.frm.allow_edit_rate) { @@ -1458,7 +1528,8 @@ class Payment { this.set_flag(); this.dialog = new frappe.ui.Dialog({ fields: this.get_fields(), - width: 800 + width: 800, + invoice_frm: this.frm }); this.set_title(); @@ -1541,6 +1612,44 @@ class Payment { fieldtype: 'HTML', fieldname: 'numpad' }, + { + fieldtype: 'Section Break', + depends_on: 'eval: this.invoice_frm.doc.loyalty_program' + }, + { + fieldtype: 'Check', + label: 'Redeem Loyalty Points', + fieldname: 'redeem_loyalty_points', + onchange: () => { + me.update_cur_frm_value("redeem_loyalty_points", () => { + frappe.flags.redeem_loyalty_points = false; + me.update_loyalty_points(); + }); + } + }, + { + fieldtype: 'Column Break', + }, + { + fieldtype: 'Int', + fieldname: "loyalty_points", + label: __("Loyalty Points"), + depends_on: "redeem_loyalty_points", + onchange: () => { + me.update_cur_frm_value("loyalty_points", () => { + frappe.flags.loyalty_points = false; + me.update_loyalty_points(); + }); + } + }, + { + fieldtype: 'Currency', + label: __("Loyalty Amount"), + fieldname: "loyalty_amount", + options: me.frm.doc.currency, + read_only: 1, + depends_on: "redeem_loyalty_points" + }, { fieldtype: 'Section Break', }, @@ -1603,6 +1712,9 @@ class Payment { set_flag() { frappe.flags.write_off_amount = true; frappe.flags.change_amount = true; + frappe.flags.loyalty_points = true; + frappe.flags.redeem_loyalty_points = true; + frappe.flags.payment_method = true; } update_cur_frm_value(fieldname, callback) { @@ -1619,15 +1731,15 @@ class Payment { update_payment_value(fieldname, value) { var me = this; - $.each(this.frm.doc.payments, function(i, data) { - if (__(data.mode_of_payment) == __(fieldname)) { - frappe.model.set_value('Sales Invoice Payment', data.name, 'amount', value) - .then(() => { - me.update_change_amount(); - me.update_write_off_amount(); - }); - } - }); + $.each(this.frm.doc.payments, function(i, data) { + if (__(data.mode_of_payment) == __(fieldname)) { + frappe.model.set_value('Sales Invoice Payment', data.name, 'amount', value) + .then(() => { + me.update_change_amount(); + me.update_write_off_amount(); + }); + } + }); } update_change_amount() { @@ -1643,4 +1755,22 @@ class Payment { this.dialog.set_value("paid_amount", this.frm.doc.paid_amount); this.dialog.set_value("outstanding_amount", this.frm.doc.outstanding_amount); } + + update_payment_amount() { + var me = this; + $.each(this.frm.doc.payments, function(i, data) { + console.log("setting the ", data.mode_of_payment, " for the value", data.amount); + me.dialog.set_value(data.mode_of_payment, data.amount); + }); + } + + update_loyalty_points() { + if (this.dialog.get_value("redeem_loyalty_points")) { + this.dialog.set_value("loyalty_points", this.frm.doc.loyalty_points); + this.dialog.set_value("loyalty_amount", this.frm.doc.loyalty_amount); + this.update_payment_amount(); + this.show_paid_amount(); + } + } + } diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 36836954f8..cd02e3003b 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -570,6 +570,7 @@ class TestDeliveryNote(unittest.TestCase): self.assertEqual(dn.name, dt.delivery_stops[0].delivery_note) def create_delivery_note(**args): + print (frappe.session.user) dn = frappe.new_doc("Delivery Note") args = frappe._dict(args) dn.posting_date = args.posting_date or nowdate() diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html index 3a6d2254fb..712eefd8cf 100644 --- a/erpnext/templates/pages/order.html +++ b/erpnext/templates/pages/order.html @@ -18,26 +18,26 @@ {% block page_content %}
-
- - {{ _(doc.indicator_title) or _(doc.status) or _("Submitted") }} - +
+ + {{ _(doc.indicator_title) or _(doc.status) or _("Submitted") }} +
-
- {{ frappe.utils.formatdate(doc.transaction_date, 'medium') }} +
+ {{ frappe.utils.formatdate(doc.transaction_date, 'medium') }} {% if doc.valid_till %}

- {{ _("Valid Till") }}: {{ frappe.utils.formatdate(doc.valid_till, 'medium') }} + {{ _("Valid Till") }}: {{ frappe.utils.formatdate(doc.valid_till, 'medium') }}

{% endif %} -
+

{% if doc.doctype == 'Supplier Quotation' %} - {{ doc.supplier_name}} + {{ doc.supplier_name}} {% else %} - {{ doc.customer_name}} + {{ doc.customer_name}} {% endif %} {% if doc.contact_display %}
@@ -51,64 +51,94 @@

- -
-
-
- {{ _("Item") }} -
-
- {{ _("Quantity") }} -
-
- {{ _("Amount") }} -
-
- {% for d in doc.items %} -
-
- {{ item_name_and_description(d) }} -
-
- {{ d.qty }} - {% if d.delivered_qty is defined and d.delivered_qty != None %} -

{{ - _("Delivered: {0}").format(d.delivered_qty) }}

- {% endif %} -
-
- {{ d.get_formatted("amount") }} -

{{ - _("@ {0}").format(d.get_formatted("rate")) }}

-
-
- {% endfor %} -
+ +
+
+
+ {{ _("Item") }} +
+
+ {{ _("Quantity") }} +
+
+ {{ _("Amount") }} +
+
+ {% for d in doc.items %} +
+
+ {{ item_name_and_description(d) }} +
+
+ {{ d.qty }} + {% if d.delivered_qty is defined and d.delivered_qty != None %} +

{{ + _("Delivered: {0}").format(d.delivered_qty) }}

+ {% endif %} +
+
+ {{ d.get_formatted("amount") }} +

{{ + _("@ {0}").format(d.get_formatted("rate")) }}

+
+
+ {% endfor %} +
- -
-
-
- {% include "erpnext/templates/includes/order/order_taxes.html" %} -
-
+ +
+
+
+ {% include "erpnext/templates/includes/order/order_taxes.html" %} +
+
-
-
-
- {% if enabled_checkout %} - {% if (doc.doctype=="Sales Order" and doc.per_billed <= 0) - or (doc.doctype=="Sales Invoice" and doc.outstanding_amount > 0) %} -
-

- {{ _("Pay") }} {{ doc.get_formatted("grand_total") }} -

+{% if enabled_checkout and ((doc.doctype=="Sales Order" and doc.per_billed <= 0) + or (doc.doctype=="Sales Invoice" and doc.outstanding_amount > 0)) %} + +
+
+
+
+ Payment
- {% endif %} - {% endif %} +
+
+
+
+
+ {% if available_loyalty_points %} +
+
Enter Loyalty Points
+
+
+ +
+ +
+
+ {% endif %} +
+ + + +
+ +
+
+
+{% endif %} + {% if attachments %}
@@ -131,7 +161,20 @@
{% if doc.terms %}
-

{{ doc.terms }}

+

{{ doc.terms }}

{% endif %} {% endblock %} + +{% block script %} + + +{% endblock %} \ No newline at end of file diff --git a/erpnext/templates/pages/order.js b/erpnext/templates/pages/order.js new file mode 100644 index 0000000000..21c3a14437 --- /dev/null +++ b/erpnext/templates/pages/order.js @@ -0,0 +1,40 @@ +// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ready(function(){ + + var loyalty_points_input = document.getElementById("loyalty-point-to-redeem"); + var loyalty_points_status = document.getElementById("loyalty-points-status"); + loyalty_points_input.onblur = apply_loyalty_points; + + function apply_loyalty_points() { + var loyalty_points = parseInt(loyalty_points_input.value); + if (loyalty_points) { + frappe.call({ + method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_redeemption_factor", + args: { + "customer": doc_info.customer + }, + callback: function(r) { + if (r) { + var message = "" + let loyalty_amount = flt(r.message*loyalty_points); + if (doc_info.grand_total && doc_info.grand_total < loyalty_amount) { + let redeemable_amount = parseInt(doc_info.grand_total/r.message); + message = "You can only redeem max " + redeemable_amount + " points in this order."; + frappe.msgprint(__(message)); + } else { + message = loyalty_points + " Loyalty Points of amount "+ loyalty_amount + " is applied." + frappe.msgprint(__(message)); + var remaining_amount = flt(doc_info.grand_total) - flt(loyalty_amount); + var payment_button = document.getElementById("pay-for-order"); + payment_button.innerHTML = __("Pay Remaining"); + payment_button.href = "/api/method/erpnext.accounts.doctype.payment_request.payment_request.make_payment_request?dn="+doc_info.doctype_name+"&dt="+doc_info.doctype+"&loyalty_points="+loyalty_points+"&submit_doc=1&order_type=Shopping Cart"; + } + loyalty_points_status.innerHTML = message; + } + } + }); + } + } +}) \ No newline at end of file diff --git a/erpnext/templates/pages/order.py b/erpnext/templates/pages/order.py index 2874047ee2..70bd702ba8 100644 --- a/erpnext/templates/pages/order.py +++ b/erpnext/templates/pages/order.py @@ -32,6 +32,13 @@ def get_context(context): if not frappe.has_website_permission(context.doc): frappe.throw(_("Not Permitted"), frappe.PermissionError) + + # check for the loyalty program of the customer + customer_loyalty_program = frappe.db.get_value("Customer", context.doc.customer, "loyalty_program") + if customer_loyalty_program: + from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details + loyalty_program_details = get_loyalty_program_details(context.doc.customer, customer_loyalty_program) + context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points")) def get_attachments(dt, dn): return frappe.get_all("File",