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."));
+ }
+ },
+ });
+ },
+});