diff --git a/posawesome/__init__.py b/posawesome/__init__.py index dd31fd1e..d2ba3e27 100644 --- a/posawesome/__init__.py +++ b/posawesome/__init__.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import frappe -__version__ = "3.0.1" +__version__ = "3.1.0" def console(*data): diff --git a/posawesome/fixtures/custom_field.json b/posawesome/fixtures/custom_field.json index 629fa240..6f660367 100644 --- a/posawesome/fixtures/custom_field.json +++ b/posawesome/fixtures/custom_field.json @@ -809,6 +809,59 @@ "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, + "fieldname": "posa_use_percentage_discount", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "posa_allow_user_to_edit_additional_discount", + "label": "Use Percentage Discount", + "length": 0, + "mandatory_depends_on": "", + "modified": "2021-09-26 14:08:06.765185", + "name": "POS Profile-posa_use_percentage_discount", + "no_copy": 0, + "non_negative": 0, + "options": null, + "parent": null, + "parentfield": null, + "parenttype": null, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": "0", + "depends_on": "", + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "POS Profile", + "fetch_from": null, + "fetch_if_empty": 0, "fieldname": "posa_max_discount_allowed", "fieldtype": "Float", "hidden": 0, @@ -821,7 +874,7 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "posa_allow_user_to_edit_additional_discount", + "insert_after": "posa_use_percentage_discount", "label": "Max Discount Percentage Allowed ", "length": 0, "mandatory_depends_on": null, @@ -1646,19 +1699,19 @@ "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, - "collapsible": 0, + "collapsible": 1, "collapsible_depends_on": null, "columns": 0, "default": null, - "depends_on": "posa_show_template_items", + "depends_on": null, "description": null, "docstatus": 0, "doctype": "Custom Field", - "dt": "POS Profile", + "dt": "Company", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "posa_hide_variants_items", - "fieldtype": "Check", + "fieldname": "posa_referral_section", + "fieldtype": "Section Break", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -1669,12 +1722,12 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "posa_show_template_items", - "label": "Hide Variants Items", + "insert_after": "total_monthly_sales", + "label": "Referral Code", "length": 0, "mandatory_depends_on": null, - "modified": "2021-06-24 03:24:52.591942", - "name": "POS Profile-posa_hide_variants_items", + "modified": "2021-07-29 23:04:22.290849", + "name": "Company-posa_referral_section", "no_copy": 0, "non_negative": 0, "options": null, @@ -1699,19 +1752,19 @@ "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, - "collapsible": 1, + "collapsible": 0, "collapsible_depends_on": null, "columns": 0, "default": null, - "depends_on": null, + "depends_on": "posa_show_template_items", "description": null, "docstatus": 0, "doctype": "Custom Field", - "dt": "Company", + "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "posa_referral_section", - "fieldtype": "Section Break", + "fieldname": "posa_hide_variants_items", + "fieldtype": "Check", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -1722,12 +1775,12 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "total_monthly_sales", - "label": "Referral Code", + "insert_after": "posa_show_template_items", + "label": "Hide Variants Items", "length": 0, "mandatory_depends_on": null, - "modified": "2021-07-29 23:04:22.290849", - "name": "Company-posa_referral_section", + "modified": "2021-06-24 03:24:52.591942", + "name": "POS Profile-posa_hide_variants_items", "no_copy": 0, "non_negative": 0, "options": null, @@ -1760,10 +1813,10 @@ "description": null, "docstatus": 0, "doctype": "Custom Field", - "dt": "POS Profile", + "dt": "Company", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "posa_fetch_coupon", + "fieldname": "posa_auto_referral", "fieldtype": "Check", "hidden": 0, "hide_border": 0, @@ -1775,12 +1828,12 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "posa_hide_variants_items", - "label": "Auto Fetch Coupon Gifts", + "insert_after": "posa_referral_section", + "label": "Auto Create Referral For New Customers", "length": 0, "mandatory_depends_on": null, - "modified": "2021-07-29 22:58:10.372543", - "name": "POS Profile-posa_fetch_coupon", + "modified": "2021-07-29 23:07:08.681215", + "name": "Company-posa_auto_referral", "no_copy": 0, "non_negative": 0, "options": null, @@ -1813,10 +1866,10 @@ "description": null, "docstatus": 0, "doctype": "Custom Field", - "dt": "Company", + "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "posa_auto_referral", + "fieldname": "posa_fetch_coupon", "fieldtype": "Check", "hidden": 0, "hide_border": 0, @@ -1828,12 +1881,12 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "posa_referral_section", - "label": "Auto Create Referral For New Customers", + "insert_after": "posa_hide_variants_items", + "label": "Auto Fetch Coupon Gifts", "length": 0, "mandatory_depends_on": null, - "modified": "2021-07-29 23:07:08.681215", - "name": "Company-posa_auto_referral", + "modified": "2021-07-29 22:58:10.372543", + "name": "POS Profile-posa_fetch_coupon", "no_copy": 0, "non_negative": 0, "options": null, @@ -1858,7 +1911,7 @@ "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, - "collapsible": 1, + "collapsible": 0, "collapsible_depends_on": null, "columns": 0, "default": null, @@ -1866,11 +1919,11 @@ "description": null, "docstatus": 0, "doctype": "Custom Field", - "dt": "POS Profile", + "dt": "Company", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "posa_pos_awesome_advance_settings", - "fieldtype": "Section Break", + "fieldname": "posa_column_break_22", + "fieldtype": "Column Break", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -1881,12 +1934,12 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "posa_fetch_coupon", - "label": "POS Awesome Advance Settings", + "insert_after": "posa_auto_referral", + "label": "", "length": 0, "mandatory_depends_on": null, - "modified": "2020-10-11 15:13:10.899536", - "name": "POS Profile-posa_pos_awesome_advance_settings", + "modified": "2021-07-29 23:11:04.558635", + "name": "Company-posa_column_break_22", "no_copy": 0, "non_negative": 0, "options": null, @@ -1915,15 +1968,15 @@ "collapsible_depends_on": null, "columns": 0, "default": null, - "depends_on": null, + "depends_on": "posa_auto_referral", "description": null, "docstatus": 0, "doctype": "Custom Field", "dt": "Company", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "posa_column_break_22", - "fieldtype": "Column Break", + "fieldname": "posa_customer_offer", + "fieldtype": "Link", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -1934,15 +1987,15 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "posa_auto_referral", - "label": "", + "insert_after": "posa_column_break_22", + "label": "Final Customer Offer", "length": 0, - "mandatory_depends_on": null, - "modified": "2021-07-29 23:11:04.558635", - "name": "Company-posa_column_break_22", + "mandatory_depends_on": "posa_auto_referral", + "modified": "2021-07-29 23:11:04.891539", + "name": "Company-posa_customer_offer", "no_copy": 0, "non_negative": 0, - "options": null, + "options": "POS Offer", "parent": null, "parentfield": null, "parenttype": null, @@ -1967,15 +2020,15 @@ "collapsible": 0, "collapsible_depends_on": null, "columns": 0, - "default": null, + "default": "0", "depends_on": null, - "description": "Send invoice to submit after printing", + "description": null, "docstatus": 0, "doctype": "Custom Field", "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "posa_allow_submissions_in_background_job", + "fieldname": "posa_allow_customer_purchase_order", "fieldtype": "Check", "hidden": 0, "hide_border": 0, @@ -1987,12 +2040,12 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "posa_pos_awesome_advance_settings", - "label": "Allow Submissions in background job", + "insert_after": "posa_fetch_coupon", + "label": "Allow Customer Purchase Order", "length": 0, - "mandatory_depends_on": "0", - "modified": "2020-10-09 16:05:54.332880", - "name": "POS Profile-posa_allow_submissions_in_background_job", + "mandatory_depends_on": null, + "modified": "2021-12-16 16:27:32.300240", + "name": "POS Profile-posa_allow_customer_purchase_order", "no_copy": 0, "non_negative": 0, "options": null, @@ -2028,7 +2081,7 @@ "dt": "Company", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "posa_customer_offer", + "fieldname": "posa_primary_offer", "fieldtype": "Link", "hidden": 0, "hide_border": 0, @@ -2040,12 +2093,12 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "posa_column_break_22", - "label": "Final Customer Offer", + "insert_after": "posa_customer_offer", + "label": "Primary Customer Offer", "length": 0, - "mandatory_depends_on": "posa_auto_referral", - "modified": "2021-07-29 23:11:04.891539", - "name": "Company-posa_customer_offer", + "mandatory_depends_on": null, + "modified": "2021-07-29 23:11:05.290809", + "name": "Company-posa_primary_offer", "no_copy": 0, "non_negative": 0, "options": "POS Offer", @@ -2070,10 +2123,10 @@ "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, - "collapsible": 0, + "collapsible": 1, "collapsible_depends_on": null, "columns": 0, - "default": "1", + "default": null, "depends_on": null, "description": null, "docstatus": 0, @@ -2081,8 +2134,8 @@ "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "posa_local_storage", - "fieldtype": "Check", + "fieldname": "posa_pos_awesome_advance_settings", + "fieldtype": "Section Break", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -2093,12 +2146,12 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "posa_allow_submissions_in_background_job", - "label": "Use Browser Local Storage", + "insert_after": "posa_allow_print_last_invoice", + "label": "POS Awesome Advance Settings", "length": 0, "mandatory_depends_on": null, - "modified": "2020-11-13 22:14:13.683091", - "name": "POS Profile-posa_local_storage", + "modified": "2020-10-11 15:13:10.899536", + "name": "POS Profile-posa_pos_awesome_advance_settings", "no_copy": 0, "non_negative": 0, "options": null, @@ -2127,15 +2180,15 @@ "collapsible_depends_on": null, "columns": 0, "default": null, - "depends_on": "posa_auto_referral", + "depends_on": null, "description": null, "docstatus": 0, "doctype": "Custom Field", - "dt": "Company", + "dt": "Customer", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "posa_primary_offer", - "fieldtype": "Link", + "fieldname": "posa_birthday", + "fieldtype": "Date", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -2146,15 +2199,15 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "posa_customer_offer", - "label": "Primary Customer Offer", + "insert_after": "contact_html", + "label": "Birthday", "length": 0, "mandatory_depends_on": null, - "modified": "2021-07-29 23:11:05.290809", - "name": "Company-posa_primary_offer", + "modified": "2021-07-31 00:12:09.417519", + "name": "Customer-posa_birthday", "no_copy": 0, "non_negative": 0, - "options": "POS Offer", + "options": null, "parent": null, "parentfield": null, "parenttype": null, @@ -2179,16 +2232,16 @@ "collapsible": 0, "collapsible_depends_on": null, "columns": 0, - "default": null, - "depends_on": null, + "default": "", + "depends_on": "posa_auto_referral", "description": null, "docstatus": 0, "doctype": "Custom Field", - "dt": "Customer", + "dt": "Company", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "posa_birthday", - "fieldtype": "Date", + "fieldname": "posa_referral_campaign", + "fieldtype": "Link", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -2199,15 +2252,15 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "contact_html", - "label": "Birthday", + "insert_after": "posa_primary_offer", + "label": "Referral Campaign", "length": 0, "mandatory_depends_on": null, - "modified": "2021-07-31 00:12:09.417519", - "name": "Customer-posa_birthday", + "modified": "2021-07-29 23:11:05.723688", + "name": "Company-posa_referral_campaign", "no_copy": 0, "non_negative": 0, - "options": null, + "options": "Campaign", "parent": null, "parentfield": null, "parenttype": null, @@ -2234,13 +2287,13 @@ "columns": 0, "default": null, "depends_on": null, - "description": null, + "description": "Send invoice to submit after printing", "docstatus": 0, "doctype": "Custom Field", "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "posa_search_serial_no", + "fieldname": "posa_allow_submissions_in_background_job", "fieldtype": "Check", "hidden": 0, "hide_border": 0, @@ -2252,12 +2305,12 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "posa_local_storage", - "label": "Search by Serial Number", + "insert_after": "posa_pos_awesome_advance_settings", + "label": "Allow Submissions in background job", "length": 0, - "mandatory_depends_on": null, - "modified": "2021-06-20 20:47:47.966800", - "name": "POS Profile-posa_search_serial_no", + "mandatory_depends_on": "0", + "modified": "2020-10-09 16:05:54.332880", + "name": "POS Profile-posa_allow_submissions_in_background_job", "no_copy": 0, "non_negative": 0, "options": null, @@ -2338,16 +2391,16 @@ "collapsible": 0, "collapsible_depends_on": null, "columns": 0, - "default": "", - "depends_on": "posa_auto_referral", + "default": "1", + "depends_on": null, "description": null, "docstatus": 0, "doctype": "Custom Field", - "dt": "Company", + "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "posa_referral_campaign", - "fieldtype": "Link", + "fieldname": "posa_local_storage", + "fieldtype": "Check", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -2358,15 +2411,15 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "posa_primary_offer", - "label": "Referral Campaign", + "insert_after": "posa_allow_submissions_in_background_job", + "label": "Use Browser Local Storage", "length": 0, "mandatory_depends_on": null, - "modified": "2021-07-29 23:11:05.723688", - "name": "Company-posa_referral_campaign", + "modified": "2020-11-13 22:14:13.683091", + "name": "POS Profile-posa_local_storage", "no_copy": 0, "non_negative": 0, - "options": "Campaign", + "options": null, "parent": null, "parentfield": null, "parenttype": null, @@ -2444,7 +2497,7 @@ "collapsible": 0, "collapsible_depends_on": null, "columns": 0, - "default": "1", + "default": null, "depends_on": null, "description": null, "docstatus": 0, @@ -2452,7 +2505,7 @@ "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "posa_tax_inclusive", + "fieldname": "posa_search_serial_no", "fieldtype": "Check", "hidden": 0, "hide_border": 0, @@ -2464,12 +2517,12 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "posa_search_serial_no", - "label": "Tax Inclusive", + "insert_after": "posa_local_storage", + "label": "Search by Serial Number", "length": 0, "mandatory_depends_on": null, - "modified": "2021-09-06 16:33:33.398280", - "name": "POS Profile-posa_tax_inclusive", + "modified": "2021-06-20 20:47:47.966800", + "name": "POS Profile-posa_search_serial_no", "no_copy": 0, "non_negative": 0, "options": null, @@ -2543,6 +2596,59 @@ "unique": 0, "width": null }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": "1", + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "POS Profile", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "posa_tax_inclusive", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "posa_search_serial_no", + "label": "Tax Inclusive", + "length": 0, + "mandatory_depends_on": null, + "modified": "2021-09-06 16:33:33.398280", + "name": "POS Profile-posa_tax_inclusive", + "no_copy": 0, + "non_negative": 0, + "options": null, + "parent": null, + "parentfield": null, + "parenttype": null, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, { "allow_in_quick_entry": 0, "allow_on_submit": 0, diff --git a/posawesome/hooks.py b/posawesome/hooks.py index 9f315bd8..db6c4f82 100644 --- a/posawesome/hooks.py +++ b/posawesome/hooks.py @@ -217,6 +217,8 @@ "Sales Order-posa_coupons", "Sales Order Item-posa_row_id", "POS Profile-posa_tax_inclusive", + "POS Profile-posa_use_percentage_discount", + "POS Profile-posa_allow_customer_purchase_order", ), ] ], diff --git a/posawesome/posawesome/api/invoice.py b/posawesome/posawesome/api/invoice.py index f6d14f68..d3fcf70c 100644 --- a/posawesome/posawesome/api/invoice.py +++ b/posawesome/posawesome/api/invoice.py @@ -7,10 +7,9 @@ import frappe from frappe import _ from frappe.model.mapper import get_mapped_doc -from frappe.utils import flt +from frappe.utils import flt, add_days from posawesome.posawesome.doctype.pos_coupon.pos_coupon import update_coupon_code_count from posawesome.posawesome.api.posapp import get_company_domain -from sqlparse import filters def validate(doc, method): @@ -46,7 +45,7 @@ def add_loyalty_point(invoice_doc): "invoice_type": "Sales Invoice", "invoice": invoice_doc.name, "loyalty_points": original_offer.loyalty_points, - "expiry_date": invoice_doc.posting_date, + "expiry_date": add_days(invoice_doc.posting_date, 10000), "posting_date": invoice_doc.posting_date, "company": invoice_doc.company, } diff --git a/posawesome/posawesome/api/m_pesa.py b/posawesome/posawesome/api/m_pesa.py new file mode 100644 index 00000000..2a2d483f --- /dev/null +++ b/posawesome/posawesome/api/m_pesa.py @@ -0,0 +1,102 @@ +# Copyright (c) 2021, Youssef Restom and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, requests +from frappe import _ +from requests.auth import HTTPBasicAuth + + +def get_token(app_key, app_secret, base_url): + authenticate_uri = "/oauth/v1/generate?grant_type=client_credentials" + authenticate_url = "{0}{1}".format(base_url, authenticate_uri) + + r = requests.get(authenticate_url, auth=HTTPBasicAuth(app_key, app_secret)) + + return r.json()["access_token"] + + +@frappe.whitelist(allow_guest=True) +def confirmation(**kwargs): + try: + args = frappe._dict(kwargs) + doc = frappe.new_doc("Mpesa Payment Register") + doc.transactiontype = args.get("TransactionType") + doc.transid = args.get("TransID") + doc.transtime = args.get("TransTime") + doc.transamount = args.get("TransAmount") + doc.businessshortcode = args.get("BusinessShortCode") + doc.billrefnumber = args.get("BillRefNumber") + doc.invoicenumber = args.get("InvoiceNumber") + doc.orgaccountbalance = args.get("OrgAccountBalance") + doc.thirdpartytransid = args.get("ThirdPartyTransID") + doc.msisdn = args.get("MSISDN") + doc.firstname = args.get("FirstName") + doc.middlename = args.get("MiddleName") + doc.lastname = args.get("LastName") + doc.insert(ignore_permissions=True) + frappe.log_error("confirmation" + " " + str(args), "confirmation") + frappe.db.commit() + context = {"ResultCode": 0, "ResultDesc": "Accepted"} + return dict(context) + except Exception as e: + frappe.log_error(frappe.get_traceback(), str(e)) + context = {"ResultCode": 1, "ResultDesc": "Rejected"} + return dict(context) + + +@frappe.whitelist(allow_guest=True) +def validation(**kwargs): + args = frappe._dict(kwargs) + frappe.log_error("validation" + " " + str(args), "validation") + context = {"ResultCode": 0, "ResultDesc": "Accepted"} + return dict(context) + + +@frappe.whitelist() +def get_mpesa_mode_of_payment(company): + modes = frappe.get_all( + "Mpesa C2B Register URL", + filters={"company": company, "register_status": "Success"}, + fields=["mode_of_payment"], + ) + modes_of_payment = [] + for mode in modes: + if not mode.mode_of_payment in modes_of_payment: + modes_of_payment.append(mode.mode_of_payment) + return modes_of_payment + + +@frappe.whitelist() +def get_mpesa_draft_payments(company, mode_of_payment, mobile_no=None, full_name=None): + filters = {"company": company, "mode_of_payment": mode_of_payment, "docstatus": 0} + if mobile_no: + filters["msisdn"] = ["like", f"%{mobile_no}%"] + if full_name: + filters["full_name"] = ["like", f"%{full_name}%"] + + payments = frappe.get_all( + "Mpesa Payment Register", + filters=filters, + fields=[ + "name", + "msisdn as mobile_no", + "full_name", + "posting_date", + "transamount as amount", + "currency", + "mode_of_payment", + "company", + ], + ) + return payments + + +@frappe.whitelist() +def submit_mpesa_payment(mpesa_payment, customer): + doc = frappe.get_doc("Mpesa Payment Register", mpesa_payment) + doc.customer = customer + doc.submit_payment = 1 + doc.submit() + doc.reload() + return frappe.get_doc("Payment Entry", doc.payment_entry) diff --git a/posawesome/posawesome/api/payment_entry.py b/posawesome/posawesome/api/payment_entry.py new file mode 100644 index 00000000..dfe76ce5 --- /dev/null +++ b/posawesome/posawesome/api/payment_entry.py @@ -0,0 +1,124 @@ +# Copyright (c) 2021, Youssef Restom and contributors +# For license information, please see license.txt + +import frappe, erpnext +from frappe import _ +from frappe.utils import nowdate +from erpnext.accounts.party import get_party_account +from erpnext.accounts.utils import get_account_currency +from erpnext.accounts.doctype.journal_entry.journal_entry import ( + get_default_bank_cash_account, +) +from erpnext.setup.utils import get_exchange_rate +from erpnext.accounts.doctype.bank_account.bank_account import get_party_bank_account + + +def create_payment_entry( + company, + customer, + amount, + currency, + mode_of_payment, + reference_date=None, + reference_no=None, + posting_date=None, + submit=0, +): + # TODO : need to have a better way to handle currency + date = nowdate() if not posting_date else posting_date + party_type = "Customer" + party_account = get_party_account(party_type, customer, company) + party_account_currency = get_account_currency(party_account) + if party_account_currency != currency: + frappe.throw( + _( + "Currency is not correct, party account currency is {party_account_currency} and transaction currency is {currency}" + ).format(party_account_currency=party_account_currency, currency=currency) + ) + payment_type = "Receive" + + bank = get_bank_cash_account(company, mode_of_payment) + company_currency = frappe.get_value("Company", company, "default_currency") + conversion_rate = get_exchange_rate(currency, company_currency, date, "for_selling") + paid_amount, received_amount = set_paid_amount_and_received_amount( + party_account_currency, bank, amount, payment_type, None, conversion_rate + ) + + pe = frappe.new_doc("Payment Entry") + pe.payment_type = payment_type + pe.company = company + pe.cost_center = erpnext.get_default_cost_center(company) + pe.posting_date = date + pe.mode_of_payment = mode_of_payment + pe.party_type = party_type + pe.party = customer + + pe.paid_from = party_account if payment_type == "Receive" else bank.account + pe.paid_to = party_account if payment_type == "Pay" else bank.account + pe.paid_from_account_currency = ( + party_account_currency if payment_type == "Receive" else bank.account_currency + ) + pe.paid_to_account_currency = ( + party_account_currency if payment_type == "Pay" else bank.account_currency + ) + pe.paid_amount = paid_amount + pe.received_amount = received_amount + pe.letter_head = frappe.get_value("Company", company, "default_letter_head") + pe.reference_date = reference_date + pe.reference_no = reference_no + if pe.party_type in ["Customer", "Supplier"]: + bank_account = get_party_bank_account(pe.party_type, pe.party) + pe.set("bank_account", bank_account) + pe.set_bank_account_data() + + pe.setup_party_account_field() + pe.set_missing_values() + + if party_account and bank: + pe.set_amounts() + if submit: + pe.docstatus = 1 + pe.insert(ignore_permissions=True) + return pe + + +def get_bank_cash_account(company, mode_of_payment, bank_account=None): + bank = get_default_bank_cash_account( + company, "Bank", mode_of_payment=mode_of_payment, account=bank_account + ) + + if not bank: + bank = get_default_bank_cash_account( + company, "Cash", mode_of_payment=mode_of_payment, account=bank_account + ) + + return bank + + +def set_paid_amount_and_received_amount( + party_account_currency, + bank, + outstanding_amount, + payment_type, + bank_amount, + conversion_rate, +): + paid_amount = received_amount = 0 + if party_account_currency == bank.account_currency: + paid_amount = received_amount = abs(outstanding_amount) + elif payment_type == "Receive": + paid_amount = abs(outstanding_amount) + if bank_amount: + received_amount = bank_amount + else: + received_amount = paid_amount * conversion_rate + + else: + received_amount = abs(outstanding_amount) + if bank_amount: + paid_amount = bank_amount + else: + # if party account currency and bank currency is different then populate paid amount as well + paid_amount = received_amount * conversion_rate + + return paid_amount, received_amount diff --git a/posawesome/posawesome/api/posapp.py b/posawesome/posawesome/api/posapp.py index 4a6213e8..dc7bbc3e 100644 --- a/posawesome/posawesome/api/posapp.py +++ b/posawesome/posawesome/api/posapp.py @@ -161,17 +161,29 @@ def get_items(pos_profile, price_list=None): items = [d.item_code for d in items_data] item_prices_data = frappe.get_all( "Item Price", - fields=["item_code", "price_list_rate", "currency"], - filters={"price_list": price_list, "item_code": ["in", items]}, + fields=["item_code", "price_list_rate", "currency", "uom"], + filters={ + "price_list": price_list, + "item_code": ["in", items], + "currency": pos_profile.get("currency"), + "selling": 1, + }, ) item_prices = {} for d in item_prices_data: - item_prices[d.item_code] = d + item_prices.setdefault(d.item_code, {}) + item_prices[d.item_code][d.get("uom") or "None"] = d for item in items_data: item_code = item.item_code - item_price = item_prices.get(item_code) or {} + item_price = {} + if item_prices.get(item_code): + item_price = ( + item_prices.get(item_code).get(item.stock_uom) + or item_prices.get(item_code).get("None") + or {} + ) item_barcode = frappe.get_all( "Item Barcode", filters={"parent": item_code}, @@ -301,7 +313,7 @@ def get_customer_names(pos_profile): condition += get_customer_group_condition(pos_profile) customers = frappe.db.sql( """ - SELECT name, mobile_no, email_id, tax_id, customer_name + SELECT name, mobile_no, email_id, tax_id, customer_name, primary_address FROM `tabCustomer` WHERE {0} ORDER by name @@ -322,9 +334,16 @@ def update_invoice(data): invoice_doc.update(data) else: invoice_doc = frappe.get_doc(data) + invoice_doc.flags.ignore_permissions = True frappe.flags.ignore_account_permission = True invoice_doc.set_missing_values() + + if invoice_doc.is_return and invoice_doc.return_against: + ref_doc = frappe.get_doc(invoice_doc.doctype, invoice_doc.return_against) + if not ref_doc.update_stock: + invoice_doc.update_stock = 0 + for item in invoice_doc.items: add_taxes_from_tax_template(item, invoice_doc) if frappe.get_value("POS Profile", invoice_doc.pos_profile, "posa_tax_inclusive"): @@ -741,10 +760,22 @@ def get_items_details(pos_profile, items_data): @frappe.whitelist() -def get_item_detail(data, doc=None): - item_code = json.loads(data).get("item_code") +def get_item_detail(item, doc=None, warehouse=None, price_list=None): + item = json.loads(item) + item_code = item.get("item_code") + if warehouse and item.get("has_batch_no") and not item.get("batch_no"): + item["batch_no"] = get_batch_no( + item_code, warehouse, item.get("qty"), False, item.get("d") + ) + item["selling_price_list"] = price_list max_discount = frappe.get_value("Item", item_code, "max_discount") - res = get_item_details(data, doc, overwrite_warehouse=False) + res = get_item_details( + item, + doc, + overwrite_warehouse=False, + ) + if item.get("is_stock_item") and warehouse: + res["actual_qty"] = get_stock_availability(item_code, warehouse) res["max_discount"] = max_discount return res @@ -827,10 +858,25 @@ def get_items_from_barcode(selling_price_list, currency, barcode): if item_list[0]: item = item_list[0] + filters = {"price_list": selling_price_list, "item_code": item_code} + prices_with_uom = frappe.db.count( + "Item Price", + filters={ + "price_list": selling_price_list, + "item_code": item_code, + "uom": item.stock_uom, + }, + ) + + if prices_with_uom > 0: + filters["uom"] = item.stock_uom + else: + filters["uom"] = ["in", ["", None, item.stock_uom]] + item_prices_data = frappe.get_all( "Item Price", fields=["item_code", "price_list_rate", "currency"], - filters={"price_list": selling_price_list, "item_code": item_code}, + filters=filters, ) item_price = 0 @@ -1025,52 +1071,65 @@ def make_address(args): return address - def build_item_cache(item_code): - parent_item_code = item_code - - attributes = [a.attribute for a in frappe.db.get_all('Item Variant Attribute', - {'parent': parent_item_code}, ['attribute'], order_by='idx asc') - ] + parent_item_code = item_code + + attributes = [ + a.attribute + for a in frappe.db.get_all( + "Item Variant Attribute", + {"parent": parent_item_code}, + ["attribute"], + order_by="idx asc", + ) + ] - item_variants_data = frappe.db.get_all('Item Variant Attribute', - {'variant_of': parent_item_code}, ['parent', 'attribute', 'attribute_value'], - order_by='name', - as_list=1 - ) + item_variants_data = frappe.db.get_all( + "Item Variant Attribute", + {"variant_of": parent_item_code}, + ["parent", "attribute", "attribute_value"], + order_by="name", + as_list=1, + ) - disabled_items = set([i.name for i in frappe.db.get_all('Item', {'disabled': 1})]) + disabled_items = set([i.name for i in frappe.db.get_all("Item", {"disabled": 1})]) - attribute_value_item_map = frappe._dict({}) - item_attribute_value_map = frappe._dict({}) + attribute_value_item_map = frappe._dict({}) + item_attribute_value_map = frappe._dict({}) - item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items] - for row in item_variants_data: - item_code, attribute, attribute_value = row - # (attr, value) => [item1, item2] - attribute_value_item_map.setdefault((attribute, attribute_value), []).append(item_code) - # item => {attr1: value1, attr2: value2} - item_attribute_value_map.setdefault(item_code, {})[attribute] = attribute_value + item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items] + for row in item_variants_data: + item_code, attribute, attribute_value = row + # (attr, value) => [item1, item2] + attribute_value_item_map.setdefault((attribute, attribute_value), []).append( + item_code + ) + # item => {attr1: value1, attr2: value2} + item_attribute_value_map.setdefault(item_code, {})[attribute] = attribute_value - optional_attributes = set() - for item_code, attr_dict in item_attribute_value_map.items(): - for attribute in attributes: - if attribute not in attr_dict: - optional_attributes.add(attribute) + optional_attributes = set() + for item_code, attr_dict in item_attribute_value_map.items(): + for attribute in attributes: + if attribute not in attr_dict: + optional_attributes.add(attribute) - frappe.cache().hset('attribute_value_item_map', parent_item_code, attribute_value_item_map) - frappe.cache().hset('item_attribute_value_map', parent_item_code, item_attribute_value_map) - frappe.cache().hset('item_variants_data', parent_item_code, item_variants_data) - frappe.cache().hset('optional_attributes', parent_item_code, optional_attributes) + frappe.cache().hset( + "attribute_value_item_map", parent_item_code, attribute_value_item_map + ) + frappe.cache().hset( + "item_attribute_value_map", parent_item_code, item_attribute_value_map + ) + frappe.cache().hset("item_variants_data", parent_item_code, item_variants_data) + frappe.cache().hset("optional_attributes", parent_item_code, optional_attributes) def get_item_optional_attributes(item_code): - val = frappe.cache().hget('optional_attributes', item_code) + val = frappe.cache().hget("optional_attributes", item_code) - if not val: - build_item_cache(item_code) + if not val: + build_item_cache(item_code) - return frappe.cache().hget('optional_attributes', item_code) + return frappe.cache().hget("optional_attributes", item_code) @frappe.whitelist() @@ -1331,7 +1390,10 @@ def get_customer_info(customer): if customer.loyalty_program: lp_details = get_loyalty_program_details_with_points( - customer.name, customer.loyalty_program, silent=True + customer.name, + customer.loyalty_program, + silent=True, + include_expired_entry=False, ) res["loyalty_points"] = lp_details.get("loyalty_points") res["conversion_factor"] = lp_details.get("conversion_factor") diff --git a/posawesome/posawesome/doctype/mpesa_c2b_register_url/__init__.py b/posawesome/posawesome/doctype/mpesa_c2b_register_url/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/posawesome/posawesome/doctype/mpesa_c2b_register_url/mpesa_c2b_register_url.js b/posawesome/posawesome/doctype/mpesa_c2b_register_url/mpesa_c2b_register_url.js new file mode 100644 index 00000000..5d2918fb --- /dev/null +++ b/posawesome/posawesome/doctype/mpesa_c2b_register_url/mpesa_c2b_register_url.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Youssef Restom and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Mpesa C2B Register URL', { + // refresh: function(frm) { + + // } +}); diff --git a/posawesome/posawesome/doctype/mpesa_c2b_register_url/mpesa_c2b_register_url.json b/posawesome/posawesome/doctype/mpesa_c2b_register_url/mpesa_c2b_register_url.json new file mode 100644 index 00000000..4a815dd6 --- /dev/null +++ b/posawesome/posawesome/doctype/mpesa_c2b_register_url/mpesa_c2b_register_url.json @@ -0,0 +1,128 @@ +{ + "actions": [], + "autoname": "field:mpesa_settings", + "creation": "2021-11-10 03:59:38.274817", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "mpesa_settings", + "till_number", + "business_shortcode", + "column_break_4", + "company", + "mode_of_payment", + "register_status" + ], + "fields": [ + { + "fieldname": "mpesa_settings", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Mpesa Settings", + "options": "Mpesa Settings", + "reqd": 1, + "unique": 1 + }, + { + "fetch_from": "mpesa_settings.till_number", + "fieldname": "till_number", + "fieldtype": "Data", + "label": "Till Number", + "read_only": 1 + }, + { + "fetch_from": "mpesa_settings.business_shortcode", + "fieldname": "business_shortcode", + "fieldtype": "Data", + "label": "Business Shortcode", + "read_only": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "mode_of_payment", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Mode of Payment", + "options": "Mode of Payment", + "reqd": 1 + }, + { + "default": "Pending", + "fieldname": "register_status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Register Status", + "options": "Pending\nSuccess\nFailed", + "read_only": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-11-18 05:08:03.509002", + "modified_by": "Administrator", + "module": "POSAwesome", + "name": "Mpesa C2B Register URL", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/posawesome/posawesome/doctype/mpesa_c2b_register_url/mpesa_c2b_register_url.py b/posawesome/posawesome/doctype/mpesa_c2b_register_url/mpesa_c2b_register_url.py new file mode 100644 index 00000000..867df6ec --- /dev/null +++ b/posawesome/posawesome/doctype/mpesa_c2b_register_url/mpesa_c2b_register_url.py @@ -0,0 +1,55 @@ +# Copyright (c) 2021, Youssef Restom and contributors +# For license information, please see license.txt + +import frappe, requests +from frappe.model.document import Document +from frappe.utils import get_request_site_address +from posawesome.posawesome.api.m_pesa import get_token + + +class MpesaC2BRegisterURL(Document): + def validate(self): + sandbox_url = "https://sandbox.safaricom.co.ke" + live_url = "https://api.safaricom.co.ke" + mpesa_settings = frappe.get_doc("Mpesa Settings", self.mpesa_settings) + env = "production" if not mpesa_settings.sandbox else "sandbox" + business_shortcode = ( + mpesa_settings.business_shortcode + if env == "production" + else mpesa_settings.till_number + ) + if env == "sandbox": + base_url = sandbox_url + else: + base_url = live_url + token = get_token( + app_key=mpesa_settings.consumer_key, + app_secret=mpesa_settings.get_password("consumer_secret"), + base_url=base_url, + ) + site_url = get_request_site_address(True) + validation_url = ( + site_url + "/api/method/posawesome.posawesome.api.m_pesa.validation" + ) + confirmation_url = ( + site_url + "/api/method/posawesome.posawesome.api.m_pesa.confirmation" + ) + register_url = base_url + "/mpesa/c2b/v1/registerurl" + + payload = { + "ShortCode": business_shortcode, + "ResponseType": "Completed", + "ConfirmationURL": validation_url, + "ValidationURL": confirmation_url, + } + headers = { + "Authorization": "Bearer {0}".format(token), + "Content-Type": "application/json", + } + r = requests.post(register_url, headers=headers, json=payload) + res = r.json() + if res.get("ResponseDescription") == "Success": + self.register_status = "Success" + else: + self.register_status = "Failed" + frappe.msgprint(str(res)) diff --git a/posawesome/posawesome/doctype/mpesa_c2b_register_url/test_mpesa_c2b_register_url.py b/posawesome/posawesome/doctype/mpesa_c2b_register_url/test_mpesa_c2b_register_url.py new file mode 100644 index 00000000..ae903c73 --- /dev/null +++ b/posawesome/posawesome/doctype/mpesa_c2b_register_url/test_mpesa_c2b_register_url.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Youssef Restom and Contributors +# See license.txt + +# import frappe +import unittest + +class TestMpesaC2BRegisterURL(unittest.TestCase): + pass diff --git a/posawesome/posawesome/doctype/mpesa_payment_register/__init__.py b/posawesome/posawesome/doctype/mpesa_payment_register/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/posawesome/posawesome/doctype/mpesa_payment_register/mpesa_payment_register.js b/posawesome/posawesome/doctype/mpesa_payment_register/mpesa_payment_register.js new file mode 100644 index 00000000..a43b92a2 --- /dev/null +++ b/posawesome/posawesome/doctype/mpesa_payment_register/mpesa_payment_register.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Youssef Restom and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Mpesa Payment Register', { + // refresh: function(frm) { + + // } +}); diff --git a/posawesome/posawesome/doctype/mpesa_payment_register/mpesa_payment_register.json b/posawesome/posawesome/doctype/mpesa_payment_register/mpesa_payment_register.json new file mode 100644 index 00000000..7eda9796 --- /dev/null +++ b/posawesome/posawesome/doctype/mpesa_payment_register/mpesa_payment_register.json @@ -0,0 +1,285 @@ +{ + "actions": [], + "allow_copy": 1, + "autoname": "MPRE.-.YY.-.MM.-.######", + "creation": "2021-11-10 03:47:20.144127", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "full_name", + "transactiontype", + "transid", + "transtime", + "transamount", + "businessshortcode", + "billrefnumber", + "invoicenumber", + "orgaccountbalance", + "thirdpartytransid", + "msisdn", + "firstname", + "middlename", + "lastname", + "column_break_14", + "posting_date", + "company", + "default_currency", + "customer", + "mode_of_payment", + "currency", + "submit_payment", + "payment_entry", + "amended_from" + ], + "fields": [ + { + "fieldname": "transactiontype", + "fieldtype": "Data", + "label": "Transaction Type", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "transid", + "fieldtype": "Data", + "label": "Trans ID", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "transtime", + "fieldtype": "Data", + "label": "Trans Time", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "transamount", + "fieldtype": "Float", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "Trans Amount", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "businessshortcode", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Business Short Code", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "billrefnumber", + "fieldtype": "Data", + "label": "Bill Ref Number", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "invoicenumber", + "fieldtype": "Data", + "label": "Invoice Number", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "orgaccountbalance", + "fieldtype": "Data", + "label": "Org Account Balance", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "thirdpartytransid", + "fieldtype": "Data", + "label": "Third Party Trans ID", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "msisdn", + "fieldtype": "Data", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "MSISDN", + "no_copy": 1, + "options": "Phone", + "read_only": 1 + }, + { + "fieldname": "firstname", + "fieldtype": "Data", + "in_preview": 1, + "in_standard_filter": 1, + "label": "First Name", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "middlename", + "fieldtype": "Data", + "label": "Middle Name", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "lastname", + "fieldtype": "Data", + "in_preview": 1, + "in_standard_filter": 1, + "label": "Last Name", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "fieldname": "customer", + "fieldtype": "Link", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "Customer", + "options": "Customer" + }, + { + "default": "Now", + "fieldname": "posting_date", + "fieldtype": "Datetime", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "Posting Date", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "payment_entry", + "fieldtype": "Link", + "label": "Payment Entry", + "options": "Payment Entry", + "read_only": 1 + }, + { + "fetch_from": "company.default_currency", + "fieldname": "default_currency", + "fieldtype": "Data", + "label": "Default Currency", + "read_only": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Mpesa Payment Register", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "mode_of_payment", + "fieldtype": "Link", + "label": "Mode of Payment", + "options": "Mode of Payment" + }, + { + "default": "KES", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "read_only": 1 + }, + { + "fieldname": "full_name", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Full Name", + "no_copy": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "submit_payment", + "fieldtype": "Check", + "label": "Submit Payment " + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2021-11-19 23:05:24.242365", + "modified_by": "Administrator", + "module": "POSAwesome", + "name": "Mpesa Payment Register", + "owner": "Administrator", + "permissions": [ + { + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "show_preview_popup": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "customer", + "track_changes": 1 +} \ No newline at end of file diff --git a/posawesome/posawesome/doctype/mpesa_payment_register/mpesa_payment_register.py b/posawesome/posawesome/doctype/mpesa_payment_register/mpesa_payment_register.py new file mode 100644 index 00000000..9295e3bf --- /dev/null +++ b/posawesome/posawesome/doctype/mpesa_payment_register/mpesa_payment_register.py @@ -0,0 +1,60 @@ +# Copyright (c) 2021, Youssef Restom and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from numpy import full +from posawesome.posawesome.api.payment_entry import create_payment_entry + + +class MpesaPaymentRegister(Document): + def before_insert(self): + self.set_missing_values() + + def set_missing_values(self): + self.currency = "KES" + self.full_name = "" + if self.firstname: + self.full_name = self.firstname + if self.middlename: + self.full_name += " " + self.middlename + if self.lastname: + self.full_name += " " + self.lastname + + register_url_list = frappe.get_all( + "Mpesa C2B Register URL", + filters={ + "business_shortcode": self.businessshortcode, + "register_status": "Success", + }, + fields=["company", "mode_of_payment"], + ) + if len(register_url_list) > 0: + self.company = register_url_list[0].company + self.mode_of_payment = register_url_list[0].mode_of_payment + + def before_submit(self): + if not self.transamount: + frappe.throw(_("Trans Amount is required")) + if not self.company: + frappe.throw(_("Company is required")) + if not self.customer: + frappe.throw(_("Customer is required")) + if not self.mode_of_payment: + frappe.throw(_("Mode of Payment is required")) + self.payment_entry = self.create_payment_entry() + + def create_payment_entry(self): + payment_entry = create_payment_entry( + self.company, + self.customer, + self.transamount, + self.currency, + self.mode_of_payment, + self.posting_date, + self.transid, + self.posting_date, + self.submit_payment, + ) + return payment_entry.name diff --git a/posawesome/posawesome/doctype/mpesa_payment_register/test_mpesa_payment_register.py b/posawesome/posawesome/doctype/mpesa_payment_register/test_mpesa_payment_register.py new file mode 100644 index 00000000..218b9a0a --- /dev/null +++ b/posawesome/posawesome/doctype/mpesa_payment_register/test_mpesa_payment_register.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Youssef Restom and Contributors +# See license.txt + +# import frappe +import unittest + +class TestMpesaPaymentRegister(unittest.TestCase): + pass diff --git a/posawesome/posawesome/workspace/mpesa_payments/mpesa_payments.json b/posawesome/posawesome/workspace/mpesa_payments/mpesa_payments.json new file mode 100644 index 00000000..205bb1d6 --- /dev/null +++ b/posawesome/posawesome/workspace/mpesa_payments/mpesa_payments.json @@ -0,0 +1,32 @@ +{ + "category": "Modules", + "charts": [], + "creation": "2021-11-18 05:04:56.066242", + "developer_mode_only": 0, + "disable_user_customization": 0, + "docstatus": 0, + "doctype": "Workspace", + "extends": "Accounting", + "extends_another_page": 1, + "hide_custom": 0, + "idx": 0, + "is_default": 0, + "is_standard": 1, + "label": "Mpesa Payments", + "links": [], + "modified": "2021-11-18 05:04:56.066242", + "modified_by": "Administrator", + "module": "POSAwesome", + "name": "Mpesa Payments", + "owner": "Administrator", + "pin_to_bottom": 0, + "pin_to_top": 0, + "shortcuts": [ + { + "doc_view": "", + "label": "Mpesa Payments", + "link_to": "Mpesa Payment Register", + "type": "DocType" + } + ] +} \ No newline at end of file diff --git a/posawesome/posawesome/workspace/mpesa_url/mpesa_url.json b/posawesome/posawesome/workspace/mpesa_url/mpesa_url.json new file mode 100644 index 00000000..b9168c3f --- /dev/null +++ b/posawesome/posawesome/workspace/mpesa_url/mpesa_url.json @@ -0,0 +1,32 @@ +{ + "category": "Modules", + "charts": [], + "creation": "2021-11-18 05:05:42.321915", + "developer_mode_only": 0, + "disable_user_customization": 0, + "docstatus": 0, + "doctype": "Workspace", + "extends": "Integrations", + "extends_another_page": 1, + "hide_custom": 0, + "idx": 0, + "is_default": 0, + "is_standard": 1, + "label": "Mpesa URL", + "links": [], + "modified": "2021-11-18 05:07:06.364132", + "modified_by": "Administrator", + "module": "POSAwesome", + "name": "Mpesa URL", + "owner": "Administrator", + "pin_to_bottom": 0, + "pin_to_top": 0, + "shortcuts": [ + { + "doc_view": "", + "label": "Mpesa C2B Register URL", + "link_to": "Mpesa C2B Register URL", + "type": "DocType" + } + ] +} \ No newline at end of file diff --git a/posawesome/public/js/posapp/components/Navbar.vue b/posawesome/public/js/posapp/components/Navbar.vue index f4a996de..368e3008 100644 --- a/posawesome/public/js/posapp/components/Navbar.vue +++ b/posawesome/public/js/posapp/components/Navbar.vue @@ -39,6 +39,22 @@ }} + + + mdi-printer + + + {{ + __('Print Last Invoice') + }} + + @@ -137,6 +153,7 @@ export default { freeze: false, freezeTitle: '', freezeMsg: '', + last_invoice: '', }; }, methods: { @@ -176,6 +193,30 @@ export default { }, }); }, + print_last_invoice() { + if (!this.last_invoice) return; + const print_format = + this.pos_profile.print_format_for_online || + this.pos_profile.print_format; + const letter_head = this.pos_profile.letter_head || 0; + const url = + frappe.urllib.get_base_url() + + '/printview?doctype=Sales%20Invoice&name=' + + this.last_invoice + + '&trigger_print=1' + + '&format=' + + print_format + + '&no_letterhead=' + + letter_head; + const printWindow = window.open(url, 'Print'); + printWindow.addEventListener( + 'load', + function () { + printWindow.print(); + }, + true + ); + }, }, created: function () { this.$nextTick(function () { @@ -191,6 +232,9 @@ export default { evntBus.$on('register_pos_profile', (data) => { this.pos_profile = data.pos_profile; }); + evntBus.$on('set_last_invoice', (data) => { + this.last_invoice = data; + }); evntBus.$on('freeze', (data) => { this.freeze = true; this.freezeTitle = data.title; diff --git a/posawesome/public/js/posapp/components/pos/Customer.vue b/posawesome/public/js/posapp/components/pos/Customer.vue index 049faa16..63b89127 100644 --- a/posawesome/public/js/posapp/components/pos/Customer.vue +++ b/posawesome/public/js/posapp/components/pos/Customer.vue @@ -44,6 +44,10 @@ v-if="data.item.mobile_no" v-html="`Mobile No: ${data.item.mobile_no}`" > + diff --git a/posawesome/public/js/posapp/components/pos/Invoice.vue b/posawesome/public/js/posapp/components/pos/Invoice.vue index 0e8b244a..fac6a5d5 100644 --- a/posawesome/public/js/posapp/components/pos/Invoice.vue +++ b/posawesome/public/js/posapp/components/pos/Invoice.vue @@ -491,7 +491,11 @@ hide-details > - + + + + { if (item.serial_no) { item.serial_no_selected = []; @@ -909,6 +940,9 @@ export default { doc.items = this.get_invoice_items(); doc.total = this.subtotal; doc.discount_amount = flt(this.discount_amount); + doc.additional_discount_percentage = flt( + this.additional_discount_percentage + ); doc.posa_pos_opening_shift = this.pos_opening_shift.name; doc.payments = this.get_payments(); doc.taxes = []; @@ -1014,15 +1048,16 @@ export default { validate() { let value = true; this.items.forEach((item) => { - if ( - this.pos_profile.update_stock && - this.stock_settings.allow_negative_stock != 1 - ) { - if (item.is_stock_item && item.stock_qty > item.actual_qty) { + if (this.stock_settings.allow_negative_stock != 1) { + if ( + (item.is_stock_item && item.stock_qty && !item.actual_qty) || + (item.is_stock_item && item.stock_qty > item.actual_qty) + ) { evntBus.$emit('show_mesage', { - text: __(`The existing quantity of item {0} is not enough`, [ - item.item_name, - ]), + text: __( + `The existing quantity '{0}' for item '{1}' is not enough`, + [item.actual_qty, item.item_name] + ), color: 'error', }); value = false; @@ -1043,8 +1078,9 @@ export default { } if (item.has_serial_no) { if ( - !item.serial_no_selected || - item.stock_qty != item.serial_no_selected.length + !this.invoice_doc.is_return && + (!item.serial_no_selected || + item.stock_qty != item.serial_no_selected.length) ) { evntBus.$emit('show_mesage', { text: __(`Selected serial numbers of item {0} is incorrect`, [ @@ -1055,7 +1091,7 @@ export default { value = false; } } - if (!this.pos_profile.posa_auto_set_batch && item.has_batch_no) { + if (item.has_batch_no) { if (item.stock_qty > item.actual_batch_qty) { evntBus.$emit('show_mesage', { text: __( @@ -1190,8 +1226,10 @@ export default { frappe.call({ method: 'posawesome.posawesome.api.posapp.get_item_detail', args: { + warehouse: this.pos_profile.warehouse, doc: this.get_invoice_doc(), - data: { + price_list: this.pos_profile.price_list, + item: { item_code: item.item_code, customer: this.customer, doctype: 'Sales Invoice', @@ -1211,11 +1249,24 @@ export default { transaction_type: 'selling', update_stock: this.pos_profile.update_stock, price_list: this.get_price_list(), + has_batch_no: item.has_batch_no, + serial_no: item.serial_no, + batch_no: item.batch_no, + is_stock_item: item.is_stock_item, }, }, callback: function (r) { if (r.message) { const data = r.message; + if ( + item.has_batch_no && + vm.pos_profile.posa_auto_set_batch && + !item.batch_no && + data.batch_no + ) { + item.batch_no = data.batch_no; + vm.set_batch_qty(item, item.batch_no, false); + } if (data.has_pricing_rule) { } else if ( vm.pos_profile.posa_apply_customer_discount && @@ -1308,6 +1359,15 @@ export default { } evntBus.$emit('update_customer_price_list', price_list); }, + update_discount_umount() { + const value = flt(this.additional_discount_percentage); + if (value >= -100 && value <= 100) { + this.discount_amount = (this.Total * value) / 100; + } else { + this.additional_discount_percentage = 0; + this.discount_amount = 0; + } + }, calc_prices(item, value, $event) { if (event.target.id === 'rate') { @@ -1400,7 +1460,7 @@ export default { } }, - set_batch_qty(item, value) { + set_batch_qty(item, value, update = true) { const batch_no = item.batch_no_data.find( (element) => element.batch_no == value ); @@ -1410,7 +1470,7 @@ export default { item.btach_price = batch_no.btach_price; item.price_list_rate = batch_no.btach_price; item.rate = batch_no.btach_price; - } else { + } else if (update) { item.btach_price = null; this.update_item_detail(item); } @@ -2205,6 +2265,8 @@ export default { evntBus.$on('load_return_invoice', (data) => { this.new_invoice(data.invoice_doc); this.discount_amount = -data.return_doc.discount_amount; + this.additional_discount_percentage = + -data.return_doc.additional_discount_percentage; this.return_doc = data.return_doc; }); document.addEventListener('keydown', this.shortOpenPayment.bind(this)); @@ -2248,6 +2310,16 @@ export default { invoiceType() { evntBus.$emit('update_invoice_type', this.invoiceType); }, + discount_amount() { + if (!this.discount_amount || this.discount_amount == 0) { + this.additional_discount_percentage = 0; + } else if (this.pos_profile.posa_use_percentage_discount) { + this.additional_discount_percentage = + (this.discount_amount / this.Total) * 100; + } else { + this.additional_discount_percentage = 0; + } + }, }, }; diff --git a/posawesome/public/js/posapp/components/pos/Mpesa-Payments.vue b/posawesome/public/js/posapp/components/pos/Mpesa-Payments.vue new file mode 100644 index 00000000..0fe6e708 --- /dev/null +++ b/posawesome/public/js/posapp/components/pos/Mpesa-Payments.vue @@ -0,0 +1,179 @@ + + + \ No newline at end of file diff --git a/posawesome/public/js/posapp/components/pos/Payments.vue b/posawesome/public/js/posapp/components/pos/Payments.vue index 0f54116c..34df0ff4 100644 --- a/posawesome/public/js/posapp/components/pos/Payments.vue +++ b/posawesome/public/js/posapp/components/pos/Payments.vue @@ -77,7 +77,7 @@ v-for="payment in invoice_doc.payments" :key="payment.name" > - + @@ -110,6 +112,17 @@ >{{ payment.mode_of_payment }} + + + {{ __(`Get Payments ${payment.mode_of_payment}`) }} + + + + + + + + + + + + + + + + @@ -435,7 +496,6 @@ - -
@@ -77,6 +78,7 @@ import EditCustomer from './EditCustomer.vue'; import NewAddress from './NewAddress.vue'; import Variants from './Variants.vue'; import Returns from './Returns.vue'; +import MpesaPayments from './Mpesa-Payments.vue'; export default { data: function () { @@ -104,6 +106,7 @@ export default { EditCustomer, NewAddress, Variants, + MpesaPayments, }, methods: { diff --git a/requirements.txt b/requirements.txt index 0f581d70..58c18d84 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -frappe -erpnext \ No newline at end of file +# frappe # https://github.com/frappe/frappe is installed during bench-init +# erpnext # https://github.com/frappe/erpnext it should be installed manually