diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/frappe_mpsa_payments.iml b/.idea/frappe_mpsa_payments.iml new file mode 100644 index 0000000..8a05c6e --- /dev/null +++ b/.idea/frappe_mpsa_payments.iml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..db8786c --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..5d6e1b9 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frappe_mpsa_payments/frappe_mpsa_payments/api/m_pesa_api.py b/frappe_mpsa_payments/frappe_mpsa_payments/api/m_pesa_api.py index 16c1b2a..c5562b7 100644 --- a/frappe_mpsa_payments/frappe_mpsa_payments/api/m_pesa_api.py +++ b/frappe_mpsa_payments/frappe_mpsa_payments/api/m_pesa_api.py @@ -63,9 +63,16 @@ def get_mpesa_mode_of_payment(company): return modes_of_payment @frappe.whitelist(allow_guest=True) -def get_mpesa_draft_c2b_payments(search_term): +def get_mpesa_draft_c2b_payments( + company, + full_name=None, + mode_of_payment=None, + from_date=None, + to_date=None, +): fields = [ "name", + "transid", "company", "msisdn", "full_name", @@ -74,30 +81,26 @@ def get_mpesa_draft_c2b_payments(search_term): "transamount", ] - filters = {"docstatus": 0} + filters = {"company": company, "docstatus": 0} order_by="posting_date desc, posting_time desc" - if search_term: - payments_by_msisdn = frappe.get_all( - "Mpesa C2B Payment Register", - filters={"msisdn": ["like", f"%{search_term}%"], "docstatus": 0}, - fields=fields, - order_by=order_by - ) - payments_by_full_name = frappe.get_all( - "Mpesa C2B Payment Register", - filters={"full_name": ["like", f"%{search_term}%"], "docstatus": 0}, - fields=fields, - order_by=order_by - ) + if mode_of_payment: + filters["mode_of_payment"] = mode_of_payment - # Merge results from both queries - payments = payments_by_full_name + payments_by_msisdn - else: - # If search_term or status is not provided, return all payments with the given status - payments = frappe.get_all( - "Mpesa C2B Payment Register", filters=filters, fields=fields,order_by=order_by - ) + if full_name: + filters["full_name"] = ["like", f"%{full_name}%"] + + if from_date and to_date: + filters["posting_date"] = ["between", [from_date, to_date]] + elif from_date: + filters["posting_date"] = [">=", from_date] + elif to_date: + filters["posting_date"] = ["<=", to_date] + + payments = frappe.get_all( + "Mpesa C2B Payment Register", + filters=filters, fields=fields,order_by=order_by + ) return payments @@ -156,6 +159,7 @@ def submit_instant_mpesa_payment(): def process_mpesa_payment(mpesa_payment, customer, submit_payment=False): try: doc = frappe.get_doc("Mpesa C2B Payment Register", mpesa_payment) + print(f"Mpesa Payment: {doc}") doc.customer = customer # doc.mode_of_payment = mode_of_payment #TODO: after testing, mode of payment @@ -181,6 +185,8 @@ def get_payment_method(pos_profile): def get_mode_of_payment(mpesa_doc): business_short_code=mpesa_doc.businessshortcode - mode_of_payment = frappe.get_value("Mpesa C2B Payment Register URL", business_short_code, "mode_of_payment") + mode_of_payment = frappe.get_value("Mpesa C2B Payment Register URL", {"business_shortcode": business_short_code, "register_status": "Success"}, "mode_of_payment") + if mode_of_payment is None: + mode_of_payment = frappe.get_value("Mpesa C2B Payment Register URL", {"till_number": business_short_code, "register_status": "Success"}, "mode_of_payment") return mode_of_payment diff --git a/frappe_mpsa_payments/frappe_mpsa_payments/api/payment_entry.py b/frappe_mpsa_payments/frappe_mpsa_payments/api/payment_entry.py index bff95fe..d50f306 100644 --- a/frappe_mpsa_payments/frappe_mpsa_payments/api/payment_entry.py +++ b/frappe_mpsa_payments/frappe_mpsa_payments/api/payment_entry.py @@ -1,3 +1,4 @@ +import json import frappe, erpnext from frappe import _ @@ -285,6 +286,8 @@ def get_outstanding_invoices( invoice_type=None, common_filter=None, posting_date=None, + from_date=None, + to_date=None, min_outstanding=None, max_outstanding=None, accounting_dimensions=None, @@ -292,6 +295,8 @@ def get_outstanding_invoices( limit=None, voucher_no=None, ): + if invoice_type is None: + invoice_type = "Sales Invoice" account=get_party_account("Customer", customer, company), ple = qb.DocType("Payment Ledger Entry") @@ -314,6 +319,12 @@ def get_outstanding_invoices( common_filter.append(ple.account.isin(account)) common_filter.append(ple.party_type == "Customer") common_filter.append(ple.party == customer) + if from_date and to_date: + common_filter.append(ple.posting_date.between(from_date, to_date)) + elif from_date: + common_filter.append(ple.posting_date >= from_date) + elif to_date: + common_filter.append(ple.posting_date <= to_date) ple_query = QueryPaymentLedger() invoice_list = ple_query.get_voucher_outstandings( @@ -482,10 +493,7 @@ def get_available_pos_profiles(company, currency): ) return pos_profiles_list -def create_and_reconcile_payment_reconciliation(invoice_name, customer, company, payment_entries): - invoice = frappe.get_doc("Sales Invoice", invoice_name) - currency = invoice.get("currency") - +def create_and_reconcile_payment_reconciliation(outstanding_invoices, customer, company, payment_entries): reconcile_doc = frappe.new_doc("Payment Reconciliation") reconcile_doc.party_type = "Customer" reconcile_doc.party = customer @@ -498,17 +506,20 @@ def create_and_reconcile_payment_reconciliation(invoice_name, customer, company, "payments": [], } - args["invoices"].append( - { - "invoice_type": "Sales Invoice", - "invoice_number": invoice.get("name"), - "invoice_date": invoice.get("posting_date"), - "amount": invoice.get("grand_total"), - "outstanding_amount": invoice.get("outstanding_amount"), - "currency": invoice.get("currency"), - "exchange_rate": 0, - } - ) + for invoice in outstanding_invoices: + invoice_doc = frappe.get_doc("Sales Invoice", invoice) + args["invoices"].append( + { + "invoice_type": "Sales Invoice", + "invoice_number": invoice_doc.get("name"), + "invoice_date": invoice_doc.get("posting_date"), + "amount": invoice_doc.get("grand_total"), + "outstanding_amount": invoice_doc.get("outstanding_amount"), + "currency": invoice_doc.get("currency"), + "exchange_rate": 0, + } + ) + for payment_entry in payment_entries: payment_entry_doc = frappe.get_doc("Payment Entry", payment_entry) @@ -530,20 +541,24 @@ def create_and_reconcile_payment_reconciliation(invoice_name, customer, company, frappe.db.commit() @frappe.whitelist() -def process_mpesa_c2b_reconciliation(): - mpesa_transaction = frappe.form_dict.get("mpesa_name") - invoice_name = frappe.form_dict.get("invoice_name") - invoice = frappe.get_doc("Sales Invoice", invoice_name) - customer = invoice.get("customer") - company = invoice.get("company") +def process_mpesa_c2b_reconciliation(mpesa_names, invoice_names): + if isinstance(mpesa_names, str): + mpesa_names = json.loads(mpesa_names) + if isinstance(invoice_names, str): + invoice_names = json.loads(invoice_names) - # TODO: after testing, withdraw this static method of payment - mode_of_payment = "Mpesa-Test" + if not invoice_names: + frappe.throw(_("No invoices provided.")) - payment_entry = submit_mpesa_payment(mpesa_transaction, customer) - payment_entries = [payment_entry.get("name")] + first_invoice_name = invoice_names[0] + first_invoice = frappe.get_doc("Sales Invoice", first_invoice_name) + customer = first_invoice.get("customer") + company = first_invoice.get("company") - create_and_reconcile_payment_reconciliation(invoice_name, customer, company, payment_entries) + payment_entries = [submit_mpesa_payment(mpesa_name, customer).get("name") for mpesa_name in + mpesa_names] + + create_and_reconcile_payment_reconciliation(invoice_names, customer, company, payment_entries) @frappe.whitelist() diff --git a/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_draft_payments/__init__.py b/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_draft_payments/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_draft_payments/mpesa_draft_payments.json b/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_draft_payments/mpesa_draft_payments.json new file mode 100644 index 0000000..77461b5 --- /dev/null +++ b/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_draft_payments/mpesa_draft_payments.json @@ -0,0 +1,67 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-10-17 07:33:08.666932", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "payment_id", + "full_name", + "column_break_fnjs", + "date", + "amount" + ], + "fields": [ + { + "fieldname": "payment_id", + "fieldtype": "Link", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "Payment ID", + "options": "Mpesa C2B Payment Register" + }, + { + "fieldname": "full_name", + "fieldtype": "Data", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "Full Name" + }, + { + "fieldname": "date", + "fieldtype": "Date", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "Date" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "Amount" + }, + { + "fieldname": "column_break_fnjs", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "is_virtual": 1, + "istable": 1, + "links": [], + "modified": "2024-10-17 07:35:16.568396", + "modified_by": "Administrator", + "module": "Frappe Mpsa Payments", + "name": "Mpesa Draft Payments", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_draft_payments/mpesa_draft_payments.py b/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_draft_payments/mpesa_draft_payments.py new file mode 100644 index 0000000..e0a2b06 --- /dev/null +++ b/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_draft_payments/mpesa_draft_payments.py @@ -0,0 +1,32 @@ +# Copyright (c) 2024, Navari Limited and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class MpesaDraftPayments(Document): + + def db_insert(self, *args, **kwargs): + pass + + def load_from_db(self): + pass + + def db_update(self): + pass + def delete(self): + pass + + @staticmethod + def get_list(args): + pass + + @staticmethod + def get_count(args): + pass + + @staticmethod + def get_stats(args): + pass + diff --git a/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments/__init__.py b/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments/mpesa_payments.js b/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments/mpesa_payments.js new file mode 100644 index 0000000..3d4900e --- /dev/null +++ b/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments/mpesa_payments.js @@ -0,0 +1,179 @@ +// Copyright (c) 2024, Navari Limited and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Mpesa Payments", { + onload(frm) { + const default_company = frappe.defaults.get_user_default("Company"); + frm.set_value("company", default_company); + }, + + refresh(frm) { + frm.disable_save(); + + frm.set_df_property("invoices", "cannot_add_rows", true); + frm.set_df_property("mpesa_payments", "cannot_add_rows", true); + + }, + + customer(frm) { + let fetch_btn = frm.add_custom_button(__("Get Unreconciled Entries"), () => { + frm.trigger("fetch_entries"); + }); + }, + + onload_post_render(frm) { + frm.set_query('invoice_name', function() { + return { + filters: { + docstatus: 1, + outstanding_amount: ['>', 0], + company: frm.doc.company, + customer: frm.doc.customer, + } + }; + }); + }, + + fetch_entries(frm) { + frm.clear_table("invoices"); + frm.clear_table("mpesa_payments"); + + // Fetch outstanding invoices + frappe.call({ + method: + "frappe_mpsa_payments.frappe_mpsa_payments.api.payment_entry.get_outstanding_invoices", + args: { + company: frm.doc.company, + currency: frm.doc.currency, + customer: frm.doc.customer, + voucher_no: frm.doc.invoice_name || "", + from_date: frm.doc.from_invoice_date || "", + to_date: frm.doc.to_invoice_date || "", + }, + callback: function (response) { + let draft_invoices = response.message; + if (draft_invoices && draft_invoices.length > 0) { + frm.clear_table("invoices"); + + draft_invoices.forEach(function (invoice) { + let row = frm.add_child("invoices"); + row.invoice = invoice.voucher_no; + row.date = invoice.posting_date; + row.total = invoice.invoice_amount; + row.outstanding_amount = invoice.outstanding_amount; + }); + + frm.refresh_field("invoices"); + } else { + frappe.msgprint({ + title: __("No Outstanding Invoices"), + message: __( + "No outstanding invoices were found for the selected customer." + ), + indicator: "orange", + }); + } + + check_for_process_payments_button(frm); + }, + }); + + // Fetch draft payments + frappe.call({ + method: + "frappe_mpsa_payments.frappe_mpsa_payments.api.m_pesa_api.get_mpesa_draft_c2b_payments", + args: { + company: frm.doc.company, + full_name: frm.doc.full_name || "", + from_date: frm.doc.from_mpesa_payment_date || "", + to_date: frm.doc.to_mpesa_payment_date || "", + }, + callback: function (response) { + let draft_payments = response.message; + + if (draft_payments && draft_payments.length > 0) { + frm.clear_table("mpesa_payments"); + + draft_payments.forEach(function (payment) { + let row = frm.add_child("mpesa_payments"); + row.payment_id = payment.name; + row.full_name = payment.full_name; + row.date = payment.posting_date; + row.amount = payment.transamount; + }); + + frm.refresh_field("mpesa_payments"); + } else { + frappe.msgprint({ + title: __("No Outstanding Payments"), + message: __( + "No outstanding payments were found for the selected customer." + ), + indicator: "orange", + }); + } + + check_for_process_payments_button(frm); + }, + }); + }, + + process_payments(frm, retryCount = 0) { + + let unpaid_invoices = frm.doc.invoices || []; + let mpesa_payments = frm.doc.mpesa_payments || []; + + if (unpaid_invoices.length === 0 || mpesa_payments.length === 0) { + frappe.msgprint({ + title: __("No Entries Found"), + message: __("Please add at least one invoice and one Mpesa payment for processing."), + indicator: "orange", + }); + return; + } + + let invoice_names = unpaid_invoices.map(invoice => invoice.invoice); + let mpesa_names = mpesa_payments.map(payment => payment.payment_id); + + frappe.call({ + method: "frappe_mpsa_payments.frappe_mpsa_payments.api.payment_entry.process_mpesa_c2b_reconciliation", + args: { + invoice_names: invoice_names, + mpesa_names: mpesa_names + }, + callback: function (response) { + if (response) { + frappe.msgprint({ + title: __("Success"), + message: __("Payment reconciliation successful."), + indicator: "green", + }); + + frm.clear_table("invoices"); + frm.clear_table("mpesa_payments"); + frm.refresh_field("invoices"); + frm.refresh_field("mpesa_payments"); + + check_for_process_payments_button(frm); + } else { + frappe.msgprint({ + title: __("Payment Processing Failed"), + message: __("Some payments could not be processed. Please try again."), + indicator: "red", + }); + } + } + }) + }, + +}); + +function check_for_process_payments_button(frm) { + if (frm.doc.invoices.length > 0 && frm.doc.mpesa_payments.length > 0) { + let process_btn = frm.add_custom_button(__("Allocate"), () => { + frm.trigger("process_payments"); + }); + + process_btn.addClass("btn-primary"); + } +} diff --git a/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments/mpesa_payments.json b/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments/mpesa_payments.json new file mode 100644 index 0000000..2c1aae4 --- /dev/null +++ b/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments/mpesa_payments.json @@ -0,0 +1,150 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-10-16 10:33:50.017925", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "customer", + "currency", + "column_break_sbgx", + "company", + "filters_section", + "from_invoice_date", + "from_mpesa_payment_date", + "column_break_pfoh", + "to_invoice_date", + "to_mpesa_payment_date", + "column_break_asbv", + "invoice_name", + "full_name", + "sales_invoices_section", + "invoices", + "section_break_lqip", + "mpesa_payments" + ], + "fields": [ + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "column_break_sbgx", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: doc.company", + "fieldname": "customer", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Customer", + "options": "Customer", + "reqd": 1 + }, + { + "fetch_from": "company.default_currency", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "read_only": 1 + }, + { + "collapsible": 1, + "default": "1", + "depends_on": "eval: doc.customer", + "fieldname": "filters_section", + "fieldtype": "Section Break", + "label": "Filters" + }, + { + "fieldname": "from_invoice_date", + "fieldtype": "Date", + "label": "From Invoice Date" + }, + { + "fieldname": "from_mpesa_payment_date", + "fieldtype": "Date", + "label": "From Mpesa Payment Date" + }, + { + "fieldname": "column_break_pfoh", + "fieldtype": "Column Break" + }, + { + "fieldname": "to_invoice_date", + "fieldtype": "Date", + "label": "To Invoice Date" + }, + { + "fieldname": "to_mpesa_payment_date", + "fieldtype": "Date", + "label": "To Mpesa Payment Date" + }, + { + "depends_on": "eval: doc.customer", + "fieldname": "sales_invoices_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "invoices", + "fieldtype": "Table", + "label": "Invoices", + "options": "Mpesa Payments Invoices" + }, + { + "depends_on": "eval: doc.customer", + "fieldname": "section_break_lqip", + "fieldtype": "Section Break" + }, + { + "fieldname": "mpesa_payments", + "fieldtype": "Table", + "label": "Mpesa Payments", + "options": "Mpesa Draft Payments" + }, + { + "fieldname": "full_name", + "fieldtype": "Data", + "label": "Full Name" + }, + { + "fieldname": "column_break_asbv", + "fieldtype": "Column Break" + }, + { + "fieldname": "invoice_name", + "fieldtype": "Link", + "label": "Invoice Name", + "options": "Sales Invoice" + } + ], + "index_web_pages_for_search": 1, + "is_virtual": 1, + "issingle": 1, + "links": [], + "modified": "2024-10-30 07:46:40.558666", + "modified_by": "Administrator", + "module": "Frappe Mpsa Payments", + "name": "Mpesa Payments", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments/mpesa_payments.py b/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments/mpesa_payments.py new file mode 100644 index 0000000..12d8973 --- /dev/null +++ b/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments/mpesa_payments.py @@ -0,0 +1,38 @@ +# Copyright (c) 2024, Navari Limited and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document + + +class MpesaPayments(Document): + + _table_fieldnames = [] + + def save(self): + return + + def db_insert(self, *args, **kwargs): + pass + + def load_from_db(self): + pass + + def db_update(self): + pass + + def delete(self): + pass + + @staticmethod + def get_list(args): + pass + + @staticmethod + def get_count(args): + pass + + @staticmethod + def get_stats(args): + pass + diff --git a/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments/test_mpesa_payments.py b/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments/test_mpesa_payments.py new file mode 100644 index 0000000..2fa6dc4 --- /dev/null +++ b/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments/test_mpesa_payments.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Navari Limited and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestMpesaPayments(FrappeTestCase): + pass diff --git a/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments_invoices/__init__.py b/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments_invoices/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments_invoices/mpesa_payments_invoices.json b/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments_invoices/mpesa_payments_invoices.json new file mode 100644 index 0000000..3e1428b --- /dev/null +++ b/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments_invoices/mpesa_payments_invoices.json @@ -0,0 +1,67 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-10-16 10:49:09.861967", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "invoice", + "date", + "column_break_uznl", + "total", + "outstanding_amount" + ], + "fields": [ + { + "fieldname": "invoice", + "fieldtype": "Link", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "Invoice", + "options": "Sales Invoice" + }, + { + "fieldname": "date", + "fieldtype": "Date", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "Date" + }, + { + "fieldname": "column_break_uznl", + "fieldtype": "Column Break" + }, + { + "fieldname": "total", + "fieldtype": "Currency", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "Total" + }, + { + "fieldname": "outstanding_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "Outstanding Amount" + } + ], + "index_web_pages_for_search": 1, + "is_virtual": 1, + "istable": 1, + "links": [], + "modified": "2024-10-22 08:37:00.706403", + "modified_by": "Administrator", + "module": "Frappe Mpsa Payments", + "name": "Mpesa Payments Invoices", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments_invoices/mpesa_payments_invoices.py b/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments_invoices/mpesa_payments_invoices.py new file mode 100644 index 0000000..85b5784 --- /dev/null +++ b/frappe_mpsa_payments/frappe_mpsa_payments/doctype/mpesa_payments_invoices/mpesa_payments_invoices.py @@ -0,0 +1,33 @@ +# Copyright (c) 2024, Navari Limited and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class MpesaPaymentsInvoices(Document): + + def db_insert(self, *args, **kwargs): + pass + + def load_from_db(self): + pass + + def db_update(self): + pass + + def delete(self): + pass + + @staticmethod + def get_list(args): + pass + + @staticmethod + def get_count(args): + pass + + @staticmethod + def get_stats(args): + pass + diff --git a/frappe_mpsa_payments/frappe_mpsa_payments/patches/sales_invoice_patch.py b/frappe_mpsa_payments/frappe_mpsa_payments/patches/sales_invoice_patch.py new file mode 100644 index 0000000..9859f6c --- /dev/null +++ b/frappe_mpsa_payments/frappe_mpsa_payments/patches/sales_invoice_patch.py @@ -0,0 +1,16 @@ +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + +def execute(): + custom_fields = { + "Sales Invoice": [ + { + "fieldname": "mpesa_payments", + "label": "Fetch Mpesa Payments", + "fieldtype": "Button", + "insert_after": "payments_section", + } + ] + } + + create_custom_fields(custom_fields) \ No newline at end of file diff --git a/frappe_mpsa_payments/hooks.py b/frappe_mpsa_payments/hooks.py index f0aa4ed..ca719d6 100644 --- a/frappe_mpsa_payments/hooks.py +++ b/frappe_mpsa_payments/hooks.py @@ -44,6 +44,7 @@ # include js in doctype views # doctype_js = {"doctype" : "public/js/doctype.js"} +doctype_js = {"Sales Invoice": "public/js/sales_invoice.js"} # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} diff --git a/frappe_mpsa_payments/patches.txt b/frappe_mpsa_payments/patches.txt index f15c3a9..d5a3142 100644 --- a/frappe_mpsa_payments/patches.txt +++ b/frappe_mpsa_payments/patches.txt @@ -2,5 +2,7 @@ # Patches added in this section will be executed before doctypes are migrated # Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations +frappe_mpsa_payments.frappe_mpsa_payments.patches.sales_invoice_patch + [post_model_sync] # Patches added in this section will be executed after doctypes are migrated \ No newline at end of file diff --git a/frappe_mpsa_payments/public/js/sales_invoice.js b/frappe_mpsa_payments/public/js/sales_invoice.js new file mode 100644 index 0000000..91e622f --- /dev/null +++ b/frappe_mpsa_payments/public/js/sales_invoice.js @@ -0,0 +1,128 @@ +frappe.ui.form.on("Sales Invoice", { + mpesa_payments: function (frm) { + frm.trigger("open_mpesa_payment_modal"); + }, + + open_mpesa_payment_modal: function (frm) { + // Fetch Mpesa payments + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "Mpesa C2B Payment Register", + filters: { + docstatus: 0, + }, + fields: [ + "name", + "transamount", + "transid", + "billrefnumber", + "mode_of_payment", + "full_name", + "posting_date", + ], + }, + callback: function (response) { + const payments = response.message; + if (payments.length) { + // Create the modal + let dialog = new frappe.ui.Dialog({ + title: __("Select Mpesa Payment"), + fields: [ + { + fieldname: "mpesa_payment", + label: "Mpesa Payment", + fieldtype: "Table", + cannot_add_rows: true, + data: payments, + fields: [ + { + fieldname: "name", + label: __("Name"), + fieldtype: "Link", + options: "Mpesa C2B Payment Register", + in_list_view: 0, + read_only: 1, + }, + { + fieldname: "posting_date", + label: __("Posting Date"), + fieldtype: "Date", + in_list_view: 1, + read_only: 1, + }, + { + fieldname: "mode_of_payment", + label: __("Mode of Payment"), + fieldtype: "Link", + options: "Mode of Payment", + in_list_view: 0, + read_only: 1, + }, + { + fieldname: "full_name", + label: __("Full Name"), + fieldtype: "Data", + in_list_view: 1, + read_only: 1, + }, + { + fieldname: "transid", + label: __("Transaction ID"), + fieldtype: "Data", + in_list_view: 1, + read_only: 1, + }, + { + fieldname: "transamount", + label: __("Payment Amount"), + fieldtype: "Currency", + in_list_view: 1, + read_only: 1, + }, + ], + }, + ], + primary_action_label: "Add Payment", + primary_action: function (data) { + if (data.mpesa_payment && data.mpesa_payment.length > 0) { + const customer = frm.doc.customer; + // Loop through selected payments and add them to the Sales Invoice + data.mpesa_payment.forEach((payment) => { + // Update the payment with the customer + frappe.call({ + method: "frappe.client.get", + args: { + doctype: "Mpesa C2B Payment Register", + name: payment.name, + }, + callback: function (paymentDocResponse) { + let paymentDoc = paymentDocResponse.message; + + paymentDoc.customer = customer; + + frm.clear_table("payments"); + + frm.add_child("payments", { + mode_of_payment: payment.mode_of_payment, + amount: payment.transamount, + reference_no: payment.transid, + }); + frm.refresh_field("payments"); + dialog.hide(); + }, + }); + }); + } else { + frappe.msgprint(__("Please select a payment.")); + } + }, + }); + dialog.show(); + } else { + frappe.msgprint(__("No draft Mpesa payments available.")); + } + }, + }); + }, +});