From 31aa177a9810822ad77397fefa9544e7e4d04b4c Mon Sep 17 00:00:00 2001 From: Sherin KR Date: Wed, 14 Jan 2026 21:44:17 +0530 Subject: [PATCH] feat: Rista Integration --- petpooja_integration/api/api.py | 128 +++++----- .../custom/custom_field/item.py | 7 + .../custom/custom_field/mode_of_payment.py | 6 + .../custom/custom_field/sales_invoice.py | 40 ++++ .../sales_invoice/sales_invoice.py | 49 ++++ petpooja_integration/hooks.py | 7 + petpooja_integration/modules.txt | 3 +- .../rista_integration/__init__.py | 0 .../rista_integration/doctype/__init__.py | 0 .../doctype/rista_branch/__init__.py | 0 .../doctype/rista_branch/rista_branch.js | 8 + .../doctype/rista_branch/rista_branch.json | 67 ++++++ .../doctype/rista_branch/rista_branch.py | 9 + .../doctype/rista_branch/test_rista_branch.py | 9 + .../doctype/rista_gst_mapping/__init__.py | 0 .../rista_gst_mapping/rista_gst_mapping.json | 71 ++++++ .../rista_gst_mapping/rista_gst_mapping.py | 9 + .../doctype/rista_sales_log/__init__.py | 0 .../rista_sales_log/rista_sales_log.js | 36 +++ .../rista_sales_log/rista_sales_log.json | 97 ++++++++ .../rista_sales_log/rista_sales_log.py | 16 ++ .../rista_sales_log/test_rista_sales_log.py | 9 + .../doctype/rista_settings/__init__.py | 0 .../doctype/rista_settings/rista_settings.js | 51 ++++ .../rista_settings/rista_settings.json | 110 +++++++++ .../doctype/rista_settings/rista_settings.py | 9 + .../rista_settings/test_rista_settings.py | 9 + .../rista_integration/rista_apis.py | 218 ++++++++++++++++++ petpooja_integration/utils.py | 177 ++++++++++++++ 29 files changed, 1076 insertions(+), 69 deletions(-) create mode 100644 petpooja_integration/custom_script/sales_invoice/sales_invoice.py create mode 100644 petpooja_integration/rista_integration/__init__.py create mode 100644 petpooja_integration/rista_integration/doctype/__init__.py create mode 100644 petpooja_integration/rista_integration/doctype/rista_branch/__init__.py create mode 100644 petpooja_integration/rista_integration/doctype/rista_branch/rista_branch.js create mode 100644 petpooja_integration/rista_integration/doctype/rista_branch/rista_branch.json create mode 100644 petpooja_integration/rista_integration/doctype/rista_branch/rista_branch.py create mode 100644 petpooja_integration/rista_integration/doctype/rista_branch/test_rista_branch.py create mode 100644 petpooja_integration/rista_integration/doctype/rista_gst_mapping/__init__.py create mode 100644 petpooja_integration/rista_integration/doctype/rista_gst_mapping/rista_gst_mapping.json create mode 100644 petpooja_integration/rista_integration/doctype/rista_gst_mapping/rista_gst_mapping.py create mode 100644 petpooja_integration/rista_integration/doctype/rista_sales_log/__init__.py create mode 100644 petpooja_integration/rista_integration/doctype/rista_sales_log/rista_sales_log.js create mode 100644 petpooja_integration/rista_integration/doctype/rista_sales_log/rista_sales_log.json create mode 100644 petpooja_integration/rista_integration/doctype/rista_sales_log/rista_sales_log.py create mode 100644 petpooja_integration/rista_integration/doctype/rista_sales_log/test_rista_sales_log.py create mode 100644 petpooja_integration/rista_integration/doctype/rista_settings/__init__.py create mode 100644 petpooja_integration/rista_integration/doctype/rista_settings/rista_settings.js create mode 100644 petpooja_integration/rista_integration/doctype/rista_settings/rista_settings.json create mode 100644 petpooja_integration/rista_integration/doctype/rista_settings/rista_settings.py create mode 100644 petpooja_integration/rista_integration/doctype/rista_settings/test_rista_settings.py create mode 100644 petpooja_integration/rista_integration/rista_apis.py create mode 100644 petpooja_integration/utils.py diff --git a/petpooja_integration/api/api.py b/petpooja_integration/api/api.py index 6b2f9ef..fc9b447 100644 --- a/petpooja_integration/api/api.py +++ b/petpooja_integration/api/api.py @@ -1,5 +1,6 @@ import frappe from frappe.utils import getdate, get_time +from petpooja_integration.utils import get_mop_account @frappe.whitelist(allow_guest=True) def response(message, data, success, status_code): @@ -99,7 +100,7 @@ def create_invoice(): sales_invoice_doc.append('items', { 'item_code': item.get('item_code'), 'item_name': item.get('item_name'), - 'qty': item.get('qty'), + 'qty': item.get('quantity'), 'rate': item.get('rate'), 'amount': item.get('amount'), 'income_account': income_account @@ -149,7 +150,7 @@ def create_invoice(): handle_payment(sales_invoice_doc.name, order_details) response('Invoice created', { 'doc':sales_invoice_doc.as_dict() }, True, 200) - + except Exception as e: frappe.log_error(frappe.get_traceback(), 'Petpooja Integration Error') response('Something went wrong', {}, False, 500) @@ -221,73 +222,64 @@ def handle_payment(sales_invoice, order_details): ''' Method to handle payment entry against the created Sales Invoice ''' - if frappe.db.exists('Sales Invoice', sales_invoice): - sales_invoice_doc = frappe.get_doc('Sales Invoice', sales_invoice) + try: + if frappe.db.exists('Sales Invoice', sales_invoice): + sales_invoice_doc = frappe.get_doc('Sales Invoice', sales_invoice) - #Getting Payment Methods - payment_methods = [] - if order_details.get('part_payments', []): - for part_payment in order_details.get('part_payments', []): + #Getting Payment Methods + payment_methods = [] + if order_details.get('part_payments', []): + for part_payment in order_details.get('part_payments', []): + payment_methods.append({ + 'method': part_payment.get('payment_type'), + 'amount': part_payment.get('amount') + }) + else: payment_methods.append({ - 'method': part_payment.get('payment_type'), - 'amount': part_payment.get('amount') + 'method': order_details.get('payment_type'), + 'amount': order_details.get('total') }) - else: - payment_methods.append({ - 'method': order_details.get('payment_type'), - 'amount': order_details.get('total') - }) - - #Creating Payment Entry for each payment method - for mode in payment_methods: - mode_of_payment = '' - if frappe.db.exists('Mode of Payment', { 'petpooja_payment_type': mode.get('method') }): - mode_of_payment = frappe.get_value('Mode of Payment', { 'petpooja_payment_type': mode.get('method') }, 'name') - if not mode_of_payment: - sales_invoice_doc.add_comment( - 'Comment', - 'Mode of Payment mapping not found for {0}, Skipped Payment Entry creation.'.format(frappe.bold(mode.get('method'))) - ) - continue - - mode_of_payment_account = get_mop_account(mode_of_payment, sales_invoice_doc.company) - if not mode_of_payment_account: - sales_invoice_doc.add_comment( - 'Comment', - 'Default account not found for Mode of Payment {0}, Skipped Payment Entry creation.'.format(frappe.bold(mode_of_payment)) - ) - continue - - payment_entry = frappe.new_doc('Payment Entry') - payment_entry.payment_type = 'Receive' - payment_entry.posting_date = getdate(order_details.get('created_on')) - payment_entry.party_type = 'Customer' - payment_entry.party = sales_invoice_doc.customer - payment_entry.reference_no = order_details.get('orderID') - payment_entry.reference_date = getdate(order_details.get('created_on')) - payment_entry.mode_of_payment = mode_of_payment - payment_entry.paid_from = sales_invoice_doc.debit_to - payment_entry.paid_to = mode_of_payment_account - payment_entry.received_amount = mode.get('amount') - payment_entry.paid_amount = mode.get('amount') - payment_entry.append('references', { - 'reference_doctype': 'Sales Invoice', - 'reference_name': sales_invoice, - 'allocated_amount': mode.get('amount'), - 'outstanding_amount': frappe.get_value('Sales Invoice', sales_invoice, 'outstanding_amount'), - 'paid_amount': mode.get('amount') - }) - payment_entry.save(ignore_permissions=True) - payment_entry.submit() -def get_mop_account(mode_of_payment, company): - ''' - Method to get default account for a given Mode of Payment and Company - ''' - result = frappe.db.sql(''' - SELECT default_account - FROM `tabMode of Payment Account` - WHERE parent=%s AND company=%s - LIMIT 1 - ''', (mode_of_payment, company), as_dict=True) - return result[0].default_account if result else None + #Creating Payment Entry for each payment method + for mode in payment_methods: + mode_of_payment = '' + if frappe.db.exists('Mode of Payment', { 'petpooja_payment_type': mode.get('method') }): + mode_of_payment = frappe.get_value('Mode of Payment', { 'petpooja_payment_type': mode.get('method') }, 'name') + if not mode_of_payment: + sales_invoice_doc.add_comment( + 'Comment', + 'Mode of Payment mapping not found for {0}, Skipped Payment Entry creation.'.format(frappe.bold(mode.get('method'))) + ) + continue + + mode_of_payment_account = get_mop_account(mode_of_payment, sales_invoice_doc.company) + if not mode_of_payment_account: + sales_invoice_doc.add_comment( + 'Comment', + 'Default account not found for Mode of Payment {0}, Skipped Payment Entry creation.'.format(frappe.bold(mode_of_payment)) + ) + continue + + payment_entry = frappe.new_doc('Payment Entry') + payment_entry.payment_type = 'Receive' + payment_entry.posting_date = getdate(order_details.get('created_on')) + payment_entry.party_type = 'Customer' + payment_entry.party = sales_invoice_doc.customer + payment_entry.reference_no = order_details.get('orderID') + payment_entry.reference_date = getdate(order_details.get('created_on')) + payment_entry.mode_of_payment = mode_of_payment + payment_entry.paid_from = sales_invoice_doc.debit_to + payment_entry.paid_to = mode_of_payment_account + payment_entry.received_amount = mode.get('amount') + payment_entry.paid_amount = mode.get('amount') + payment_entry.append('references', { + 'reference_doctype': 'Sales Invoice', + 'reference_name': sales_invoice, + 'allocated_amount': mode.get('amount'), + 'outstanding_amount': frappe.get_value('Sales Invoice', sales_invoice, 'outstanding_amount'), + 'paid_amount': mode.get('amount') + }) + payment_entry.save(ignore_permissions=True) + payment_entry.submit() + except Exception as e: + sales_invoice_doc.add_comment('Comment', 'Error while creating Payment Entry: {0}'.format(frappe.bold(str(e)))) diff --git a/petpooja_integration/custom/custom_field/item.py b/petpooja_integration/custom/custom_field/item.py index b023e67..5b8ab05 100644 --- a/petpooja_integration/custom/custom_field/item.py +++ b/petpooja_integration/custom/custom_field/item.py @@ -10,6 +10,13 @@ def get_item_custom_fields(): "label": "Petpooja Item Code", "insert_after": "item_code", "unique": 1 + }, + { + "fieldname": "rista_item_code", + "fieldtype": "Data", + "label": "Rista Item Code", + "insert_after": "petpooja_item_code", + "unique": 1 } ] } diff --git a/petpooja_integration/custom/custom_field/mode_of_payment.py b/petpooja_integration/custom/custom_field/mode_of_payment.py index fe22bcb..bf55bec 100644 --- a/petpooja_integration/custom/custom_field/mode_of_payment.py +++ b/petpooja_integration/custom/custom_field/mode_of_payment.py @@ -9,6 +9,12 @@ def get_mode_of_payment_custom_fields(): "fieldtype": "Data", "label": "Petpooja Payment Type", "insert_after": "type", + }, + { + "fieldname": "rista_payment_type", + "fieldtype": "Data", + "label": "Rista Payment Type", + "insert_after": "petpooja_payment_type", } ] } diff --git a/petpooja_integration/custom/custom_field/sales_invoice.py b/petpooja_integration/custom/custom_field/sales_invoice.py index a1ee4c9..e475cd4 100644 --- a/petpooja_integration/custom/custom_field/sales_invoice.py +++ b/petpooja_integration/custom/custom_field/sales_invoice.py @@ -42,6 +42,46 @@ def get_sales_invoice_custom_fields(): "label": "Petpooja Table No.", "insert_after": "petpooja_order_type", "read_only": 1, + }, + { + "fieldname": "rista_details", + "fieldtype": "Section Break", + "label": "Rista Details", + "insert_after": "petpooja_table_no", + }, + { + "fieldname": "rista_branch", + "fieldtype": "Link", + "label": "Rista Branch", + "options": "Rista Branch", + "insert_after": "rista_details", + "read_only": 1, + }, + { + "fieldname": "rista_invoice_number", + "fieldtype": "Data", + "label": "Rista Invoice Number", + "insert_after": "rista_branch", + "read_only": 1, + }, + { + "fieldname": "rista_cb1", + "fieldtype": "Column Break", + "insert_after": "rista_invoice_number", + }, + { + "fieldname": "rista_channel", + "fieldtype": "Data", + "label": "Rista Channel", + "insert_after": "rista_cb1", + "read_only": 1, + }, + { + "fieldname": "rista_order_url", + "fieldtype": "Small Text", + "label": "Rista Order URL", + "insert_after": "rista_channel", + "read_only": 1, } ] } diff --git a/petpooja_integration/custom_script/sales_invoice/sales_invoice.py b/petpooja_integration/custom_script/sales_invoice/sales_invoice.py new file mode 100644 index 0000000..835d9a1 --- /dev/null +++ b/petpooja_integration/custom_script/sales_invoice/sales_invoice.py @@ -0,0 +1,49 @@ +import frappe + +def after_insert(doc, method): + ''' + Method to handle actions after Sales Invoice insertion. + ''' + link_to_rista_sales_log(doc) + +def on_submit(doc, method): + ''' + Method to handle actions on Sales Invoice submission. + ''' + if doc.update_stock: + for item in doc.items: + handle_bom_stock_update(item.item_code, item.qty, item.warehouse) + +def handle_bom_stock_update(item_code, qty, warehouse): + ''' + Function to handle stock update for BOM items. + ''' + bom = frappe.get_all('BOM', filters={'item': item_code, 'is_active': 1, 'is_default': 1}, fields=['name']) + if bom: + bom_doc = frappe.get_doc('BOM', bom[0].name) + stock_entry = frappe.new_doc('Stock Entry') + stock_entry.stock_entry_type = 'Material Issue' + for bom_item in bom_doc.items: + required_qty = bom_item.qty * qty + stock_entry.append('items', { + 'item_code': bom_item.item_code, + 'qty': required_qty, + 's_warehouse': warehouse + }) + stock_entry.save(ignore_permissions=True) + stock_entry.submit() + +def link_to_rista_sales_log(doc): + ''' + Function to link Sales Invoice to Rista Sales Log if applicable. + ''' + if doc.rista_invoice_number and doc.rista_branch: + if frappe.db.exists('Rista Sales Log', { + 'invoice_number': doc.rista_invoice_number, + 'branch_code': doc.rista_branch, + }): + log = frappe.db.get_value('Rista Sales Log', { + 'invoice_number': doc.rista_invoice_number, + 'branch_code': doc.rista_branch, + }, 'name') + frappe.db.set_value('Rista Sales Log', log, 'sales_invoice_reference', doc.name, update_modified=False) \ No newline at end of file diff --git a/petpooja_integration/hooks.py b/petpooja_integration/hooks.py index 05cc2ec..f064778 100644 --- a/petpooja_integration/hooks.py +++ b/petpooja_integration/hooks.py @@ -146,6 +146,13 @@ # } # } +doc_events = { + "Sales Invoice": { + "after_insert": "petpooja_integration.custom_script.sales_invoice.sales_invoice.after_insert", + "on_submit": "petpooja_integration.custom_script.sales_invoice.sales_invoice.on_submit", + } +} + # Scheduled Tasks # --------------- diff --git a/petpooja_integration/modules.txt b/petpooja_integration/modules.txt index 5a03dce..4054279 100644 --- a/petpooja_integration/modules.txt +++ b/petpooja_integration/modules.txt @@ -1 +1,2 @@ -Petpooja Integration \ No newline at end of file +Petpooja Integration +Rista Integration \ No newline at end of file diff --git a/petpooja_integration/rista_integration/__init__.py b/petpooja_integration/rista_integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petpooja_integration/rista_integration/doctype/__init__.py b/petpooja_integration/rista_integration/doctype/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petpooja_integration/rista_integration/doctype/rista_branch/__init__.py b/petpooja_integration/rista_integration/doctype/rista_branch/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petpooja_integration/rista_integration/doctype/rista_branch/rista_branch.js b/petpooja_integration/rista_integration/doctype/rista_branch/rista_branch.js new file mode 100644 index 0000000..c627fbb --- /dev/null +++ b/petpooja_integration/rista_integration/doctype/rista_branch/rista_branch.js @@ -0,0 +1,8 @@ +// Copyright (c) 2026, efeone and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Rista Branch", { +// refresh(frm) { + +// }, +// }); diff --git a/petpooja_integration/rista_integration/doctype/rista_branch/rista_branch.json b/petpooja_integration/rista_integration/doctype/rista_branch/rista_branch.json new file mode 100644 index 0000000..5529136 --- /dev/null +++ b/petpooja_integration/rista_integration/doctype/rista_branch/rista_branch.json @@ -0,0 +1,67 @@ +{ + "actions": [], + "autoname": "field:branch_code", + "creation": "2026-01-13 11:06:34.517926", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "branch_code", + "branch_name", + "company" + ], + "fields": [ + { + "fieldname": "branch_code", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Branch Code", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "branch_name", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Branch Name", + "reqd": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Company", + "options": "Company" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-01-13 11:10:22.687168", + "modified_by": "Administrator", + "module": "Rista Integration", + "name": "Rista Branch", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/petpooja_integration/rista_integration/doctype/rista_branch/rista_branch.py b/petpooja_integration/rista_integration/doctype/rista_branch/rista_branch.py new file mode 100644 index 0000000..e5cd0c2 --- /dev/null +++ b/petpooja_integration/rista_integration/doctype/rista_branch/rista_branch.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, efeone and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class RistaBranch(Document): + pass diff --git a/petpooja_integration/rista_integration/doctype/rista_branch/test_rista_branch.py b/petpooja_integration/rista_integration/doctype/rista_branch/test_rista_branch.py new file mode 100644 index 0000000..f8c8b92 --- /dev/null +++ b/petpooja_integration/rista_integration/doctype/rista_branch/test_rista_branch.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, efeone and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestRistaBranch(FrappeTestCase): + pass diff --git a/petpooja_integration/rista_integration/doctype/rista_gst_mapping/__init__.py b/petpooja_integration/rista_integration/doctype/rista_gst_mapping/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petpooja_integration/rista_integration/doctype/rista_gst_mapping/rista_gst_mapping.json b/petpooja_integration/rista_integration/doctype/rista_gst_mapping/rista_gst_mapping.json new file mode 100644 index 0000000..bc0b7ce --- /dev/null +++ b/petpooja_integration/rista_integration/doctype/rista_gst_mapping/rista_gst_mapping.json @@ -0,0 +1,71 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-01-14 13:50:39.724053", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "rista_account", + "type", + "tax_percent", + "company", + "account_head" + ], + "fields": [ + { + "fieldname": "rista_account", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Rista Account", + "reqd": 1 + }, + { + "fieldname": "tax_percent", + "fieldtype": "Percent", + "in_list_view": 1, + "label": "Tax Percent", + "precision": "1", + "reqd": 1 + }, + { + "fieldname": "type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Type", + "options": "\nIn State\nOut State", + "reqd": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "account_head", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Account Head", + "options": "Account", + "reqd": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2026-01-14 14:19:11.811970", + "modified_by": "Administrator", + "module": "Rista Integration", + "name": "Rista GST Mapping", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/petpooja_integration/rista_integration/doctype/rista_gst_mapping/rista_gst_mapping.py b/petpooja_integration/rista_integration/doctype/rista_gst_mapping/rista_gst_mapping.py new file mode 100644 index 0000000..3a1a6f6 --- /dev/null +++ b/petpooja_integration/rista_integration/doctype/rista_gst_mapping/rista_gst_mapping.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, efeone and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class RistaGSTMapping(Document): + pass diff --git a/petpooja_integration/rista_integration/doctype/rista_sales_log/__init__.py b/petpooja_integration/rista_integration/doctype/rista_sales_log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petpooja_integration/rista_integration/doctype/rista_sales_log/rista_sales_log.js b/petpooja_integration/rista_integration/doctype/rista_sales_log/rista_sales_log.js new file mode 100644 index 0000000..7a040d7 --- /dev/null +++ b/petpooja_integration/rista_integration/doctype/rista_sales_log/rista_sales_log.js @@ -0,0 +1,36 @@ +// Copyright (c) 2026, efeone and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Rista Sales Log", { + refresh(frm) { + frm.disable_save(); + frm.disable_form(); + handle_custom_buttons(frm); + }, +}); + +function handle_custom_buttons(frm) { + // if (!frm.is_new() && !frm.doc.sales_invoice_reference) { + if (!frm.is_new()) { + frm.add_custom_button(__('Sync Sales'), () => { + frappe.call({ + method: "petpooja_integration.rista_integration.rista_apis.sync_sales_invoice", + args: { + invoice_id: frm.doc.invoice_number + }, + freeze: true, + freeze_message: __("Syncing from Rista..."), + callback: function (r) { + if (!r.exc) { + frappe.msgprint({ + title: __("Success"), + indicator: "green", + message: __("Rista sync completed successfully") + }); + frm.reload_doc(); + } + } + }); + }); + } +} diff --git a/petpooja_integration/rista_integration/doctype/rista_sales_log/rista_sales_log.json b/petpooja_integration/rista_integration/doctype/rista_sales_log/rista_sales_log.json new file mode 100644 index 0000000..6f07607 --- /dev/null +++ b/petpooja_integration/rista_integration/doctype/rista_sales_log/rista_sales_log.json @@ -0,0 +1,97 @@ +{ + "actions": [], + "creation": "2026-01-13 12:26:30.229449", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "branch_code", + "invoice_number", + "invoice_date", + "invoice_time", + "column_break_ddpm", + "payload", + "invoice_reference_section", + "sales_invoice_reference" + ], + "fields": [ + { + "fieldname": "invoice_number", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Invoice Number", + "reqd": 1 + }, + { + "fieldname": "invoice_date", + "fieldtype": "Date", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Invoice Date" + }, + { + "fieldname": "invoice_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "Invoice Time" + }, + { + "fieldname": "column_break_ddpm", + "fieldtype": "Column Break" + }, + { + "fieldname": "invoice_reference_section", + "fieldtype": "Section Break", + "label": "Invoice Reference" + }, + { + "fieldname": "sales_invoice_reference", + "fieldtype": "Link", + "label": "Sales Invoice Reference", + "options": "Sales Invoice" + }, + { + "fieldname": "payload", + "fieldtype": "JSON", + "label": "Payload", + "read_only": 1 + }, + { + "fieldname": "branch_code", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Branch", + "options": "Rista Branch", + "reqd": 1 + } + ], + "grid_page_length": 50, + "in_create": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-01-14 15:19:02.689101", + "modified_by": "Administrator", + "module": "Rista Integration", + "name": "Rista Sales Log", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/petpooja_integration/rista_integration/doctype/rista_sales_log/rista_sales_log.py b/petpooja_integration/rista_integration/doctype/rista_sales_log/rista_sales_log.py new file mode 100644 index 0000000..2a7966d --- /dev/null +++ b/petpooja_integration/rista_integration/doctype/rista_sales_log/rista_sales_log.py @@ -0,0 +1,16 @@ +# Copyright (c) 2026, efeone and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document + + +class RistaSalesLog(Document): + def after_insert(self): + self.handle_invoice_creation() + + def handle_invoice_creation(self): + ''' + Handle Sales Invoice creation after Rista Sales Log is created + ''' + frappe.enqueue("petpooja_integration.rista_integration.rista_apis.sync_sales_invoice", invoice_id=self.invoice_number, queue='long') diff --git a/petpooja_integration/rista_integration/doctype/rista_sales_log/test_rista_sales_log.py b/petpooja_integration/rista_integration/doctype/rista_sales_log/test_rista_sales_log.py new file mode 100644 index 0000000..96c8537 --- /dev/null +++ b/petpooja_integration/rista_integration/doctype/rista_sales_log/test_rista_sales_log.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, efeone and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestRistaSalesLog(FrappeTestCase): + pass diff --git a/petpooja_integration/rista_integration/doctype/rista_settings/__init__.py b/petpooja_integration/rista_integration/doctype/rista_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/petpooja_integration/rista_integration/doctype/rista_settings/rista_settings.js b/petpooja_integration/rista_integration/doctype/rista_settings/rista_settings.js new file mode 100644 index 0000000..b405d76 --- /dev/null +++ b/petpooja_integration/rista_integration/doctype/rista_settings/rista_settings.js @@ -0,0 +1,51 @@ +// Copyright (c) 2026, efeone and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Rista Settings", { + refresh(frm) { + handle_custom_button(frm); + }, +}); + +function handle_custom_button(frm) { + add_sync_branch_btn(frm); + add_sync_sales_btn(frm); +} + +function add_sync_branch_btn(frm) { + frm.add_custom_button(__('Branches'), () => { + frappe.call({ + method: "petpooja_integration.rista_integration.rista_apis.sync_branches", + freeze: true, + freeze_message: __("Syncing from Rista..."), + callback: function (r) { + if (r && r.message) { + frappe.msgprint({ + title: __("Success"), + indicator: "green", + message: __(r.message) + }); + } + } + }); + }, 'Sync Data'); +} + +function add_sync_sales_btn(frm) { + frm.add_custom_button(__('Sales'), () => { + frappe.call({ + method: "petpooja_integration.rista_integration.rista_apis.sync_sales_data", + freeze: true, + freeze_message: __("Syncing from Rista..."), + callback: function (r) { + if (!r.exc) { + frappe.msgprint({ + title: __("Success"), + indicator: "green", + message: __("Rista sync completed successfully") + }); + } + } + }); + }, 'Sync Data'); +} \ No newline at end of file diff --git a/petpooja_integration/rista_integration/doctype/rista_settings/rista_settings.json b/petpooja_integration/rista_integration/doctype/rista_settings/rista_settings.json new file mode 100644 index 0000000..e1ef5fc --- /dev/null +++ b/petpooja_integration/rista_integration/doctype/rista_settings/rista_settings.json @@ -0,0 +1,110 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-01-09 14:03:05.254696", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "api_configurations_section", + "api_key", + "secret_key", + "column_break_srbt", + "api_base_url", + "section_break_bxct", + "default_customer", + "column_break_dhru", + "rounding_adjustment_account", + "section_break_ityj", + "gst_ledger_mappings" + ], + "fields": [ + { + "fieldname": "api_key", + "fieldtype": "Data", + "in_list_view": 1, + "label": "API Key", + "reqd": 1 + }, + { + "fieldname": "secret_key", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Secret Key", + "reqd": 1 + }, + { + "fieldname": "column_break_srbt", + "fieldtype": "Column Break" + }, + { + "fieldname": "api_base_url", + "fieldtype": "Data", + "in_list_view": 1, + "label": "API Base URL", + "reqd": 1 + }, + { + "fieldname": "default_customer", + "fieldtype": "Link", + "label": "Default Customer", + "options": "Customer", + "reqd": 1 + }, + { + "fieldname": "section_break_ityj", + "fieldtype": "Section Break" + }, + { + "fieldname": "gst_ledger_mappings", + "fieldtype": "Table", + "label": "GST Ledger Mappings", + "options": "Rista GST Mapping" + }, + { + "fieldname": "api_configurations_section", + "fieldtype": "Section Break", + "label": "API Configurations" + }, + { + "fieldname": "section_break_bxct", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_dhru", + "fieldtype": "Column Break" + }, + { + "fieldname": "rounding_adjustment_account", + "fieldtype": "Link", + "label": "Rounding Adjustment Account", + "options": "Account", + "reqd": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2026-01-14 14:33:46.006246", + "modified_by": "Administrator", + "module": "Rista Integration", + "name": "Rista Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/petpooja_integration/rista_integration/doctype/rista_settings/rista_settings.py b/petpooja_integration/rista_integration/doctype/rista_settings/rista_settings.py new file mode 100644 index 0000000..a7e9000 --- /dev/null +++ b/petpooja_integration/rista_integration/doctype/rista_settings/rista_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, efeone and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document + + +class RistaSettings(Document): + pass diff --git a/petpooja_integration/rista_integration/doctype/rista_settings/test_rista_settings.py b/petpooja_integration/rista_integration/doctype/rista_settings/test_rista_settings.py new file mode 100644 index 0000000..a5625cf --- /dev/null +++ b/petpooja_integration/rista_integration/doctype/rista_settings/test_rista_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, efeone and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestRistaSettings(FrappeTestCase): + pass diff --git a/petpooja_integration/rista_integration/rista_apis.py b/petpooja_integration/rista_integration/rista_apis.py new file mode 100644 index 0000000..51fb109 --- /dev/null +++ b/petpooja_integration/rista_integration/rista_apis.py @@ -0,0 +1,218 @@ +import frappe +import time +import jwt +import requests +from dateutil import parser +from frappe.utils.data import today, getdate + +#Utils Methods +from petpooja_integration.utils import create_rista_branch +from petpooja_integration.utils import create_rista_sales_log +from petpooja_integration.utils import create_sales_invoice +from petpooja_integration.utils import get_mop_account + +#API Key and Secret +API_KEY = frappe.db.get_single_value('Rista Settings', 'api_key') +BASE_URL = frappe.db.get_single_value('Rista Settings', 'api_base_url') +API_SECRET = frappe.utils.password.get_decrypted_password('Rista Settings', 'Rista Settings', 'secret_key') + +@frappe.whitelist() +def sync_branches(): + ''' + Method to sync branches from Rista to ERPNext + ''' + try: + # Token creation time (seconds) + token_creation_time = int(time.time()) + + # JWT payload + payload = { + 'iss': API_KEY, + 'iat': token_creation_time + } + + # Generate JWT (HS256 default) + token = jwt.encode(payload, API_SECRET, algorithm='HS256') + + url = '{0}/branch/list'.format(BASE_URL) + + headers = { + 'x-api-key': API_KEY, + 'x-api-token': token, + 'Content-Type': 'application/json' + } + + response = requests.get(url, headers=headers) + data = response.json() + + if response.status_code != 200: + if data.get("message"): + return f'API Error {response.status_code}: {data.get("message")}' + return f'API Error Occured with statud code {response.status_code}' + for d in data: + create_rista_branch(d) + return 'Branches synced successfully.' + + except Exception as e: + return f'Error syncing branches: {str(e)}' + +@frappe.whitelist() +def sync_sales_data(posting_date=today()): + ''' + Method to sync sales data from Rista to ERPNext for all branches + ''' + branches = frappe.db.get_all('Rista Branch', pluck='name') + for branch in branches: + sync_branch_sales_data(branch, posting_date) + +@frappe.whitelist() +def sync_branch_sales_data(branch_code, posting_date=today()): + ''' + Method to sync sales data from Rista to ERPNext for a specific branch + ''' + # Token creation time (seconds) + token_creation_time = int(time.time()) + + # JWT payload + payload = { + "iss": API_KEY, + "iat": token_creation_time + } + + # API Arguments + args = { + "branch": branch_code, + "day": posting_date + } + + sales_data = [] + last_key = '' + while True: + if last_key: + args['lastKey'] = last_key + # Generate JWT (HS256 default) + token = jwt.encode(payload, API_SECRET, algorithm="HS256") + + url = "{0}/sales/summary".format(BASE_URL) + + headers = { + "x-api-key": API_KEY, + "x-api-token": token, + "Content-Type": "application/json" + } + + response = requests.get(url, headers=headers, params=args) + data = response.json() + + if response.status_code != 200: + if data.get("message"): + return f'API Error {response.status_code}: {data.get("message")}' + return f'API Error Occured with statud code {response.status_code}' + if data.get('data', []): + sales_data.extend(data.get('data', [])) + if data.get('lastKey'): + last_key = data.get('lastKey') + else: + break + for sale in sales_data: + create_rista_sales_log(sale) + return 'Sales Invoices synced successfully.' + +@frappe.whitelist() +def sync_sales_invoice(invoice_id): + ''' + Method to sync a specific sales invoice from Rista to ERPNext + ''' + # Token creation time (seconds) + token_creation_time = int(time.time()) + + # JWT payload + payload = { + "iss": API_KEY, + "iat": token_creation_time + } + + # API Arguments + args = { + "invoice": invoice_id, + } + + # Generate JWT (HS256 default) + token = jwt.encode(payload, API_SECRET, algorithm="HS256") + + url = "{0}/sale".format(BASE_URL) + + headers = { + "x-api-key": API_KEY, + "x-api-token": token, + "Content-Type": "application/json" + } + + response = requests.get(url, headers=headers, params=args) + data = response.json() + if response.status_code != 200: + if data.get("message"): + return f'API Error {response.status_code}: {data.get("message")}' + return f'API Error Occured with statud code {response.status_code}' + invoice = create_sales_invoice(data) + payments = data.get('payments', []) + if invoice and payments: + handle_payments(invoice, payments) + return 'Sales Invoices synced successfully.' + +def handle_payments(sales_invoice, payments): + ''' + Method to handle payments for Sales Invoice + ''' + try: + if not frappe.db.exists('Sales Invoice', sales_invoice): + return + sales_invoice_doc = frappe.get_doc('Sales Invoice', sales_invoice) + for payment in payments: + pay_mode = payment.get('mode', ) + mode_of_payment = '' + if frappe.db.exists('Mode of Payment', { 'rista_payment_type': pay_mode }): + mode_of_payment = frappe.get_value('Mode of Payment', { 'rista_payment_type': pay_mode }, 'name') + if not mode_of_payment: + sales_invoice_doc.add_comment( + 'Comment', + 'Mode of Payment mapping not found for {0}, Skipped Payment Entry creation.'.format(frappe.bold(pay_mode)) + ) + continue + amount = payment.get('amount', 0) + dt = parser.isoparse(payment.get('postedDate')) + posting_datetime = dt.strftime('%Y-%m-%d %H:%M:%S') + company = sales_invoice_doc.company + mop_account = get_mop_account(mode_of_payment, company) + if not mop_account: + sales_invoice_doc.add_comment( + 'Comment', + 'Default account not found for Mode of Payment {0}, Skipped Payment Entry creation.'.format(frappe.bold(mode_of_payment)) + ) + continue + payment_entry = frappe.new_doc('Payment Entry') + payment_entry.party_type = 'Customer' + payment_entry.party = sales_invoice_doc.customer + payment_entry.payment_type = 'Receive' + payment_entry.company = company + payment_entry.posting_date = getdate(posting_datetime) + payment_entry.mode_of_payment = mode_of_payment + payment_entry.paid_from = sales_invoice_doc.debit_to + payment_entry.paid_to = mop_account + payment_entry.paid_amount = amount + payment_entry.received_amount = amount + payment_entry.reference_no = sales_invoice_doc.rista_invoice_number + payment_entry.reference_date = getdate(posting_datetime) + payment_entry.append('references', { + 'reference_doctype': 'Sales Invoice', + 'reference_name': sales_invoice, + 'outstanding_amount': frappe.db.get_value('Sales Invoice', sales_invoice, 'outstanding_amount'), + 'allocated_amount': amount + }) + payment_entry.save(ignore_permissions=True) + payment_entry.submit() + except Exception as e: + sales_invoice_doc.add_comment( + 'Comment', + 'Error creating Payment Entry: {0}'.format(frappe.bold(str(e))) + ) \ No newline at end of file diff --git a/petpooja_integration/utils.py b/petpooja_integration/utils.py new file mode 100644 index 0000000..88bc49c --- /dev/null +++ b/petpooja_integration/utils.py @@ -0,0 +1,177 @@ +import frappe +import json +from dateutil import parser +from frappe.utils import get_time, getdate + +def create_rista_branch(data): + ''' + Method to create Rista Branch if not exists from API data + ''' + branch_code = data.get('branchCode', '') + branch_name = data.get('branchName', '') + if branch_code and not frappe.db.exists('Rista Branch', branch_code): + new_branch = frappe.get_doc({ + 'doctype': 'Rista Branch', + 'branch_code': branch_code, + 'branch_name': branch_name + }) + new_branch.insert() + return branch_code + +def create_rista_sales_log(data): + ''' + Method to create Rista Invoice Log + ''' + branch_code = data.get('branchCode', '') + invoice_number = data.get('invoiceNumber', '') + invoice_date = data.get('invoiceDate', '') + dt = parser.isoparse(invoice_date) + posting_datetime = dt.strftime('%Y-%m-%d %H:%M:%S') + + if not frappe.db.exists('Rista Sales Log', { 'rista_branch': branch_code, 'invoice_number': invoice_number }): + new_log = frappe.get_doc({ + 'doctype': 'Rista Sales Log', + 'branch_code': branch_code, + 'invoice_number': invoice_number, + 'invoice_date': getdate(posting_datetime), + 'invoice_time': get_time(posting_datetime), + 'payload': json.dumps(data, indent=2) + }) + new_log.insert() + return new_log.name + +def create_sales_invoice(data): + ''' + Method to create Sales Invoice from Rista Sales Log + ''' + try: + invoice_no = data.get('invoiceNumber') + invoice_date = data.get('invoiceDate') + branch_code = data.get('branchCode', '') + if not invoice_no or not invoice_date: + return + dt = parser.isoparse(invoice_date) + posting_datetime = dt.strftime('%Y-%m-%d %H:%M:%S') + default_customer = frappe.db.get_single_value('Rista Settings', 'default_customer') + + #Getting Company and Income Account + company = frappe.db.get_single_value('Global Defaults', 'default_company') + if frappe.db.exists('Rista Branch', branch_code): + company = frappe.get_value('Rista Branch', branch_code, 'company') + income_account = frappe.get_value('Company', company, 'default_income_account') or frappe.get_single_value('Accounts Settings', 'default_income_account') + + if not frappe.db.exists('Sales Invoice', { 'invoice_no':invoice_no }): + #Creating Sales Invoice + sales_invoice_doc = frappe.new_doc('Sales Invoice') + sales_invoice_doc.company = company + sales_invoice_doc.invoice_no = invoice_no + sales_invoice_doc.posting_date = getdate(posting_datetime) + sales_invoice_doc.due_date = getdate(posting_datetime) + sales_invoice_doc.posting_time = get_time(posting_datetime) + sales_invoice_doc.set_posting_time = 1 + sales_invoice_doc.customer = default_customer + sales_invoice_doc.rista_branch = branch_code + sales_invoice_doc.rista_invoice_number = invoice_no + sales_invoice_doc.rista_channel = data.get('channel', '') + sales_invoice_doc.rista_order_url = data.get('url', '') + + #Adding Items to Sales Invoice + for item in data.get('items', []): + item_code = '' + rista_sku = item.get('skuCode', '') + if frappe.db.exists('Item', { 'rista_item_code': rista_sku }): + item_code = frappe.get_value('Item', { 'rista_item_code': rista_sku }, 'item_code') + sales_invoice_doc.append('items', { + 'item_code': item_code, + 'item_name': item.get('shortName'), + 'qty': item.get('quantity'), + 'rate': item.get('unitPrice'), + 'amount': item.get('itemTotalAmount'), + 'income_account': income_account + }) + + #Handling Item Addons + for addon in item.get('options', []): + addon_sku = addon.get('skuCode', '') + addon_item_code = '' + if frappe.db.exists('Item', { 'rista_item_code': addon_sku }): + addon_item_code = frappe.get_value('Item', { 'rista_item_code': addon_sku }, 'item_code') + sales_invoice_doc.append('items', { + 'item_code': addon_item_code, + 'item_name': addon.get('itemName'), + 'qty': addon.get('quantity'), + 'rate': addon.get('unitPrice'), + 'amount': addon.get('amount'), + 'income_account': income_account + }) + + #Handling Taxes + for tax in data.get('taxes', []): + tax_name = tax.get('name') + tax_percent = tax.get('percentage', 0) + account_head = get_tax_account_head(tax_name, tax_percent, company) + if not account_head: + frappe.throw(f'Please set up Tax Account Head for Tax: {tax_name} ({tax_percent}%) in Rista GST Mapping') + sales_invoice_doc.append('taxes', { + 'charge_type': 'Actual', + 'account_head': account_head, + 'description': tax.get('name'), + 'rate': tax.get('percentage', 0), + 'charge_type': 'Actual', + 'tax_amount': tax.get('amount', 0) + }) + + #Hanlding rounding adjustment + sales_invoice_doc.disable_rounded_total = 1 + rounding_adjustment = data.get('roundOffAmount', 0) + rounding_adjustment_account = frappe.db.get_single_value('Rista Settings', 'rounding_adjustment_account') + if not rounding_adjustment_account: + frappe.throw('Please set Rounding Adjustment Account in Rista Settings') + if rounding_adjustment: + sales_invoice_doc.append('taxes', { + 'charge_type': 'Actual', + 'account_head': rounding_adjustment_account, + 'description': 'Rounding Adjustment', + 'rate': 0, + 'charge_type': 'Actual', + 'tax_amount': rounding_adjustment + }) + + #Handling Discounts + sales_invoice_doc.apply_discount_on = 'Net Total' + sales_invoice_doc.discount_amount = abs(data.get('netDiscountAmount', 0)) + + sales_invoice_doc.save(ignore_permissions=True) + sales_invoice_doc.submit() + + return sales_invoice_doc.name + except Exception as e: + frappe.log_error(f'Error creating Sales Invoice from Rista Data: {str(e)}', 'Rista Sales Invoice Creation Error') + return None + +def get_tax_account_head(tax_name, tax_percent, company, type='Out State'): + ''' + Method to get or create Tax Account Head + ''' + account_head = '' + filter_data = { + 'rista_account': tax_name, + 'tax_percent': tax_percent, + 'company': company, + 'type': type + } + if frappe.db.exists('Rista GST Mapping', filter_data): + account_head = frappe.get_value('Rista GST Mapping', filter_data, 'account_head') + return account_head + +def get_mop_account(mode_of_payment, company): + ''' + Method to get default account for a given Mode of Payment and Company + ''' + result = frappe.db.sql(''' + SELECT default_account + FROM `tabMode of Payment Account` + WHERE parent=%s AND company=%s + LIMIT 1 + ''', (mode_of_payment, company), as_dict=True) + return result[0].default_account if result else None