diff --git a/av_tools/av_tools/doctype/foreign_import_exchange_difference_details/__init__.py b/av_tools/av_tools/doctype/foreign_import_exchange_difference_details/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/av_tools/av_tools/doctype/foreign_import_exchange_difference_details/foreign_import_exchange_difference_details.json b/av_tools/av_tools/doctype/foreign_import_exchange_difference_details/foreign_import_exchange_difference_details.json new file mode 100644 index 0000000..f6745c6 --- /dev/null +++ b/av_tools/av_tools/doctype/foreign_import_exchange_difference_details/foreign_import_exchange_difference_details.json @@ -0,0 +1,95 @@ +{ + "actions": [], + "creation": "2025-08-19 10:15:00.000000", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "reference_type", + "reference_name", + "column_break_1", + "difference_type", + "amount", + "section_break_je", + "journal_entry", + "posting_date", + "column_break_2", + "remarks" + ], + "fields": [ + { + "fieldname": "reference_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Reference Type", + "options": "Purchase Invoice\nLanded Cost Voucher\nPayment Entry", + "reqd": 1 + }, + { + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Reference Name", + "options": "reference_type", + "reqd": 1 + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, + { + "fieldname": "difference_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Difference Type", + "options": "Gain\nLoss", + "reqd": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "precision": "2", + "reqd": 1 + }, + { + "fieldname": "section_break_je", + "fieldtype": "Section Break", + "label": "Journal Entry Details" + }, + { + "fieldname": "journal_entry", + "fieldtype": "Link", + "label": "Journal Entry", + "options": "Journal Entry", + "read_only": 1 + }, + { + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date", + "reqd": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "remarks", + "fieldtype": "Text", + "label": "Remarks" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "modified": "2025-08-19 10:15:00.000000", + "modified_by": "Administrator", + "module": "Av Tools", + "name": "Foreign Import Exchange Difference Details", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/av_tools/av_tools/doctype/foreign_import_exchange_difference_details/foreign_import_exchange_difference_details.py b/av_tools/av_tools/doctype/foreign_import_exchange_difference_details/foreign_import_exchange_difference_details.py new file mode 100644 index 0000000..d9aaec0 --- /dev/null +++ b/av_tools/av_tools/doctype/foreign_import_exchange_difference_details/foreign_import_exchange_difference_details.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, Aakvatech and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class ForeignImportExchangeDifferenceDetails(Document): + pass diff --git a/av_tools/av_tools/doctype/foreign_import_lcv_details/__init__.py b/av_tools/av_tools/doctype/foreign_import_lcv_details/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/av_tools/av_tools/doctype/foreign_import_lcv_details/foreign_import_lcv_details.json b/av_tools/av_tools/doctype/foreign_import_lcv_details/foreign_import_lcv_details.json new file mode 100644 index 0000000..7a24a67 --- /dev/null +++ b/av_tools/av_tools/doctype/foreign_import_lcv_details/foreign_import_lcv_details.json @@ -0,0 +1,97 @@ +{ + "actions": [], + "creation": "2025-08-19 10:05:00", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "landed_cost_voucher", + "lcv_date", + "column_break_1", + "lcv_amount_foreign", + "lcv_amount_base", + "section_break_rates", + "exchange_rate_used", + "column_break_2", + "allocated_to_items", + "exchange_difference" + ], + "fields": [ + { + "fieldname": "landed_cost_voucher", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Landed Cost Voucher", + "options": "Landed Cost Voucher", + "reqd": 1 + }, + { + "fetch_from": "landed_cost_voucher.posting_date", + "fieldname": "lcv_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "LCV Date", + "read_only": 1 + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, + { + "fieldname": "lcv_amount_foreign", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "LCV Amount (Foreign)", + "precision": "2" + }, + { + "fetch_from": "landed_cost_voucher.total_taxes_and_charges", + "fieldname": "lcv_amount_base", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "LCV Amount (Base)", + "read_only": 1 + }, + { + "fieldname": "section_break_rates", + "fieldtype": "Section Break" + }, + { + "default": "1", + "fieldname": "exchange_rate_used", + "fieldtype": "Float", + "label": "Exchange Rate Used", + "precision": "9" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "allocated_to_items", + "fieldtype": "Currency", + "label": "Allocated to Items", + "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "exchange_difference", + "fieldtype": "Currency", + "label": "Exchange Difference", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2026-04-12 01:42:55.306084", + "modified_by": "mchoksi@aakvatech.com", + "module": "Av Tools", + "name": "Foreign Import LCV Details", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/av_tools/av_tools/doctype/foreign_import_lcv_details/foreign_import_lcv_details.py b/av_tools/av_tools/doctype/foreign_import_lcv_details/foreign_import_lcv_details.py new file mode 100644 index 0000000..0cef688 --- /dev/null +++ b/av_tools/av_tools/doctype/foreign_import_lcv_details/foreign_import_lcv_details.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, Aakvatech and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class ForeignImportLCVDetails(Document): + pass diff --git a/av_tools/av_tools/doctype/foreign_import_payment_details/__init__.py b/av_tools/av_tools/doctype/foreign_import_payment_details/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/av_tools/av_tools/doctype/foreign_import_payment_details/foreign_import_payment_details.json b/av_tools/av_tools/doctype/foreign_import_payment_details/foreign_import_payment_details.json new file mode 100644 index 0000000..8ae5fcd --- /dev/null +++ b/av_tools/av_tools/doctype/foreign_import_payment_details/foreign_import_payment_details.json @@ -0,0 +1,99 @@ +{ + "actions": [], + "creation": "2025-08-19 10:10:00", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "payment_entry", + "payment_date", + "column_break_1", + "payment_amount_foreign", + "payment_amount_base", + "section_break_rates", + "payment_exchange_rate", + "column_break_2", + "exchange_difference", + "journal_entry_created" + ], + "fields": [ + { + "fieldname": "payment_entry", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Payment Entry", + "options": "Payment Entry", + "reqd": 1 + }, + { + "fetch_from": "payment_entry.posting_date", + "fieldname": "payment_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Payment Date", + "read_only": 1 + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, + { + "fetch_from": "payment_entry.paid_amount", + "fieldname": "payment_amount_foreign", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Payment Amount (Foreign)", + "read_only": 1 + }, + { + "fetch_from": "payment_entry.base_paid_amount", + "fieldname": "payment_amount_base", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Payment Amount (Base)", + "read_only": 1 + }, + { + "fieldname": "section_break_rates", + "fieldtype": "Section Break" + }, + { + "fetch_from": "payment_entry.source_exchange_rate", + "fieldname": "payment_exchange_rate", + "fieldtype": "Float", + "label": "Payment Exchange Rate", + "precision": "9", + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "exchange_difference", + "fieldtype": "Currency", + "label": "Exchange Difference", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "journal_entry_created", + "fieldtype": "Check", + "label": "Journal Entry Created", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2026-04-12 01:42:28.494953", + "modified_by": "mchoksi@aakvatech.com", + "module": "Av Tools", + "name": "Foreign Import Payment Details", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/av_tools/av_tools/doctype/foreign_import_payment_details/foreign_import_payment_details.py b/av_tools/av_tools/doctype/foreign_import_payment_details/foreign_import_payment_details.py new file mode 100644 index 0000000..dc0eb75 --- /dev/null +++ b/av_tools/av_tools/doctype/foreign_import_payment_details/foreign_import_payment_details.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, Aakvatech and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class ForeignImportPaymentDetails(Document): + pass diff --git a/av_tools/av_tools/doctype/foreign_import_settings/__init__.py b/av_tools/av_tools/doctype/foreign_import_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/av_tools/av_tools/doctype/foreign_import_settings/foreign_import_settings.js b/av_tools/av_tools/doctype/foreign_import_settings/foreign_import_settings.js new file mode 100644 index 0000000..9548328 --- /dev/null +++ b/av_tools/av_tools/doctype/foreign_import_settings/foreign_import_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Aakvatech and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Foreign Import Settings", { +// refresh(frm) { + +// }, +// }); diff --git a/av_tools/av_tools/doctype/foreign_import_settings/foreign_import_settings.json b/av_tools/av_tools/doctype/foreign_import_settings/foreign_import_settings.json new file mode 100644 index 0000000..632d1b3 --- /dev/null +++ b/av_tools/av_tools/doctype/foreign_import_settings/foreign_import_settings.json @@ -0,0 +1,150 @@ +{ + "actions": [], + "creation": "2025-08-19 10:20:00.000000", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "section_break_general", + "company", + "exchange_difference_threshold", + "column_break_general", + "auto_create_journal_entries", + "enable_lcv_exchange_tracking", + "section_break_accounts", + "default_exchange_gain_account", + "column_break_accounts", + "default_exchange_loss_account", + "section_break_notifications", + "notification_on_large_differences", + "column_break_notifications", + "large_difference_threshold", + "section_break_other", + "is_default" + ], + "fields": [ + { + "fieldname": "section_break_general", + "fieldtype": "Section Break", + "label": "General Settings" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "default": "0.01", + "description": "Minimum amount to record exchange differences", + "fieldname": "exchange_difference_threshold", + "fieldtype": "Float", + "label": "Exchange Difference Threshold", + "precision": "2" + }, + { + "fieldname": "column_break_general", + "fieldtype": "Column Break" + }, + { + "default": "1", + "description": "Automatically create Journal Entries for exchange differences", + "fieldname": "auto_create_journal_entries", + "fieldtype": "Check", + "label": "Auto Create Journal Entries" + }, + { + "default": "1", + "description": "Track exchange differences on Landed Cost Vouchers", + "fieldname": "enable_lcv_exchange_tracking", + "fieldtype": "Check", + "label": "Enable LCV Exchange Tracking" + }, + { + "fieldname": "section_break_accounts", + "fieldtype": "Section Break", + "label": "Default Accounts" + }, + { + "fieldname": "default_exchange_gain_account", + "fieldtype": "Link", + "label": "Default Exchange Gain Account", + "options": "Account" + }, + { + "fieldname": "column_break_accounts", + "fieldtype": "Column Break" + }, + { + "fieldname": "default_exchange_loss_account", + "fieldtype": "Link", + "label": "Default Exchange Loss Account", + "options": "Account" + }, + { + "fieldname": "section_break_notifications", + "fieldtype": "Section Break", + "label": "Notifications" + }, + { + "default": "0", + "description": "Send notification when exchange differences exceed threshold", + "fieldname": "notification_on_large_differences", + "fieldtype": "Check", + "label": "Notification on Large Differences" + }, + { + "fieldname": "column_break_notifications", + "fieldtype": "Column Break" + }, + { + "default": "1000", + "description": "Amount above which to send notifications", + "fieldname": "large_difference_threshold", + "fieldtype": "Currency", + "label": "Large Difference Threshold" + }, + { + "fieldname": "section_break_other", + "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "is_default", + "fieldtype": "Check", + "label": "Is Default" + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "modified": "2025-08-19 10:20:00.000000", + "modified_by": "Administrator", + "module": "Av Tools", + "name": "Foreign Import Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "Accounts Manager", + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/av_tools/av_tools/doctype/foreign_import_settings/foreign_import_settings.py b/av_tools/av_tools/doctype/foreign_import_settings/foreign_import_settings.py new file mode 100644 index 0000000..6c515b1 --- /dev/null +++ b/av_tools/av_tools/doctype/foreign_import_settings/foreign_import_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, Aakvatech and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class ForeignImportSettings(Document): + pass diff --git a/av_tools/av_tools/doctype/foreign_import_settings/test_foreign_import_settings.py b/av_tools/av_tools/doctype/foreign_import_settings/test_foreign_import_settings.py new file mode 100644 index 0000000..b8ddb49 --- /dev/null +++ b/av_tools/av_tools/doctype/foreign_import_settings/test_foreign_import_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, Aakvatech and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestForeignImportSettings(FrappeTestCase): + pass diff --git a/av_tools/av_tools/doctype/foreign_import_transaction/__init__.py b/av_tools/av_tools/doctype/foreign_import_transaction/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/av_tools/av_tools/doctype/foreign_import_transaction/foreign_import_transaction.js b/av_tools/av_tools/doctype/foreign_import_transaction/foreign_import_transaction.js new file mode 100644 index 0000000..b45232b --- /dev/null +++ b/av_tools/av_tools/doctype/foreign_import_transaction/foreign_import_transaction.js @@ -0,0 +1,213 @@ +// Copyright (c) 2025, Aakvatech and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Foreign Import Transaction', { + refresh: function(frm) { + if (frm.doc.docstatus === 1 && frm.doc.status !== 'Completed') { + frm.add_custom_button(__('Recalculate Differences'), function() { + frappe.call({ + method: 'recalculate_differences', + doc: frm.doc, + callback: function(r) { + if (r.message) { + frappe.msgprint(__('Exchange differences recalculated successfully')); + frm.reload_doc(); + } + } + }); + }); + } + + if (frm.doc.docstatus === 1) { + frm.add_custom_button(__('View Exchange Report'), function() { + frappe.route_options = { + "purchase_invoice": frm.doc.purchase_invoice, + "supplier": frm.doc.supplier + }; + frappe.set_route("query-report", "Import Exchange Differences"); + }); + + // Add debugging button + frm.add_custom_button(__('Debug Payment Linking'), function() { + let payment_entry = prompt(__('Enter Payment Entry name to debug:')); + if (payment_entry) { + frappe.call({ + method: 'av_tools.av_tools_hooks.foreign_import.debug_payment_linking_issue', + args: { + payment_entry_name: payment_entry + }, + callback: function(r) { + if (r.message && !r.message.error) { + let debug_info = r.message; + let msg = `

Payment Details:

+ `; + + if (debug_info.issues.length > 0) { + msg += `

Issues Found:

`; + } + + if (debug_info.potential_trackers.length > 0) { + msg += `

Potential Trackers:

`; + debug_info.potential_trackers.forEach(tracker => { + let color = tracker.issues.length > 0 ? 'red' : 'green'; + msg += `
+ ${tracker.name} (${tracker.purchase_invoice})
+ Currency: ${tracker.currency} | Status: ${tracker.status}
+ Currency Match: ${tracker.currency_match ? '✅' : '❌'} | + Status OK: ${tracker.status_ok ? '✅' : '❌'}`; + if (tracker.issues.length > 0) { + msg += `
Issues: ${tracker.issues.join(', ')}`; + } + msg += `
`; + }); + } else { + msg += `

No trackers found for supplier: ${debug_info.payment_details.party}

`; + } + + frappe.msgprint({ + title: __('Payment Linking Debug Info'), + message: msg, + indicator: 'blue' + }); + } else if (r.message && r.message.error) { + frappe.msgprint(`Error: ${r.message.error}`, 'Error'); + } + } + }); + } + }, __('Debug')); + + // Add manual linking button + frm.add_custom_button(__('Link Payment Manually'), function() { + let payment_entry = prompt(__('Enter Payment Entry name to link:')); + if (payment_entry) { + frappe.call({ + method: 'av_tools.av_tools_hooks.foreign_import.manually_link_payment_to_tracker', + args: { + payment_entry_name: payment_entry, + tracker_name: frm.doc.name + }, + callback: function(r) { + if (r.message && r.message.success) { + frappe.msgprint(r.message.success, 'Success'); + frm.reload_doc(); + } else if (r.message && r.message.error) { + frappe.msgprint(`Error: ${r.message.error}`, 'Error'); + } + } + }); + } + }, __('Debug')); + } + + // Set color indicator based on status + if (frm.doc.status && frm.dashboard) { + let color = { + 'Draft': 'orange', + 'Active': 'blue', + 'Completed': 'green', + 'Cancelled': 'red' + }[frm.doc.status]; + + // Use the correct method for setting indicators + if (frm.dashboard.add_indicator) { + frm.dashboard.add_indicator(__('Status: {0}', [frm.doc.status]), color); + } + } + + // Show total differences summary + if (frm.doc.total_gain_loss && frm.dashboard && frm.dashboard.add_indicator) { + let message = frm.doc.total_gain_loss >= 0 ? + __('Total Exchange Gain: {0}', [format_currency(frm.doc.total_gain_loss)]) : + __('Total Exchange Loss: {0}', [format_currency(Math.abs(frm.doc.total_gain_loss))]); + + frm.dashboard.add_indicator(message, frm.doc.total_gain_loss >= 0 ? 'green' : 'red'); + } + }, + + purchase_invoice: function(frm) { + if (frm.doc.purchase_invoice) { + frappe.call({ + method: 'frappe.client.get', + args: { + doctype: 'Purchase Invoice', + name: frm.doc.purchase_invoice + }, + callback: function(r) { + if (r.message) { + let pi = r.message; + + // Check if it's a foreign currency invoice + frappe.db.get_value('Company', pi.company, 'default_currency') + .then(result => { + if (pi.currency === result.message.default_currency) { + frappe.msgprint(__('Selected Purchase Invoice is not in foreign currency')); + frm.set_value('purchase_invoice', ''); + return; + } + + // Set fields from PI + frm.set_value({ + 'supplier': pi.supplier, + 'transaction_date': pi.posting_date, + 'currency': pi.currency, + 'original_exchange_rate': pi.conversion_rate, + 'invoice_amount_foreign': pi.grand_total, + 'invoice_amount_base': pi.base_grand_total, + 'company': pi.company + }); + }); + } + } + }); + } + } +}); + +frappe.ui.form.on('Foreign Import Payment Details', { + payment_entry: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.payment_entry) { + frappe.call({ + method: 'frappe.client.get', + args: { + doctype: 'Payment Entry', + name: row.payment_entry + }, + callback: function(r) { + if (r.message) { + let pe = r.message; + frappe.model.set_value(cdt, cdn, { + 'payment_date': pe.posting_date, + 'payment_amount_foreign': pe.paid_amount, + 'payment_amount_base': pe.base_paid_amount, + 'payment_exchange_rate': pe.source_exchange_rate + }); + + // Calculate exchange difference + let original_rate = flt(frm.doc.original_exchange_rate); + let payment_rate = flt(pe.source_exchange_rate); + let paid_amount = flt(pe.paid_amount); + + if (original_rate !== payment_rate) { + let exchange_diff = paid_amount * (payment_rate - original_rate); + frappe.model.set_value(cdt, cdn, 'exchange_difference', exchange_diff); + } + } + } + }); + } + } +}); diff --git a/av_tools/av_tools/doctype/foreign_import_transaction/foreign_import_transaction.json b/av_tools/av_tools/doctype/foreign_import_transaction/foreign_import_transaction.json new file mode 100644 index 0000000..98cb02c --- /dev/null +++ b/av_tools/av_tools/doctype/foreign_import_transaction/foreign_import_transaction.json @@ -0,0 +1,275 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "naming_series:", + "creation": "2025-08-19 10:00:00.000000", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "naming_series", + "section_break_basic", + "purchase_invoice", + "supplier", + "transaction_date", + "column_break_basic", + "currency", + "original_exchange_rate", + "status", + "section_break_amounts", + "invoice_amount_foreign", + "invoice_amount_base", + "column_break_amounts", + "total_gain_loss", + "net_difference", + "section_break_lcv", + "landed_cost_vouchers", + "section_break_payments", + "payments", + "section_break_differences", + "exchange_differences", + "section_break_other", + "company", + "column_break_other", + "amended_from" + ], + "fields": [ + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "FIT-.YY.-", + "print_hide": 1, + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "section_break_basic", + "fieldtype": "Section Break", + "label": "Transaction Details" + }, + { + "fieldname": "purchase_invoice", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Purchase Invoice", + "options": "Purchase Invoice", + "reqd": 1 + }, + { + "fetch_from": "purchase_invoice.supplier", + "fieldname": "supplier", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Supplier", + "options": "Supplier", + "read_only": 1 + }, + { + "fetch_from": "purchase_invoice.posting_date", + "fieldname": "transaction_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Transaction Date", + "reqd": 1 + }, + { + "fieldname": "column_break_basic", + "fieldtype": "Column Break" + }, + { + "fetch_from": "purchase_invoice.currency", + "fieldname": "currency", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Currency", + "options": "Currency", + "read_only": 1 + }, + { + "fetch_from": "purchase_invoice.conversion_rate", + "fieldname": "original_exchange_rate", + "fieldtype": "Float", + "label": "Original Exchange Rate", + "precision": "9", + "read_only": 1 + }, + { + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Draft\nActive\nCompleted\nCancelled", + "reqd": 1 + }, + { + "fieldname": "section_break_amounts", + "fieldtype": "Section Break", + "label": "Amounts" + }, + { + "fetch_from": "purchase_invoice.grand_total", + "fieldname": "invoice_amount_foreign", + "fieldtype": "Currency", + "label": "Invoice Amount (Foreign)", + "options": "currency", + "read_only": 1 + }, + { + "fetch_from": "purchase_invoice.base_grand_total", + "fieldname": "invoice_amount_base", + "fieldtype": "Currency", + "label": "Invoice Amount (Base)", + "read_only": 1 + }, + { + "fieldname": "column_break_amounts", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "total_gain_loss", + "fieldtype": "Currency", + "label": "Total Gain/Loss", + "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "net_difference", + "fieldtype": "Currency", + "label": "Net Difference", + "read_only": 1 + }, + { + "fieldname": "section_break_lcv", + "fieldtype": "Section Break", + "label": "Landed Cost Vouchers" + }, + { + "allow_on_submit": 1, + "fieldname": "landed_cost_vouchers", + "fieldtype": "Table", + "label": "Landed Cost Vouchers", + "options": "Foreign Import LCV Details" + }, + { + "fieldname": "section_break_payments", + "fieldtype": "Section Break", + "label": "Payments" + }, + { + "allow_on_submit": 1, + "fieldname": "payments", + "fieldtype": "Table", + "label": "Payment Details", + "options": "Foreign Import Payment Details" + }, + { + "fieldname": "section_break_differences", + "fieldtype": "Section Break", + "label": "Exchange Differences" + }, + { + "allow_on_submit": 1, + "fieldname": "exchange_differences", + "fieldtype": "Table", + "label": "Exchange Difference Details", + "options": "Foreign Import Exchange Difference Details" + }, + { + "fieldname": "section_break_other", + "fieldtype": "Section Break", + "label": "Other Details" + }, + { + "fetch_from": "purchase_invoice.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "column_break_other", + "fieldtype": "Column Break" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Foreign Import Transaction", + "print_hide": 1, + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [ + { + "link_doctype": "Purchase Invoice", + "link_fieldname": "name" + }, + { + "link_doctype": "Payment Entry", + "link_fieldname": "party" + }, + { + "link_doctype": "Journal Entry", + "link_fieldname": "user_remark" + } + ], + "modified": "2025-08-19 10:00:00.000000", + "modified_by": "Administrator", + "module": "Av Tools", + "name": "Foreign Import Transaction", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "purchase_invoice", + "track_changes": 1 +} \ No newline at end of file diff --git a/av_tools/av_tools/doctype/foreign_import_transaction/foreign_import_transaction.py b/av_tools/av_tools/doctype/foreign_import_transaction/foreign_import_transaction.py new file mode 100644 index 0000000..d032b73 --- /dev/null +++ b/av_tools/av_tools/doctype/foreign_import_transaction/foreign_import_transaction.py @@ -0,0 +1,194 @@ +# Copyright (c) 2025, Aakvatech and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe.utils import flt, getdate, nowdate +from frappe import _ + + +class ForeignImportTransaction(Document): + def validate(self): + self.validate_currency() + self.calculate_totals() + self.set_status() + + def on_submit(self): + self.update_status("Active") + + def on_cancel(self): + self.cancel_related_journal_entries() + self.update_status("Cancelled") + + def validate_currency(self): + """Validate that the purchase invoice is in foreign currency""" + if not self.currency: + frappe.throw("Currency is required") + + company_currency = frappe.get_cached_value( + "Company", self.company, "default_currency" + ) + if self.currency == company_currency: + frappe.throw( + f"Foreign Import Transaction can only be created for foreign currency invoices. Company currency is {company_currency}" + ) + + def calculate_totals(self): + """Calculate total gains and losses""" + total_gain = 0 + total_loss = 0 + + for diff in self.exchange_differences: + if diff.difference_type == "Gain": + total_gain += flt(diff.amount) + else: + total_loss += flt(diff.amount) + + self.total_gain_loss = total_gain - total_loss + self.net_difference = self.total_gain_loss + + def set_status(self): + """Set status based on completion""" + if self.docstatus == 0: + self.status = "Draft" + elif self.docstatus == 1: + # Check if all payments are made + invoice_amount = flt(self.invoice_amount_foreign) + total_paid = sum([flt(p.payment_amount_foreign) for p in self.payments]) + + if total_paid >= invoice_amount: + self.status = "Completed" + else: + self.status = "Active" + else: + self.status = "Cancelled" + + def update_status(self, status): + """Update status without triggering validations""" + frappe.db.set_value(self.doctype, self.name, "status", status) + + def cancel_related_journal_entries(self): + """Cancel all related journal entries""" + for diff in self.exchange_differences: + if diff.journal_entry: + try: + je = frappe.get_doc("Journal Entry", diff.journal_entry) + if je.docstatus == 1: + je.cancel() + except Exception as e: + frappe.log_error( + f"Error cancelling Journal Entry {diff.journal_entry}: {str(e)}" + ) + + def add_exchange_difference( + self, + reference_type, + reference_name, + difference_type, + amount, + posting_date, + remarks, + journal_entry=None, + ): + """Add exchange difference entry""" + diff_row = self.append("exchange_differences", {}) + diff_row.reference_type = reference_type + diff_row.reference_name = reference_name + diff_row.difference_type = difference_type + diff_row.amount = round(amount, 3) + diff_row.posting_date = posting_date + diff_row.remarks = remarks + diff_row.journal_entry = journal_entry + + self.calculate_totals() + self.save() + + return diff_row + + def add_payment_detail(self, payment_entry): + """Add payment detail from Payment Entry""" + for existing in self.payments: + if existing.payment_entry == payment_entry: + return existing + payment_doc = frappe.get_doc("Payment Entry", payment_entry) + + payment_row = self.append("payments", {}) + payment_row.payment_entry = payment_entry + payment_row.payment_date = payment_doc.posting_date + payment_row.payment_amount_foreign = payment_doc.paid_amount + payment_row.payment_amount_base = payment_doc.base_paid_amount + payment_row.payment_exchange_rate = payment_doc.source_exchange_rate + + # Calculate exchange difference + original_rate = flt(self.original_exchange_rate) + payment_rate = flt(payment_doc.source_exchange_rate) + paid_amount = flt(payment_doc.paid_amount) + + if original_rate != payment_rate: + exchange_diff = paid_amount * (payment_rate - original_rate) + payment_row.exchange_difference = exchange_diff + + self.save() + return payment_row + + def add_lcv_detail(self, lcv_name): + """Add LCV detail from Landed Cost Voucher""" + for existing in self.landed_cost_vouchers: + if existing.landed_cost_voucher == lcv_name: + return existing + lcv_doc = frappe.get_doc("Landed Cost Voucher", lcv_name) + + lcv_row = self.append("landed_cost_vouchers", {}) + lcv_row.landed_cost_voucher = lcv_name + lcv_row.lcv_date = lcv_doc.posting_date + lcv_row.lcv_amount_base = lcv_doc.total_taxes_and_charges + lcv_row.exchange_rate_used = flt(lcv_doc.get("conversion_rate", 1)) + + # Get allocated amount from LCV items + allocated_amount = 0 + for item in lcv_doc.items: + allocated_amount += flt(item.applicable_charges) + lcv_row.allocated_to_items = allocated_amount + + self.save() + return lcv_row + + @frappe.whitelist() + def recalculate_differences(self): + """Manually recalculate all exchange differences""" + # Import here to avoid circular imports + from av_tools.av_tools_hooks.foreign_import import ( + recalculate_import_differences, + ) + + return recalculate_import_differences(self.name) + + @frappe.whitelist() + def get_exchange_summary(self): + """Get summary of exchange differences""" + summary = { + "total_gain": 0, + "total_loss": 0, + "payment_differences": 0, + "lcv_differences": 0, + "manual_entries": 0, + } + + for diff in self.exchange_differences: + amount = flt(diff.amount) + if diff.difference_type == "Gain": + summary["total_gain"] += amount + else: + summary["total_loss"] += amount + + # Categorize by reference type + if diff.reference_type == "Payment Entry": + summary["payment_differences"] += amount + elif diff.reference_type == "Landed Cost Voucher": + summary["lcv_differences"] += amount + else: + summary["manual_entries"] += amount + + summary["net_difference"] = summary["total_gain"] - summary["total_loss"] + + return summary diff --git a/av_tools/av_tools/doctype/foreign_import_transaction/test_foreign_import_transaction.py b/av_tools/av_tools/doctype/foreign_import_transaction/test_foreign_import_transaction.py new file mode 100644 index 0000000..5704e1d --- /dev/null +++ b/av_tools/av_tools/doctype/foreign_import_transaction/test_foreign_import_transaction.py @@ -0,0 +1,435 @@ +# Copyright (c) 2025, Aakvatech and Contributors +# See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import nowdate +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + + +class TestForeignImportTransaction(FrappeTestCase): + def setUp(self): + """Set up test data""" + self.company = "_Test Company" + self.supplier = "_Test Supplier USD" + self.currency = "USD" + self.original_rate = 2500.0 # 1 USD = 2500 TZS + self.payment_rate = 2600.0 # 1 USD = 2600 TZS (currency strengthened) + + # Create test supplier group if not exists + if not frappe.db.exists("Supplier Group", "_Test Supplier Group"): + supplier_group = frappe.get_doc({ + "doctype": "Supplier Group", + "supplier_group_name": "_Test Supplier Group" + }) + supplier_group.insert(ignore_permissions=True) + + # Create test supplier if not exists + if not frappe.db.exists("Supplier", self.supplier): + supplier_doc = frappe.get_doc({ + "doctype": "Supplier", + "supplier_name": self.supplier, + "supplier_group": "_Test Supplier Group", + "supplier_type": "Company" + }) + supplier_doc.insert(ignore_permissions=True) + + # Create Foreign Import Settings if not exists + if not frappe.db.exists("Foreign Import Settings"): + settings = frappe.get_doc({ + "doctype": "Foreign Import Settings", + "company": self.company, + "exchange_difference_threshold": 0.01, + "auto_create_journal_entries": 0, # Disable to avoid account setup issues + "enable_lcv_exchange_tracking": 1 + }) + settings.insert(ignore_permissions=True) + + def tearDown(self): + """Clean up test data""" + # Cancel and delete test documents + frappe.db.rollback() + + def test_automatic_tracker_creation_on_foreign_pi_submit(self): + """Test that Foreign Import Transaction is created automatically when foreign PI is submitted""" + # Create foreign currency Purchase Invoice + pi = make_purchase_invoice( + supplier=self.supplier, + currency=self.currency, + conversion_rate=self.original_rate, + rate=100, + do_not_submit=True + ) + + # Check no tracker exists before submission + tracker_before = frappe.db.exists("Foreign Import Transaction", {"purchase_invoice": pi.name}) + self.assertIsNone(tracker_before) + + # Submit the Purchase Invoice + pi.submit() + + # Check tracker is created after submission + tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": pi.name}, "name") + self.assertIsNotNone(tracker_name) + + # Verify tracker details + tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) + self.assertEqual(tracker.purchase_invoice, pi.name) + self.assertEqual(tracker.supplier, pi.supplier) + self.assertEqual(tracker.currency, pi.currency) + self.assertEqual(tracker.original_exchange_rate, pi.conversion_rate) + self.assertEqual(tracker.invoice_amount_foreign, pi.grand_total) + self.assertEqual(tracker.invoice_amount_base, pi.base_grand_total) + self.assertEqual(tracker.status, "Active") + self.assertEqual(tracker.docstatus, 1) + + def test_no_tracker_creation_for_base_currency_pi(self): + """Test that no tracker is created for base currency Purchase Invoice""" + # Get company's default currency + company_currency = frappe.get_cached_value("Company", self.company, "default_currency") + + # Create base currency Purchase Invoice + pi = make_purchase_invoice( + supplier="_Test Supplier", + currency=company_currency, # Use actual company currency + rate=100, + do_not_submit=True + ) + + pi.submit() + + # Check no tracker is created + tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": pi.name}, "name") + self.assertIsNone(tracker_name) + + def test_payment_entry_linking_and_exchange_calculation(self): + """Test that Payment Entry links to tracker and calculates exchange differences""" + # Create foreign currency Purchase Invoice + pi = make_purchase_invoice( + supplier=self.supplier, + currency=self.currency, + conversion_rate=self.original_rate, + rate=100, + qty=10, # Total: 1000 USD + do_not_submit=True + ) + pi.submit() + + # Get the created tracker + tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": pi.name}, "name") + tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) + + # Create Payment Entry with different exchange rate + pe = get_payment_entry("Purchase Invoice", pi.name) + pe.source_exchange_rate = self.payment_rate # Different rate + pe.paid_amount = 500 # Pay half the invoice + pe.base_paid_amount = 500 * self.payment_rate + pe.insert() + pe.submit() + + # Reload tracker to check if payment was linked + tracker.reload() + + # Verify payment was linked + self.assertEqual(len(tracker.payments), 1) + payment_row = tracker.payments[0] + self.assertEqual(payment_row.payment_entry, pe.name) + self.assertEqual(payment_row.payment_amount_foreign, 500) + self.assertEqual(payment_row.payment_exchange_rate, self.payment_rate) + + # Verify exchange difference calculation + expected_diff = 500 * (self.payment_rate - self.original_rate) # 500 * (2600 - 2500) = 50,000 + self.assertEqual(payment_row.exchange_difference, expected_diff) + + # Verify exchange difference entry was created + self.assertEqual(len(tracker.exchange_differences), 1) + diff_row = tracker.exchange_differences[0] + self.assertEqual(diff_row.reference_type, "Payment Entry") + self.assertEqual(diff_row.reference_name, pe.name) + self.assertEqual(diff_row.difference_type, "Gain") # Rate increased + self.assertEqual(diff_row.amount, expected_diff) + + # Verify status is still Active (partial payment) + self.assertEqual(tracker.status, "Active") + + def test_status_change_to_completed_on_full_payment(self): + """Test that status changes to Completed when full payment is made""" + # Create foreign currency Purchase Invoice + pi = make_purchase_invoice( + supplier=self.supplier, + currency=self.currency, + conversion_rate=self.original_rate, + rate=100, + qty=10, # Total: 1000 USD + do_not_submit=True + ) + pi.submit() + + # Get the created tracker + tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": pi.name}, "name") + tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) + + # Create Payment Entry for full amount + pe = get_payment_entry("Purchase Invoice", pi.name) + pe.source_exchange_rate = self.payment_rate + pe.paid_amount = 1000 # Full payment + pe.base_paid_amount = 1000 * self.payment_rate + pe.insert() + pe.submit() + + # Reload tracker + tracker.reload() + + # Verify status changed to Completed + self.assertEqual(tracker.status, "Completed") + + def test_exchange_loss_calculation(self): + """Test exchange loss calculation when currency weakens""" + # Create foreign currency Purchase Invoice + pi = make_purchase_invoice( + supplier=self.supplier, + currency=self.currency, + conversion_rate=self.original_rate, + rate=100, + qty=10, + do_not_submit=True + ) + pi.submit() + + # Get the created tracker + tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": pi.name}, "name") + tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) + + # Create Payment Entry with lower exchange rate (currency weakened) + weaker_rate = 2400.0 # 1 USD = 2400 TZS (currency weakened) + pe = get_payment_entry("Purchase Invoice", pi.name) + pe.source_exchange_rate = weaker_rate + pe.paid_amount = 500 + pe.base_paid_amount = 500 * weaker_rate + pe.insert() + pe.submit() + + # Reload tracker + tracker.reload() + + # Verify exchange loss calculation + expected_diff = 500 * (weaker_rate - self.original_rate) # 500 * (2400 - 2500) = -50,000 + payment_row = tracker.payments[0] + self.assertEqual(payment_row.exchange_difference, expected_diff) + + # Verify exchange difference entry shows Loss + diff_row = tracker.exchange_differences[0] + self.assertEqual(diff_row.difference_type, "Loss") + self.assertEqual(diff_row.amount, abs(expected_diff)) # Amount is always positive + + def test_tracker_cancellation_on_pi_cancel(self): + """Test that tracker is cancelled when Purchase Invoice is cancelled""" + # Create and submit foreign currency Purchase Invoice + pi = make_purchase_invoice( + supplier=self.supplier, + currency=self.currency, + conversion_rate=self.original_rate, + rate=100, + do_not_submit=True + ) + pi.submit() + + # Get the created tracker + tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": pi.name}, "name") + tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) + self.assertEqual(tracker.docstatus, 1) + + # Cancel the Purchase Invoice + pi.cancel() + + # Reload tracker and verify it's cancelled + tracker.reload() + self.assertEqual(tracker.docstatus, 2) + self.assertEqual(tracker.status, "Cancelled") + + def test_currency_validation(self): + """Test that tracker validates foreign currency requirement""" + # Get company's default currency + company_currency = frappe.get_cached_value("Company", self.company, "default_currency") + + # Create tracker manually with same currency as company + tracker = frappe.get_doc({ + "doctype": "Foreign Import Transaction", + "purchase_invoice": "TEST-PI-001", + "supplier": self.supplier, + "currency": company_currency, # Same as company currency + "company": self.company + }) + + # Should throw error on validation + with self.assertRaises(frappe.ValidationError): + tracker.insert() + + def test_totals_calculation(self): + """Test that totals are calculated correctly""" + # Create foreign currency Purchase Invoice + pi = make_purchase_invoice( + supplier=self.supplier, + currency=self.currency, + conversion_rate=self.original_rate, + rate=100, + qty=10, + do_not_submit=True + ) + pi.submit() + + # Get the created tracker + tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": pi.name}, "name") + tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) + + # Add manual exchange differences + tracker.add_exchange_difference( + "Manual Entry", "TEST-001", "Gain", 25000, nowdate(), "Test gain" + ) + tracker.add_exchange_difference( + "Manual Entry", "TEST-002", "Loss", 15000, nowdate(), "Test loss" + ) + + # Verify totals + self.assertEqual(tracker.total_gain_loss, 10000) # 25000 - 15000 + self.assertEqual(tracker.net_difference, 10000) + + def test_exchange_summary_method(self): + """Test the get_exchange_summary method""" + # Create foreign currency Purchase Invoice + pi = make_purchase_invoice( + supplier=self.supplier, + currency=self.currency, + conversion_rate=self.original_rate, + rate=100, + qty=10, + do_not_submit=True + ) + pi.submit() + + # Get the created tracker + tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": pi.name}, "name") + tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) + + # Create Payment Entry + pe = get_payment_entry("Purchase Invoice", pi.name) + pe.source_exchange_rate = self.payment_rate + pe.paid_amount = 500 + pe.base_paid_amount = 500 * self.payment_rate + pe.insert() + pe.submit() + + # Reload tracker + tracker.reload() + + # Get exchange summary + summary = tracker.get_exchange_summary() + + # Verify summary + expected_gain = 500 * (self.payment_rate - self.original_rate) + self.assertEqual(summary["total_gain"], expected_gain) + self.assertEqual(summary["total_loss"], 0) + self.assertEqual(summary["payment_differences"], expected_gain) + self.assertEqual(summary["lcv_differences"], 0) + self.assertEqual(summary["manual_entries"], 0) + self.assertEqual(summary["net_difference"], expected_gain) + + def test_no_duplicate_tracker_creation(self): + """Test that duplicate trackers are not created for same PI""" + # Create foreign currency Purchase Invoice + pi = make_purchase_invoice( + supplier=self.supplier, + currency=self.currency, + conversion_rate=self.original_rate, + rate=100, + do_not_submit=True + ) + pi.submit() + + # Get initial tracker count + initial_count = frappe.db.count("Foreign Import Transaction", {"purchase_invoice": pi.name}) + self.assertEqual(initial_count, 1) + + # Try to trigger tracker creation again (simulate hook being called again) + from av_tools.av_tools_hooks.foreign_import import create_import_tracker + create_import_tracker(pi, "on_submit") + + # Verify no duplicate tracker was created + final_count = frappe.db.count("Foreign Import Transaction", {"purchase_invoice": pi.name}) + self.assertEqual(final_count, 1) + + def test_payment_currency_mismatch_no_linking(self): + """Test that payment with different currency doesn't link to tracker""" + # Create foreign currency Purchase Invoice in USD + pi = make_purchase_invoice( + supplier=self.supplier, + currency=self.currency, # USD + conversion_rate=self.original_rate, + rate=100, + do_not_submit=True + ) + pi.submit() + + # Get the created tracker + tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": pi.name}, "name") + tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) + + # Create Payment Entry in different currency (EUR) + pe = get_payment_entry("Purchase Invoice", pi.name) + pe.paid_to_account_currency = "EUR" # Different currency + pe.source_exchange_rate = 2800.0 # EUR rate + pe.paid_amount = 500 + pe.base_paid_amount = 500 * 2800 + pe.insert() + pe.submit() + + # Reload tracker and verify no payment was linked + tracker.reload() + self.assertEqual(len(tracker.payments), 0) + self.assertEqual(len(tracker.exchange_differences), 0) + + def test_recalculate_differences_method(self): + """Test the recalculate_differences method""" + # Create foreign currency Purchase Invoice + pi = make_purchase_invoice( + supplier=self.supplier, + currency=self.currency, + conversion_rate=self.original_rate, + rate=100, + qty=10, + do_not_submit=True + ) + pi.submit() + + # Get the created tracker + tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": pi.name}, "name") + tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) + + # Create Payment Entry + pe = get_payment_entry("Purchase Invoice", pi.name) + pe.source_exchange_rate = self.payment_rate + pe.paid_amount = 500 + pe.base_paid_amount = 500 * self.payment_rate + pe.insert() + pe.submit() + + # Reload tracker + tracker.reload() + initial_differences_count = len(tracker.exchange_differences) + + # Clear exchange differences manually + tracker.exchange_differences = [] + tracker.save() + + # Verify differences are cleared + tracker.reload() + self.assertEqual(len(tracker.exchange_differences), 0) + + # Recalculate differences + result = tracker.recalculate_differences() + self.assertTrue(result) + + # Verify differences are recalculated + tracker.reload() + self.assertEqual(len(tracker.exchange_differences), initial_differences_count) diff --git a/av_tools/av_tools/doctype/import_file/__init__.py b/av_tools/av_tools/doctype/import_file/__init__.py new file mode 100644 index 0000000..b0c1005 --- /dev/null +++ b/av_tools/av_tools/doctype/import_file/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2021, Aakvatech and contributors + diff --git a/av_tools/av_tools/doctype/import_file/import_file.js b/av_tools/av_tools/doctype/import_file/import_file.js new file mode 100644 index 0000000..07f9bdc --- /dev/null +++ b/av_tools/av_tools/doctype/import_file/import_file.js @@ -0,0 +1,4 @@ +// Copyright (c) 2021, Aakvatech and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Import File", {}); diff --git a/av_tools/av_tools/doctype/import_file/import_file.json b/av_tools/av_tools/doctype/import_file/import_file.json new file mode 100644 index 0000000..2060ac4 --- /dev/null +++ b/av_tools/av_tools/doctype/import_file/import_file.json @@ -0,0 +1,98 @@ +{ + "actions": [], + "autoname": "field:import_file_number", + "creation": "2021-01-20 10:53:40.586792", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "import_file_number", + "bl_number", + "no_of_containers", + "tansad_no", + "clearing_agent_name", + "column_break_6", + "supplier_name", + "supplier_invoice_no", + "invoice_currency", + "invoice_amount" + ], + "fields": [ + { + "fieldname": "import_file_number", + "fieldtype": "Data", + "label": "Import File Number", + "unique": 1 + }, + { + "fieldname": "bl_number", + "fieldtype": "Data", + "label": "BL Number" + }, + { + "fieldname": "no_of_containers", + "fieldtype": "Data", + "label": "No of Containers" + }, + { + "fieldname": "clearing_agent_name", + "fieldtype": "Data", + "label": "Clearing Agent Name" + }, + { + "fieldname": "supplier_name", + "fieldtype": "Link", + "label": "Supplier Name", + "options": "Supplier" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "supplier_invoice_no", + "fieldtype": "Data", + "label": "Supplier Invoice No" + }, + { + "fieldname": "invoice_currency", + "fieldtype": "Link", + "label": "Invoice Currency", + "options": "Currency" + }, + { + "fieldname": "invoice_amount", + "fieldtype": "Currency", + "label": "Invoice Amount" + }, + { + "fieldname": "tansad_no", + "fieldtype": "Data", + "label": "TANSAD No." + } + ], + "links": [], + "modified": "2022-03-01 15:26:26.262866", + "modified_by": "Administrator", + "module": "Av Tools", + "name": "Import File", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "search_fields": "supplier_name, bl_number, supplier_invoice_no", + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} diff --git a/av_tools/av_tools/doctype/import_file/import_file.py b/av_tools/av_tools/doctype/import_file/import_file.py new file mode 100644 index 0000000..afc72b8 --- /dev/null +++ b/av_tools/av_tools/doctype/import_file/import_file.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Aakvatech and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from frappe.model.document import Document + + +class ImportFile(Document): + pass diff --git a/av_tools/av_tools/landed_cost_voucher.js b/av_tools/av_tools/landed_cost_voucher.js new file mode 100644 index 0000000..61efcdb --- /dev/null +++ b/av_tools/av_tools/landed_cost_voucher.js @@ -0,0 +1,59 @@ +frappe.ui.form.on("Landed Cost Voucher", { + validate(frm) { + (frm.doc.items || []).forEach((d) => { + if (!d.qty) return; + const applicable_item = (d.applicable_charges || 0) / d.qty; + const price_item = applicable_item + (d.amount || 0) / d.qty; + frappe.model.set_value( + d.doctype, + d.name, + "applicable_charges_per_item", + applicable_item + ); + frappe.model.set_value( + d.doctype, + d.name, + "price_per_item", + price_item + ); + }); + }, + import_file(frm) { + frm.clear_table("taxes"); + if (frm.doc.import_file) { + frappe.call({ + method: "av_tools.av_tools_hooks.landed_cost_voucher.get_landed_cost_expenses", + args: { + import_file: frm.doc.import_file, + }, + async: false, + callback(r) { + if (r.message) { + r.message.forEach((element) => { + const child = frm.add_child("taxes"); + frappe.model.set_value( + child.doctype, + child.name, + "expense_account", + element.expense_account + ); + frappe.model.set_value( + child.doctype, + child.name, + "description", + element.description + ); + frappe.model.set_value( + child.doctype, + child.name, + "amount", + element.amount + ); + }); + } + }, + }); + } + frm.refresh_field("taxes"); + }, +}); diff --git a/av_tools/av_tools/report/import_exchange_differences/__init__.py b/av_tools/av_tools/report/import_exchange_differences/__init__.py new file mode 100644 index 0000000..449800f --- /dev/null +++ b/av_tools/av_tools/report/import_exchange_differences/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2025, Aakvatech and contributors +# For license information, please see license.txt diff --git a/av_tools/av_tools/report/import_exchange_differences/import_exchange_differences.js b/av_tools/av_tools/report/import_exchange_differences/import_exchange_differences.js new file mode 100644 index 0000000..965955a --- /dev/null +++ b/av_tools/av_tools/report/import_exchange_differences/import_exchange_differences.js @@ -0,0 +1,54 @@ +// Copyright (c) 2025, Aakvatech and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Import Exchange Differences"] = { + "filters": [ + { + "fieldname": "company", + "fieldtype": "Link", + "label": __("Company"), + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, + { + "fieldname": "from_date", + "fieldtype": "Date", + "label": __("From Date"), + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + "reqd": 1 + }, + { + "fieldname": "to_date", + "fieldtype": "Date", + "label": __("To Date"), + "default": frappe.datetime.get_today(), + "reqd": 1 + }, + { + "fieldname": "purchase_invoice", + "fieldtype": "Link", + "label": __("Purchase Invoice"), + "options": "Purchase Invoice" + }, + { + "fieldname": "supplier", + "fieldtype": "Link", + "label": __("Supplier"), + "options": "Supplier" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "label": __("Currency"), + "options": "Currency" + }, + { + "fieldname": "status", + "fieldtype": "Select", + "label": __("Status"), + "options": "\nDraft\nActive\nCompleted\nCancelled" + } + ] +}; diff --git a/av_tools/av_tools/report/import_exchange_differences/import_exchange_differences.json b/av_tools/av_tools/report/import_exchange_differences/import_exchange_differences.json new file mode 100644 index 0000000..c15781d --- /dev/null +++ b/av_tools/av_tools/report/import_exchange_differences/import_exchange_differences.json @@ -0,0 +1,31 @@ +{ + "add_total_row": 1, + "columns": [], + "creation": "2025-08-20 10:00:00.000000", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2025-08-20 10:00:00.000000", + "modified_by": "Administrator", + "module": "Av Tools", + "name": "Import Exchange Differences", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Foreign Import Transaction", + "report_name": "Import Exchange Differences", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Accounts Manager" + }, + { + "role": "Accounts User" + } + ] +} \ No newline at end of file diff --git a/av_tools/av_tools/report/import_exchange_differences/import_exchange_differences.py b/av_tools/av_tools/report/import_exchange_differences/import_exchange_differences.py new file mode 100644 index 0000000..c61bc9e --- /dev/null +++ b/av_tools/av_tools/report/import_exchange_differences/import_exchange_differences.py @@ -0,0 +1,176 @@ +# Copyright (c) 2025, Aakvatech and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.utils import flt, getdate, cstr + + +def execute(filters=None): + if not filters: + filters = {} + + columns = get_columns() + data = get_data(filters) + + return columns, data + + +def get_columns(): + return [ + { + "fieldname": "foreign_import_transaction", + "label": _("Import Transaction"), + "fieldtype": "Link", + "options": "Foreign Import Transaction", + "width": 150 + }, + { + "fieldname": "purchase_invoice", + "label": _("Purchase Invoice"), + "fieldtype": "Link", + "options": "Purchase Invoice", + "width": 150 + }, + { + "fieldname": "supplier", + "label": _("Supplier"), + "fieldtype": "Link", + "options": "Supplier", + "width": 150 + }, + { + "fieldname": "transaction_date", + "label": _("Transaction Date"), + "fieldtype": "Date", + "width": 100 + }, + { + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Link", + "options": "Currency", + "width": 80 + }, + { + "fieldname": "original_exchange_rate", + "label": _("Original Rate"), + "fieldtype": "Float", + "precision": 4, + "width": 100 + }, + { + "fieldname": "invoice_amount_foreign", + "label": _("Invoice Amount (Foreign)"), + "fieldtype": "Currency", + "options": "currency", + "width": 150 + }, + { + "fieldname": "invoice_amount_base", + "label": _("Invoice Amount (Base)"), + "fieldtype": "Currency", + "width": 150 + }, + { + "fieldname": "total_payments", + "label": _("Total Payments"), + "fieldtype": "Currency", + "options": "currency", + "width": 120 + }, + { + "fieldname": "payment_differences", + "label": _("Payment Differences"), + "fieldtype": "Currency", + "width": 120 + }, + { + "fieldname": "lcv_differences", + "label": _("LCV Differences"), + "fieldtype": "Currency", + "width": 120 + }, + { + "fieldname": "total_gain_loss", + "label": _("Total Gain/Loss"), + "fieldtype": "Currency", + "width": 120 + }, + { + "fieldname": "status", + "label": _("Status"), + "fieldtype": "Data", + "width": 100 + } + ] + + +def get_data(filters): + conditions = get_conditions(filters) + + query = """ + SELECT + fit.name as foreign_import_transaction, + fit.purchase_invoice, + fit.supplier, + fit.transaction_date, + fit.currency, + fit.original_exchange_rate, + fit.invoice_amount_foreign, + fit.invoice_amount_base, + fit.total_gain_loss, + fit.status, + COALESCE(payment_summary.total_payments, 0) as total_payments, + COALESCE(payment_summary.payment_differences, 0) as payment_differences, + COALESCE(lcv_summary.lcv_differences, 0) as lcv_differences + FROM `tabForeign Import Transaction` fit + LEFT JOIN ( + SELECT + parent, + SUM(payment_amount_foreign) as total_payments, + SUM(exchange_difference) as payment_differences + FROM `tabForeign Import Payment Details` + GROUP BY parent + ) payment_summary ON payment_summary.parent = fit.name + LEFT JOIN ( + SELECT + parent, + SUM(exchange_difference) as lcv_differences + FROM `tabForeign Import LCV Details` + GROUP BY parent + ) lcv_summary ON lcv_summary.parent = fit.name + WHERE fit.docstatus = 1 {conditions} + ORDER BY fit.transaction_date DESC, fit.name + """.format(conditions=conditions) + + data = frappe.db.sql(query, filters, as_dict=1) + + return data + + +def get_conditions(filters): + conditions = [] + + if filters.get("company"): + conditions.append("AND fit.company = %(company)s") + + if filters.get("from_date"): + conditions.append("AND fit.transaction_date >= %(from_date)s") + + if filters.get("to_date"): + conditions.append("AND fit.transaction_date <= %(to_date)s") + + if filters.get("purchase_invoice"): + conditions.append("AND fit.purchase_invoice = %(purchase_invoice)s") + + if filters.get("supplier"): + conditions.append("AND fit.supplier = %(supplier)s") + + if filters.get("currency"): + conditions.append("AND fit.currency = %(currency)s") + + if filters.get("status"): + conditions.append("AND fit.status = %(status)s") + + return " ".join(conditions) diff --git a/av_tools/av_tools_hooks/foreign_import.py b/av_tools/av_tools_hooks/foreign_import.py new file mode 100644 index 0000000..d051a98 --- /dev/null +++ b/av_tools/av_tools_hooks/foreign_import.py @@ -0,0 +1,727 @@ +import frappe +from frappe.utils import flt, getdate, nowdate, add_days +from frappe import _ +import json + + +def create_import_tracker(doc, method): + """Create Foreign Import Transaction when Purchase Invoice is submitted""" + if not doc.currency: + return + + company_currency = frappe.get_cached_value( + "Company", doc.company, "default_currency" + ) + + # Only create tracker for foreign currency invoices + if doc.currency == company_currency: + return + + # Check if tracker already exists + existing = frappe.db.exists( + "Foreign Import Transaction", {"purchase_invoice": doc.name} + ) + if existing: + return + + try: + import_tracker = frappe.new_doc("Foreign Import Transaction") + import_tracker.purchase_invoice = doc.name + import_tracker.supplier = doc.supplier + import_tracker.transaction_date = doc.posting_date + import_tracker.currency = doc.currency + import_tracker.original_exchange_rate = doc.conversion_rate + import_tracker.invoice_amount_foreign = doc.grand_total + import_tracker.invoice_amount_base = doc.base_grand_total + import_tracker.company = doc.company + import_tracker.status = "Draft" + + import_tracker.insert() + import_tracker.submit() + + # Add custom field reference + frappe.db.set_value( + "Purchase Invoice", doc.name, "foreign_import_tracker", import_tracker.name + ) + + frappe.msgprint( + _("Foreign Import Transaction {0} created successfully").format( + import_tracker.name + ), + alert=True, + ) + + except Exception as e: + frappe.log_error( + f"Error creating Foreign Import Transaction for {doc.name}: {str(e)}" + ) + + +def cancel_import_tracker(doc, method): + """Cancel Foreign Import Transaction when Purchase Invoice is cancelled""" + tracker_name = frappe.db.get_value( + "Foreign Import Transaction", {"purchase_invoice": doc.name}, "name" + ) + + if tracker_name: + try: + tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) + if tracker.docstatus == 1: + tracker.cancel() + except Exception as e: + frappe.log_error( + "Error on Cancelling Foreign Import Transaction", + f"Error cancelling Foreign Import Transaction {tracker_name}: {str(e)}", + ) + + +def link_lcv_to_import_tracker(doc, method): + """Link Landed Cost Voucher to Foreign Import Transaction""" + if not doc.purchase_receipts: + return + + for pr_row in doc.purchase_receipts: + receipt_doc = pr_row.get("receipt_document") or pr_row.get("purchase_receipt") + if not receipt_doc: + continue + + # Find related purchase invoice through purchase receipt + if pr_row.get("receipt_document_type") == "Purchase Invoice": + pi_items = [{"purchase_invoice": receipt_doc}] + else: + pi_items = frappe.db.sql( + """ + SELECT DISTINCT pii.parent as purchase_invoice + FROM `tabPurchase Invoice Item` pii + WHERE pii.purchase_receipt = %s + """, + receipt_doc, + as_dict=True, + ) + + for pi_item in pi_items: + purchase_invoice = pi_item.get("purchase_invoice") + if not purchase_invoice: + continue + tracker_name = frappe.db.get_value( + "Foreign Import Transaction", + {"purchase_invoice": purchase_invoice}, + "name", + ) + + if tracker_name: + try: + tracker_doc = frappe.get_doc( + "Foreign Import Transaction", tracker_name + ) + + # Check if LCV already added + existing_lcv = any( + row.landed_cost_voucher == doc.name + for row in tracker_doc.landed_cost_vouchers + ) + if not existing_lcv: + tracker_doc.add_lcv_detail(doc.name) + + # Calculate LCV exchange difference + calculate_lcv_exchange_difference(tracker_doc, doc) + + except Exception as e: + frappe.log_error( + f"Error linking LCV {doc.name} to tracker {tracker_name}: {str(e)}" + ) + + +def unlink_lcv_from_import_tracker(doc, method): + """Remove LCV from Foreign Import Transaction when cancelled""" + trackers = frappe.db.sql( + """ + SELECT DISTINCT fit.name + FROM `tabForeign Import Transaction` fit + JOIN `tabForeign Import LCV Details` lcv ON lcv.parent = fit.name + WHERE lcv.landed_cost_voucher = %s + """, + doc.name, + as_dict=True, + ) + + for tracker in trackers: + try: + tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker.name) + + # Remove LCV rows + tracker_doc.landed_cost_vouchers = [ + row + for row in tracker_doc.landed_cost_vouchers + if row.landed_cost_voucher != doc.name + ] + + # Remove related exchange difference entries + tracker_doc.exchange_differences = [ + row + for row in tracker_doc.exchange_differences + if not ( + row.reference_type == "Landed Cost Voucher" + and row.reference_name == doc.name + ) + ] + + tracker_doc.flags.ignore_validate_update_after_submit = True + tracker_doc.save() + + except Exception as e: + frappe.log_error( + f"Error unlinking LCV {doc.name} from tracker {tracker.name}: {str(e)}" + ) + + +def link_payment_to_import_tracker(doc, method): + """Link Payment Entry to Foreign Import Transaction""" + if doc.payment_type != "Pay" or doc.party_type != "Supplier": + return + + # Find active import trackers for this supplier + trackers = frappe.db.sql( + """ + SELECT name, purchase_invoice, currency, original_exchange_rate, invoice_amount_foreign + FROM `tabForeign Import Transaction` + WHERE supplier = %s AND status IN ('Active', 'Draft') AND docstatus = 1 + ORDER BY transaction_date DESC + """, + doc.party, + as_dict=True, + ) + + for tracker_data in trackers: + # Check if payment currency matches tracker currency + if doc.paid_to_account_currency == tracker_data.currency: + try: + tracker_doc = frappe.get_doc( + "Foreign Import Transaction", tracker_data.name + ) + + # Check if payment already added + existing_payment = any( + row.payment_entry == doc.name for row in tracker_doc.payments + ) + if not existing_payment: + payment_row = tracker_doc.add_payment_detail(doc.name) + + # Calculate and create exchange difference entry + calculate_payment_exchange_difference(tracker_doc, doc, payment_row) + + # Add custom field reference + frappe.db.set_value( + "Payment Entry", + doc.name, + "foreign_import_tracker", + tracker_doc.name, + ) + + break # Link to first matching tracker only + + except Exception as e: + # Instead of passing the full error as title: + frappe.log_error( + title=f"Error linking payment {doc.name} to tracker {tracker_doc.name}", # keep short, <140 chars + message=frappe.get_traceback(), + ) + + +def unlink_payment_from_import_tracker(doc, method): + """Remove payment from Foreign Import Transaction when cancelled""" + tracker_name = frappe.db.get_value( + "Payment Entry", doc.name, "foreign_import_tracker" + ) + + if tracker_name: + try: + tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker_name) + + # Remove payment rows + tracker_doc.payments = [ + row for row in tracker_doc.payments if row.payment_entry != doc.name + ] + + # Remove related exchange difference entries and cancel JEs + for diff_row in tracker_doc.exchange_differences: + if ( + diff_row.reference_type == "Payment Entry" + and diff_row.reference_name == doc.name + ): + if diff_row.journal_entry: + try: + je = frappe.get_doc("Journal Entry", diff_row.journal_entry) + if je.docstatus == 1: + je.cancel() + except: + pass + + tracker_doc.exchange_differences = [ + row + for row in tracker_doc.exchange_differences + if not ( + row.reference_type == "Payment Entry" + and row.reference_name == doc.name + ) + ] + + tracker_doc.flags.ignore_validate_update_after_submit = True + tracker_doc.save() + + except Exception as e: + frappe.log_error( + f"Error unlinking payment {doc.name} from tracker {tracker_name}: {str(e)}" + ) + + +def calculate_payment_exchange_difference(tracker_doc, payment_doc, payment_row): + """Calculate exchange difference for payment and create Journal Entry""" + settings = get_import_settings(tracker_doc.company) + + original_rate = flt(tracker_doc.original_exchange_rate) + payment_rate = flt(payment_doc.source_exchange_rate) + paid_amount = flt(payment_doc.paid_amount) + + if abs(original_rate - payment_rate) < 0.000001: # No significant difference + return + + # Calculate exchange difference + exchange_diff = paid_amount * (payment_rate - original_rate) + + if abs(exchange_diff) < flt(settings.exchange_difference_threshold): + return # Below threshold + + difference_type = "Gain" if exchange_diff > 0 else "Loss" + + # Create Journal Entry if auto-creation is enabled + journal_entry = None + if settings.auto_create_journal_entries: + journal_entry = create_exchange_difference_je( + tracker_doc, + abs(exchange_diff), + difference_type, + payment_doc, + f"Payment Exchange {difference_type}", + ) + + # Add exchange difference entry + tracker_doc.add_exchange_difference( + "Payment Entry", + payment_doc.name, + difference_type, + abs(exchange_diff), + payment_doc.posting_date, + f"Exchange {difference_type.lower()} on payment against PI {tracker_doc.purchase_invoice}", + journal_entry.name if journal_entry else None, + ) + + # Update payment row + payment_row.exchange_difference = exchange_diff + payment_row.journal_entry_created = 1 if journal_entry else 0 + + +def calculate_lcv_exchange_difference(tracker_doc, lcv_doc): + """Calculate exchange difference for LCV and create Journal Entry""" + settings = get_import_settings(tracker_doc.company) + + if not settings.enable_lcv_exchange_tracking: + return + + # For LCV, we calculate the impact on inventory valuation + original_rate = flt(tracker_doc.original_exchange_rate) + + # Get LCV conversion rate (if available) + lcv_rate = flt(lcv_doc.get("conversion_rate", original_rate)) + + if abs(original_rate - lcv_rate) < 0.000001: + return + + # Calculate LCV amount in foreign currency + lcv_base_amount = flt(lcv_doc.total_taxes_and_charges) + lcv_foreign_amount = lcv_base_amount / lcv_rate + + # Calculate what it would have been at original rate + original_base_amount = lcv_foreign_amount * original_rate + + # Exchange difference + exchange_diff = lcv_base_amount - original_base_amount + + if abs(exchange_diff) < flt(settings.exchange_difference_threshold): + return + + difference_type = "Loss" if exchange_diff > 0 else "Gain" # Reversed for LCV + + # Create Journal Entry + journal_entry = None + if settings.auto_create_journal_entries: + journal_entry = create_exchange_difference_je( + tracker_doc, + abs(exchange_diff), + difference_type, + lcv_doc, + f"LCV Exchange {difference_type}", + ) + + # Add exchange difference entry + tracker_doc.add_exchange_difference( + "Landed Cost Voucher", + lcv_doc.name, + difference_type, + abs(exchange_diff), + lcv_doc.posting_date, + f"Exchange {difference_type.lower()} on importation costs - LCV {lcv_doc.name}", + journal_entry.name if journal_entry else None, + ) + + +def create_exchange_difference_je( + tracker_doc, amount, gain_loss_type, reference_doc, description +): + """Create Journal Entry for exchange gain/loss""" + settings = get_import_settings(tracker_doc.company) + + # Determine accounts + if gain_loss_type == "Gain": + debit_account = get_supplier_payable_account( + tracker_doc.supplier, tracker_doc.company + ) + credit_account = ( + settings.default_exchange_gain_account + or get_exchange_gain_loss_account(tracker_doc.company) + ) + else: + debit_account = ( + settings.default_exchange_loss_account + or get_exchange_gain_loss_account(tracker_doc.company) + ) + credit_account = get_supplier_payable_account( + tracker_doc.supplier, tracker_doc.company + ) + + if not debit_account or not credit_account: + frappe.throw(_("Exchange Gain/Loss accounts not configured")) + + # Create Journal Entry + je = frappe.new_doc("Journal Entry") + je.company = tracker_doc.company + je.posting_date = reference_doc.posting_date + je.voucher_type = "Exchange Gain Or Loss" + je.user_remark = f"{description} - {tracker_doc.purchase_invoice}" + je.multi_currency = 1 + + # Debit entry + je.append( + "accounts", + { + "account": debit_account, + "debit_in_account_currency": amount, + "party_type": "Supplier" if is_payable_account(debit_account) else "", + "party": tracker_doc.supplier if is_payable_account(debit_account) else "", + }, + ) + + # Credit entry + je.append( + "accounts", + { + "account": credit_account, + "credit_in_account_currency": amount, + "party_type": "Supplier" if is_payable_account(credit_account) else "", + "party": tracker_doc.supplier if is_payable_account(credit_account) else "", + }, + ) + + je.insert() + je.submit() + + return je + + +def get_import_settings(company): + """Get Foreign Import Settings for company""" + settings = frappe.get_single("Foreign Import Settings") + + if not settings.company: + settings.company = company + settings.save() + + return settings + + +def get_supplier_payable_account(supplier, company): + # Try the current ERPNext field name first + payable_account = frappe.db.get_value( + "Party Account", + {"parenttype": "Supplier", "parent": supplier, "company": company}, + "account", + ) + + if not payable_account: + # Fallback: try the supplier-level field (field name varies by version) + payable_account = frappe.db.get_value( + "Supplier", supplier, "payable_account" + ) or frappe.db.get_value("Supplier", supplier, "default_payable_account") + + if not payable_account: + # Final fallback: get default payable account from Company + payable_account = frappe.db.get_value( + "Company", company, "default_payable_account" + ) + + return payable_account + + +def get_exchange_gain_loss_account(company): + """Get exchange gain/loss account for company""" + return frappe.get_cached_value("Company", company, "exchange_gain_loss_account") + + +def is_payable_account(account): + """Check if account is a payable account""" + account_type = frappe.get_cached_value("Account", account, "account_type") + return account_type == "Payable" + + +def recalculate_import_differences(tracker_name): + """Recalculate all exchange differences for an import transaction""" + tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker_name) + + # Clear existing differences (but don't cancel JEs) + tracker_doc.exchange_differences = [] + + # Recalculate payment differences + for payment_row in tracker_doc.payments: + if payment_row.payment_entry: + payment_doc = frappe.get_doc("Payment Entry", payment_row.payment_entry) + calculate_payment_exchange_difference(tracker_doc, payment_doc, payment_row) + + # Recalculate LCV differences + for lcv_row in tracker_doc.landed_cost_vouchers: + if lcv_row.landed_cost_voucher: + lcv_doc = frappe.get_doc("Landed Cost Voucher", lcv_row.landed_cost_voucher) + calculate_lcv_exchange_difference(tracker_doc, lcv_doc) + + tracker_doc.flags.ignore_validate_update_after_submit = True + tracker_doc.save() + return True + + +def update_pending_transactions(): + """Scheduled task to update pending import transactions""" + pending_trackers = frappe.db.sql( + """ + SELECT name FROM `tabForeign Import Transaction` + WHERE status = 'Active' AND docstatus = 1 + """, + as_dict=True, + ) + + for tracker in pending_trackers: + try: + tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker.name) + tracker_doc.calculate_totals() + tracker_doc.set_status() + tracker_doc.flags.ignore_validate_update_after_submit = True + tracker_doc.save() + except Exception as e: + frappe.log_error(f"Error updating tracker {tracker.name}: {str(e)}") + + +@frappe.whitelist() +def create_manual_exchange_entry( + tracker_name, reference_type, reference_name, difference_type, amount, remarks +): + """Create manual exchange difference entry""" + tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker_name) + + if tracker_doc.docstatus != 1: + frappe.throw("Transaction must be submitted to add manual entries") + + amount = flt(amount) + if amount <= 0: + frappe.throw("Amount must be greater than 0") + + settings = get_import_settings(tracker_doc.company) + + journal_entry = None + if settings.auto_create_journal_entries: + reference_doc = frappe.get_doc(reference_type, reference_name) + journal_entry = create_exchange_difference_je( + tracker_doc, + amount, + difference_type, + reference_doc, + f"Manual {difference_type} Entry", + ) + + tracker_doc.add_exchange_difference( + reference_type, + reference_name, + difference_type, + amount, + nowdate(), + remarks, + journal_entry.name if journal_entry else None, + ) + + return tracker_doc.name + + +@frappe.whitelist() +def debug_payment_linking_issue(payment_entry_name): + """Debug why a payment entry is not linking to import tracker""" + try: + payment_doc = frappe.get_doc("Payment Entry", payment_entry_name) + + debug_info = { + "payment_entry": payment_entry_name, + "payment_details": { + "payment_type": payment_doc.payment_type, + "party_type": payment_doc.party_type, + "party": payment_doc.party, + "paid_to_account_currency": payment_doc.paid_to_account_currency, + "source_exchange_rate": payment_doc.source_exchange_rate, + "paid_amount": payment_doc.paid_amount, + "docstatus": payment_doc.docstatus, + }, + "issues": [], + "potential_trackers": [], + } + + # Check basic conditions + if payment_doc.payment_type != "Pay": + debug_info["issues"].append( + f"Payment type is '{payment_doc.payment_type}', should be 'Pay'" + ) + + if payment_doc.party_type != "Supplier": + debug_info["issues"].append( + f"Party type is '{payment_doc.party_type}', should be 'Supplier'" + ) + + if payment_doc.docstatus != 1: + debug_info["issues"].append( + f"Payment Entry not submitted (docstatus = {payment_doc.docstatus})" + ) + + # Find potential trackers for this supplier + if payment_doc.party_type == "Supplier": + trackers = frappe.db.sql( + """ + SELECT name, purchase_invoice, currency, original_exchange_rate, + invoice_amount_foreign, status, docstatus, supplier + FROM `tabForeign Import Transaction` + WHERE supplier = %s + ORDER BY transaction_date DESC + """, + payment_doc.party, + as_dict=True, + ) + + for tracker in trackers: + tracker_info = { + "name": tracker.name, + "purchase_invoice": tracker.purchase_invoice, + "currency": tracker.currency, + "status": tracker.status, + "docstatus": tracker.docstatus, + "currency_match": payment_doc.paid_to_account_currency + == tracker.currency, + "status_ok": tracker.status in ("Active", "Draft") + and tracker.docstatus == 1, + "issues": [], + } + + if not tracker_info["currency_match"]: + tracker_info["issues"].append( + f"Currency mismatch: Payment={payment_doc.paid_to_account_currency}, Tracker={tracker.currency}" + ) + + if not tracker_info["status_ok"]: + tracker_info["issues"].append( + f"Status issue: Status={tracker.status}, Docstatus={tracker.docstatus}" + ) + + # Check if already linked + tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker.name) + already_linked = any( + row.payment_entry == payment_doc.name + for row in tracker_doc.payments + ) + if already_linked: + tracker_info["issues"].append( + "Payment already linked to this tracker" + ) + + debug_info["potential_trackers"].append(tracker_info) + + return debug_info + + except Exception as e: + return {"error": str(e)} + + +@frappe.whitelist() +def manually_link_payment_to_tracker(payment_entry_name, tracker_name=None): + """Manually link a payment entry to a foreign import tracker""" + try: + payment_doc = frappe.get_doc("Payment Entry", payment_entry_name) + + if not tracker_name: + # Find the best matching tracker + trackers = frappe.db.sql( + """ + SELECT name, currency, status, docstatus + FROM `tabForeign Import Transaction` + WHERE supplier = %s AND status IN ('Active', 'Draft') AND docstatus = 1 + ORDER BY transaction_date DESC + LIMIT 1 + """, + payment_doc.party, + as_dict=True, + ) + + if not trackers: + return {"error": "No active trackers found for this supplier"} + + tracker_name = trackers[0].name + + tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker_name) + + # Validate + if payment_doc.party != tracker_doc.supplier: + return { + "error": f"Payment party ({payment_doc.party}) doesn't match tracker supplier ({tracker_doc.supplier})" + } + + # Check if already linked + existing_payment = any( + row.payment_entry == payment_doc.name for row in tracker_doc.payments + ) + if existing_payment: + return {"error": "Payment entry is already linked to this tracker"} + + # Add payment detail + payment_row = tracker_doc.add_payment_detail(payment_doc.name) + + # Calculate and create exchange difference entry + calculate_payment_exchange_difference(tracker_doc, payment_doc, payment_row) + + # Add custom field reference + frappe.db.set_value( + "Payment Entry", + payment_doc.name, + "foreign_import_tracker", + tracker_doc.name, + ) + + return { + "success": f"Payment {payment_doc.name} successfully linked to tracker {tracker_doc.name}" + } + + except Exception as e: + frappe.log_error( + f"Error manually linking payment {payment_entry_name} to tracker {tracker_name}: {str(e)}" + ) + return {"error": str(e)} diff --git a/av_tools/av_tools_hooks/landed_cost_voucher.py b/av_tools/av_tools_hooks/landed_cost_voucher.py new file mode 100644 index 0000000..b8c5c4e --- /dev/null +++ b/av_tools/av_tools_hooks/landed_cost_voucher.py @@ -0,0 +1,47 @@ +from __future__ import unicode_literals + +import frappe + + +def total_amount(doc, method): + grand_total = 0 + for item in doc.items: + if item.amount and item.applicable_charges: + item.custom_total_amount = item.amount + item.applicable_charges + else: + item.custom_total_amount = 0 + grand_total += item.custom_total_amount or 0 + doc.custom_grand_total = grand_total if doc.items else 0 + + +@frappe.whitelist() +def get_landed_cost_expenses(import_file=None): + if not import_file: + return + + je_landed_cost = frappe.db.sql( + """ + SELECT jea.account AS expense_account, je.title AS description, jea.debit AS amount + FROM `tabJournal Entry` je + INNER JOIN `tabJournal Entry Account` jea ON jea.parent = je.name + WHERE je.import_file = %s + AND je.docstatus = 1 + AND jea.debit > 0 + """, + import_file, + as_dict=1, + ) + + pinv_landed_cost = frappe.db.sql( + """ + SELECT pii.expense_account AS expense_account, pi.title AS description, pii.base_net_amount AS amount + FROM `tabPurchase Invoice` pi + INNER JOIN `tabPurchase Invoice Item` pii ON pii.parent = pi.name + WHERE pi.import_file = %s + AND pi.docstatus = 1 + """, + import_file, + as_dict=1, + ) + + return je_landed_cost + pinv_landed_cost diff --git a/av_tools/hooks.py b/av_tools/hooks.py index 8b7920a..2e66d71 100644 --- a/av_tools/hooks.py +++ b/av_tools/hooks.py @@ -58,6 +58,7 @@ "av_tools/purchase_order.js", ], "Stock Entry": "av_tools/stock_entry.js", + "Landed Cost Voucher": "av_tools/landed_cost_voucher.js", "Purchase Invoice": "weigh_bridge/doctype/purchase_invoice_weighbridge_ticket.js", "Purchase Receipt": "weigh_bridge/doctype/purchase_receipt_weighbridge_ticket.js", "Customer": "authotp/api/customer.js", @@ -100,7 +101,7 @@ # Installation # ------------ -# before_install = "av_tools.install.before_install" +before_install = "av_tools.install.before_install" after_install = [ "av_tools.weigh_bridge.custom_fields.setup_custom_fields", "av_tools.patches.custom_fields.auth_otp_custom_fields.execute", @@ -187,8 +188,21 @@ "av_tools.av_tools_hooks.purchase_order.target_warehouse_based_price_list", ] }, - "Purchase Invoice": {"validate": "av_tools.weigh_bridge.validation.validate_weighbridge_ticket"}, + "Purchase Invoice": { + "validate": "av_tools.weigh_bridge.validation.validate_weighbridge_ticket", + "on_submit": "av_tools.av_tools_hooks.foreign_import.create_import_tracker", + "on_cancel": "av_tools.av_tools_hooks.foreign_import.cancel_import_tracker", + }, "Purchase Receipt": {"validate": "av_tools.weigh_bridge.validation.validate_weighbridge_ticket"}, + "Payment Entry": { + "on_submit": "av_tools.av_tools_hooks.foreign_import.link_payment_to_import_tracker", + "on_cancel": "av_tools.av_tools_hooks.foreign_import.unlink_payment_from_import_tracker", + }, + "Landed Cost Voucher": { + "validate": "av_tools.av_tools_hooks.landed_cost_voucher.total_amount", + "on_submit": "av_tools.av_tools_hooks.foreign_import.link_lcv_to_import_tracker", + "on_cancel": "av_tools.av_tools_hooks.foreign_import.unlink_lcv_from_import_tracker", + }, "Custom DocPerm": { "validate": "av_tools.av_tools_hooks.custom_docperm.grant_dependant_access", }, @@ -228,6 +242,7 @@ }, "daily": [ "av_tools.av_tools.doctype.visibility.visibility.trigger_daily_alerts", + "av_tools.av_tools_hooks.foreign_import.update_pending_transactions", ] } diff --git a/av_tools/install.py b/av_tools/install.py new file mode 100644 index 0000000..964220e --- /dev/null +++ b/av_tools/install.py @@ -0,0 +1,10 @@ +import frappe + + +MODULES_TO_CLAIM = ("AuthOTP", "Feedback", "AI Integration") + + +def before_install(): + for module in MODULES_TO_CLAIM: + if frappe.db.exists("Module Def", module): + frappe.db.delete("Module Def", {"name": module}) diff --git a/av_tools/patches.txt b/av_tools/patches.txt index c5579df..dbb6e7f 100644 --- a/av_tools/patches.txt +++ b/av_tools/patches.txt @@ -22,6 +22,8 @@ av_tools.patches.v1_0.move_attachment_and_maintenance_doctypes av_tools.patches.v1_0.move_root_cause_doctypes av_tools.patches.v1_0.move_misc_csf_tz_doctypes av_tools.patches.v1_0.move_repack_and_consignment_doctypes +av_tools.patches.v1_0.move_import_file_feature +av_tools.patches.v1_0.move_foreign_import_feature av_tools.patches.v1_0.move_csf_tz_print_formats av_tools.patches.v1_0.move_salary_register_reports av_tools.patches.v1_0.move_financial_and_trade_reports diff --git a/av_tools/patches/custom_fields/custom_fields_json/foreign_import_custom_fields.json b/av_tools/patches/custom_fields/custom_fields_json/foreign_import_custom_fields.json new file mode 100644 index 0000000..c953bcd --- /dev/null +++ b/av_tools/patches/custom_fields/custom_fields_json/foreign_import_custom_fields.json @@ -0,0 +1,145 @@ +[ + { + "dt": "Purchase Invoice", + "fieldname": "section_break_foreign_import", + "label": "Foreign Import Tracking", + "fieldtype": "Section Break", + "insert_after": "terms", + "collapsible": 1, + "depends_on": "eval:doc.currency != doc.company_default_currency", + "doctype": "Custom Field", + "module": "Av Tools" + }, + { + "dt": "Purchase Invoice", + "fieldname": "foreign_import_tracker", + "label": "Foreign Import Tracker", + "fieldtype": "Data", + "read_only": 1, + "print_hide": 1, + "no_copy": 1, + "insert_after": "section_break_foreign_import", + "doctype": "Custom Field", + "module": "Av Tools" + }, + { + "dt": "Purchase Invoice", + "fieldname": "enable_import_tracking", + "label": "Enable Import Tracking", + "fieldtype": "Check", + "default": "1", + "insert_after": "foreign_import_tracker", + "depends_on": "eval:!doc.foreign_import_tracker", + "doctype": "Custom Field", + "module": "Av Tools" + }, + { + "dt": "Payment Entry", + "fieldname": "foreign_import_tracker", + "label": "Foreign Import Tracker", + "fieldtype": "Link", + "options": "Foreign Import Transaction", + "read_only": 1, + "print_hide": 1, + "no_copy": 1, + "insert_after": "amended_from", + "depends_on": "eval:doc.payment_type == 'Pay' && doc.party_type == 'Supplier'", + "doctype": "Custom Field", + "module": "Av Tools" + }, + { + "dt": "Payment Entry", + "fieldname": "exchange_difference_amount", + "label": "Exchange Difference", + "fieldtype": "Currency", + "read_only": 1, + "insert_after": "foreign_import_tracker", + "depends_on": "foreign_import_tracker", + "doctype": "Custom Field", + "module": "Av Tools" + }, + { + "dt": "Landed Cost Voucher", + "fieldname": "section_break_import_tracking", + "label": "Import Tracking", + "fieldtype": "Section Break", + "insert_after": "amended_from", + "collapsible": 1, + "doctype": "Custom Field", + "module": "Av Tools" + }, + { + "dt": "Landed Cost Voucher", + "fieldname": "foreign_import_trackers", + "label": "Related Import Trackers", + "fieldtype": "Table", + "options": "Foreign Import LCV Details", + "read_only": 1, + "insert_after": "section_break_import_tracking", + "doctype": "Custom Field", + "module": "Av Tools" + }, + { + "dt": "Company", + "fieldname": "section_break_foreign_import", + "label": "Foreign Import Settings", + "fieldtype": "Section Break", + "insert_after": "exchange_gain_loss_account", + "collapsible": 1, + "doctype": "Custom Field", + "module": "Av Tools" + }, + { + "dt": "Company", + "fieldname": "auto_create_import_tracker", + "label": "Auto Create Import Tracker", + "fieldtype": "Check", + "default": "1", + "insert_after": "section_break_foreign_import", + "doctype": "Custom Field", + "module": "Av Tools" + }, + { + "dt": "Company", + "fieldname": "import_exchange_threshold", + "label": "Exchange Difference Threshold", + "fieldtype": "Currency", + "default": "1.00", + "insert_after": "auto_create_import_tracker", + "doctype": "Custom Field", + "module": "Av Tools" + }, + { + "dt": "Supplier", + "fieldname": "track_import_exchanges", + "label": "Track Import Exchange Differences", + "fieldtype": "Check", + "default": "1", + "insert_after": "default_price_list", + "doctype": "Custom Field", + "module": "Av Tools" + }, + { + "dt": "Supplier", + "fieldname": "preferred_exchange_account", + "label": "Preferred Exchange Account", + "fieldtype": "Link", + "options": "Account", + "insert_after": "track_import_exchanges", + "doctype": "Custom Field", + "module": "Av Tools" + }, + { + "dt": "Journal Entry", + "fieldname": "foreign_import_tracker", + "label": "Foreign Import Tracker", + "fieldtype": "Link", + "options": "Foreign Import Transaction", + "read_only": 1, + "print_hide": 1, + "insert_after": "amended_from", + "depends_on": "eval:doc.voucher_type == 'Exchange Gain Or Loss'", + "doctype": "Custom Field", + "module": "Av Tools" + } +] \ No newline at end of file diff --git a/av_tools/patches/custom_fields/custom_fields_json/import_file_custom_fields.json b/av_tools/patches/custom_fields/custom_fields_json/import_file_custom_fields.json new file mode 100644 index 0000000..61eb48b --- /dev/null +++ b/av_tools/patches/custom_fields/custom_fields_json/import_file_custom_fields.json @@ -0,0 +1,116 @@ +[ + { + "doctype": "Custom Field", + "dt": "Journal Entry", + "fieldname": "import_file", + "fieldtype": "Link", + "insert_after": "clearance_date", + "label": "Import File", + "options": "Import File", + "allow_on_submit": 1, + "permlevel": 0, + "hidden": 0, + "read_only": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "in_global_search": 0, + "in_preview": 0, + "bold": 0, + "collapsible": 0, + "report_hide": 0, + "reqd": 0, + "ignore_user_permissions": 0, + "no_copy": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "unique": 0, + "search_index": 0, + "allow_in_quick_entry": 0, + "ignore_xss_filter": 0, + "translatable": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "non_negative": 0, + "fetch_if_empty": 0, + "columns": 0, + "length": 0, + "precision": "" + }, + { + "doctype": "Custom Field", + "dt": "Landed Cost Voucher", + "fieldname": "import_file", + "fieldtype": "Link", + "insert_after": "sec_break1", + "label": "Import File", + "options": "Import File", + "permlevel": 0, + "hidden": 0, + "read_only": 0, + "allow_on_submit": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "in_global_search": 0, + "in_preview": 0, + "bold": 0, + "collapsible": 0, + "report_hide": 0, + "reqd": 0, + "ignore_user_permissions": 0, + "no_copy": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "unique": 0, + "search_index": 0, + "allow_in_quick_entry": 0, + "ignore_xss_filter": 0, + "translatable": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "non_negative": 0, + "fetch_if_empty": 0, + "columns": 0, + "length": 0, + "precision": "" + }, + { + "doctype": "Custom Field", + "dt": "Purchase Invoice", + "fieldname": "import_file", + "fieldtype": "Link", + "insert_after": "reference", + "label": "Import File", + "options": "Import File", + "allow_on_submit": 1, + "permlevel": 0, + "hidden": 0, + "read_only": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "in_global_search": 0, + "in_preview": 0, + "bold": 0, + "collapsible": 0, + "report_hide": 0, + "reqd": 0, + "ignore_user_permissions": 0, + "no_copy": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "unique": 0, + "search_index": 0, + "allow_in_quick_entry": 0, + "ignore_xss_filter": 0, + "translatable": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "non_negative": 0, + "fetch_if_empty": 0, + "columns": 0, + "length": 0, + "precision": "" + } +] diff --git a/av_tools/patches/v1_0/move_foreign_import_feature.py b/av_tools/patches/v1_0/move_foreign_import_feature.py new file mode 100644 index 0000000..87e7e65 --- /dev/null +++ b/av_tools/patches/v1_0/move_foreign_import_feature.py @@ -0,0 +1,42 @@ +import frappe + + +FOREIGN_IMPORT_DOCTYPES = ( + "Foreign Import Transaction", + "Foreign Import Settings", + "Foreign Import LCV Details", + "Foreign Import Payment Details", + "Foreign Import Exchange Difference Details", +) + +FOREIGN_IMPORT_REPORTS = ("Import Exchange Differences",) + +CUSTOM_FIELDS = ( + "Purchase Invoice-section_break_foreign_import", + "Purchase Invoice-foreign_import_tracker", + "Purchase Invoice-enable_import_tracking", + "Payment Entry-foreign_import_tracker", + "Payment Entry-exchange_difference_amount", + "Landed Cost Voucher-section_break_import_tracking", + "Landed Cost Voucher-foreign_import_trackers", + "Company-section_break_foreign_import", + "Company-auto_create_import_tracker", + "Company-import_exchange_threshold", + "Supplier-track_import_exchanges", + "Supplier-preferred_exchange_account", + "Journal Entry-foreign_import_tracker", +) + + +def execute(): + for doctype_name in FOREIGN_IMPORT_DOCTYPES: + if frappe.db.exists("DocType", doctype_name): + frappe.db.set_value("DocType", doctype_name, "module", "Av Tools") + + for report_name in FOREIGN_IMPORT_REPORTS: + if frappe.db.exists("Report", report_name): + frappe.db.set_value("Report", report_name, "module", "Av Tools") + + for cf_name in CUSTOM_FIELDS: + if frappe.db.exists("Custom Field", cf_name): + frappe.db.set_value("Custom Field", cf_name, "module", "Av Tools") diff --git a/av_tools/patches/v1_0/move_import_file_feature.py b/av_tools/patches/v1_0/move_import_file_feature.py new file mode 100644 index 0000000..55698b2 --- /dev/null +++ b/av_tools/patches/v1_0/move_import_file_feature.py @@ -0,0 +1,46 @@ +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + + +DOCTYPES = ("Import File",) + + +def execute(): + for doctype_name in DOCTYPES: + if frappe.db.exists("DocType", doctype_name): + frappe.db.set_value("DocType", doctype_name, "module", "Av Tools") + + create_custom_fields( + { + "Journal Entry": [ + { + "fieldname": "import_file", + "fieldtype": "Link", + "insert_after": "clearance_date", + "label": "Import File", + "options": "Import File", + "allow_on_submit": 1, + } + ], + "Landed Cost Voucher": [ + { + "fieldname": "import_file", + "fieldtype": "Link", + "insert_after": "sec_break1", + "label": "Import File", + "options": "Import File", + } + ], + "Purchase Invoice": [ + { + "fieldname": "import_file", + "fieldtype": "Link", + "insert_after": "reference", + "label": "Import File", + "options": "Import File", + "allow_on_submit": 1, + } + ], + }, + update=True, + )