diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index 48337cee640..d1a97f87ffb 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -68,6 +68,6 @@ if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
wait $wkpid
-bench start &> bench_run_logs.txt &
+bench start &>> ~/frappe-bench/bench_start.log &
CI=Yes bench build --app frappe &
bench --site test_site reinstall --yes
diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml
index aae2928bf0d..07b8de7a900 100644
--- a/.github/workflows/patch.yml
+++ b/.github/workflows/patch.yml
@@ -23,7 +23,7 @@ jobs:
services:
mysql:
- image: mariadb:10.3
+ image: mariadb:10.6
env:
MARIADB_ROOT_PASSWORD: 'root'
ports:
@@ -45,9 +45,7 @@ jobs:
- name: Setup Python
uses: "actions/setup-python@v4"
with:
- python-version: |
- 3.7
- 3.10
+ python-version: '3.10'
- name: Setup Node
uses: actions/setup-node@v2
@@ -102,40 +100,60 @@ jobs:
- name: Run Patch Tests
run: |
cd ~/frappe-bench/
- wget https://erpnext.com/files/v10-erpnext.sql.gz
- bench --site test_site --force restore ~/frappe-bench/v10-erpnext.sql.gz
+ bench remove-app payments --force
+ jq 'del(.install_apps)' ~/frappe-bench/sites/test_site/site_config.json > tmp.json
+ mv tmp.json ~/frappe-bench/sites/test_site/site_config.json
+
+ wget https://erpnext.com/files/v13-erpnext.sql.gz
+ bench --site test_site --force restore ~/frappe-bench/v13-erpnext.sql.gz
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
- for version in $(seq 12 13)
- do
- echo "Updating to v$version"
- branch_name="version-$version-hotfix"
- git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name
- git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name
+ function update_to_version() {
+ version=$1
- git -C "apps/frappe" checkout -q -f $branch_name
- git -C "apps/erpnext" checkout -q -f $branch_name
+ branch_name="version-$version-hotfix"
+ echo "Updating to v$version"
- rm -rf ~/frappe-bench/env
- bench setup env --python python3.7
- bench pip install -e ./apps/payments
- bench pip install -e ./apps/erpnext
+ # Fetch and checkout branches
+ git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name
+ git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name
+ git -C "apps/frappe" checkout -q -f $branch_name
+ git -C "apps/erpnext" checkout -q -f $branch_name
- bench --site test_site migrate
- done
+ # Resetup env and install apps
+ pgrep honcho | xargs kill
+ rm -rf ~/frappe-bench/env
+ bench -v setup env
+ bench pip install -e ./apps/erpnext
+ bench start &>> ~/frappe-bench/bench_start.log &
+ bench --site test_site migrate
+ }
+
+ update_to_version 14
echo "Updating to latest version"
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
+ pgrep honcho | xargs kill
rm -rf ~/frappe-bench/env
- bench -v setup env --python python3.10
- bench pip install -e ./apps/payments
+ bench -v setup env
bench pip install -e ./apps/erpnext
+ bench start &>> ~/frappe-bench/bench_start.log &
bench --site test_site migrate
- bench --site test_site install-app payments
+
+ - name: Show bench output
+ if: ${{ always() }}
+ run: |
+ cd ~/frappe-bench
+ cat bench_start.log || true
+ cd logs
+ for f in ./*.log*; do
+ echo "Printing log: $f";
+ cat $f
+ done
diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml
index 2ce1125456e..559be06993e 100644
--- a/.github/workflows/server-tests-mariadb.yml
+++ b/.github/workflows/server-tests-mariadb.yml
@@ -123,6 +123,10 @@ jobs:
CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
+ - name: Show bench output
+ if: ${{ always() }}
+ run: cat ~/frappe-bench/bench_start.log || true
+
- name: Upload coverage data
uses: actions/upload-artifact@v3
with:
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 35a378856b0..cdd1203d49a 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -50,6 +50,8 @@ frappe.ui.form.on("Journal Entry", {
frm.trigger("make_inter_company_journal_entry");
}, __('Make'));
}
+
+ erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
},
make_inter_company_journal_entry: function(frm) {
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 9a0adf5815d..794a4ef1bc0 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -9,7 +9,7 @@ erpnext.accounts.taxes.setup_tax_filters("Advance Taxes and Charges");
frappe.ui.form.on('Payment Entry', {
onload: function(frm) {
- frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger'];
+ frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payments', 'Unreconcile Payment Entries'];
if(frm.doc.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
@@ -154,6 +154,7 @@ frappe.ui.form.on('Payment Entry', {
frm.events.set_dynamic_labels(frm);
frm.events.show_general_ledger(frm);
erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm);
+ erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
},
validate_company: (frm) => {
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 2c2efc06455..feacb0fd2ba 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -98,7 +98,6 @@ class PaymentEntry(AccountsController):
if self.difference_amount:
frappe.throw(_("Difference Amount must be zero"))
self.make_gl_entries()
- self.make_advance_gl_entries()
self.update_outstanding_amounts()
self.update_advance_paid()
self.update_payment_schedule()
@@ -149,10 +148,11 @@ class PaymentEntry(AccountsController):
"Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
+ "Unreconcile Payments",
+ "Unreconcile Payment Entries",
)
super(PaymentEntry, self).on_cancel()
self.make_gl_entries(cancel=1)
- self.make_advance_gl_entries(cancel=1)
self.update_outstanding_amounts()
self.update_advance_paid()
self.delink_advance_entry_references()
@@ -1060,6 +1060,8 @@ class PaymentEntry(AccountsController):
else:
self.make_exchange_gain_loss_journal()
+ self.make_advance_gl_entries(cancel=cancel)
+
def add_party_gl_entries(self, gl_entries):
if self.party_account:
if self.payment_type == "Receive":
@@ -1128,7 +1130,7 @@ class PaymentEntry(AccountsController):
if self.book_advance_payments_in_separate_party_account:
gl_entries = []
for d in self.get("references"):
- if d.reference_doctype in ("Sales Invoice", "Purchase Invoice"):
+ if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Journal Entry"):
if not (against_voucher_type and against_voucher) or (
d.reference_doctype == against_voucher_type and d.reference_name == against_voucher
):
@@ -1164,6 +1166,13 @@ class PaymentEntry(AccountsController):
"voucher_detail_no": invoice.name,
}
+ posting_date = frappe.db.get_value(
+ invoice.reference_doctype, invoice.reference_name, "posting_date"
+ )
+
+ if getdate(posting_date) < getdate(self.posting_date):
+ posting_date = self.posting_date
+
dr_or_cr = "credit" if invoice.reference_doctype == "Sales Invoice" else "debit"
args_dict["account"] = invoice.account
args_dict[dr_or_cr] = invoice.allocated_amount
@@ -1172,6 +1181,7 @@ class PaymentEntry(AccountsController):
{
"against_voucher_type": invoice.reference_doctype,
"against_voucher": invoice.reference_name,
+ "posting_date": posting_date,
}
)
gle = self.get_gl_dict(
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
index 7b7ce7a8920..d9f00befa90 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
@@ -24,7 +24,8 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
filters: {
"company": this.frm.doc.company,
"is_group": 0,
- "account_type": frappe.boot.party_account_types[this.frm.doc.party_type]
+ "account_type": frappe.boot.party_account_types[this.frm.doc.party_type],
+ "root_type": this.frm.doc.party_type == 'Customer' ? "Asset" : "Liability"
}
};
});
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index efe97415a55..c8c9ad1b3a9 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -161,6 +161,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1);
+ erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
}
unblock_invoice() {
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 642e99cd58a..d4d923902f1 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -37,7 +37,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
super.onload();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
- 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger"];
+ 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payments", "Unreconcile Payment Entries"];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format
@@ -183,8 +183,11 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
}, __('Create'));
}
}
+
+ erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
}
+
make_maintenance_schedule() {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index fba2fa7552e..7bdb2b49cea 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -388,6 +388,8 @@ class SalesInvoice(SellingController):
"Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
+ "Unreconcile Payments",
+ "Unreconcile Payment Entries",
"Payment Ledger Entry",
"Serial and Batch Bundle",
)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py
index 6fdcf263a55..fd95c1fe0e5 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py
@@ -15,9 +15,11 @@ def get_data():
},
"internal_links": {
"Sales Order": ["items", "sales_order"],
- "Delivery Note": ["items", "delivery_note"],
"Timesheet": ["timesheets", "time_sheet"],
},
+ "internal_and_external_links": {
+ "Delivery Note": ["items", "delivery_note"],
+ },
"transactions": [
{
"label": _("Payment"),
diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py
index f3acdc5aa87..75223c2ccca 100644
--- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py
+++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py
@@ -57,18 +57,17 @@ def get_plan_rate(
prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
if prorate:
- prorate_factor = flt(
- date_diff(start_date, get_first_day(start_date))
- / date_diff(get_last_day(start_date), get_first_day(start_date)),
- 1,
- )
-
- prorate_factor += flt(
- date_diff(get_last_day(end_date), end_date)
- / date_diff(get_last_day(end_date), get_first_day(end_date)),
- 1,
- )
-
- cost -= plan.cost * prorate_factor
-
+ cost -= plan.cost * get_prorate_factor(start_date, end_date)
return cost
+
+
+def get_prorate_factor(start_date, end_date):
+ total_days_to_skip = date_diff(start_date, get_first_day(start_date))
+ total_days_in_month = int(get_last_day(start_date).strftime("%d"))
+ prorate_factor = flt(total_days_to_skip / total_days_in_month)
+
+ total_days_to_skip = date_diff(get_last_day(end_date), end_date)
+ total_days_in_month = int(get_last_day(end_date).strftime("%d"))
+ prorate_factor += flt(total_days_to_skip / total_days_in_month)
+
+ return prorate_factor
diff --git a/erpnext/crm/doctype/linkedin_settings/__init__.py b/erpnext/accounts/doctype/unreconcile_payment_entries/__init__.py
similarity index 100%
rename from erpnext/crm/doctype/linkedin_settings/__init__.py
rename to erpnext/accounts/doctype/unreconcile_payment_entries/__init__.py
diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json
new file mode 100644
index 00000000000..42da669e650
--- /dev/null
+++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json
@@ -0,0 +1,83 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-08-22 10:28:10.196712",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "account",
+ "party_type",
+ "party",
+ "reference_doctype",
+ "reference_name",
+ "allocated_amount",
+ "account_currency",
+ "unlinked"
+ ],
+ "fields": [
+ {
+ "fieldname": "reference_name",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Reference Name",
+ "options": "reference_doctype"
+ },
+ {
+ "fieldname": "allocated_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Allocated Amount",
+ "options": "account_currency"
+ },
+ {
+ "default": "0",
+ "fieldname": "unlinked",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Unlinked",
+ "read_only": 1
+ },
+ {
+ "fieldname": "reference_doctype",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Reference Type",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "account",
+ "fieldtype": "Data",
+ "label": "Account"
+ },
+ {
+ "fieldname": "party_type",
+ "fieldtype": "Data",
+ "label": "Party Type"
+ },
+ {
+ "fieldname": "party",
+ "fieldtype": "Data",
+ "label": "Party"
+ },
+ {
+ "fieldname": "account_currency",
+ "fieldtype": "Link",
+ "label": "Account Currency",
+ "options": "Currency",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2023-09-05 09:33:28.620149",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Unreconcile Payment Entries",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.py b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.py
new file mode 100644
index 00000000000..c41545c2685
--- /dev/null
+++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class UnreconcilePaymentEntries(Document):
+ pass
diff --git a/erpnext/crm/doctype/social_media_post/__init__.py b/erpnext/accounts/doctype/unreconcile_payments/__init__.py
similarity index 100%
rename from erpnext/crm/doctype/social_media_post/__init__.py
rename to erpnext/accounts/doctype/unreconcile_payments/__init__.py
diff --git a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py
new file mode 100644
index 00000000000..78e04bff819
--- /dev/null
+++ b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py
@@ -0,0 +1,316 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import today
+
+from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
+
+
+class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
+ def setUp(self):
+ self.create_company()
+ self.create_customer()
+ self.create_usd_receivable_account()
+ self.create_item()
+ self.clear_old_entries()
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def create_sales_invoice(self, do_not_submit=False):
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debit_to,
+ posting_date=today(),
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=100,
+ price_list_rate=100,
+ do_not_submit=do_not_submit,
+ )
+ return si
+
+ def create_payment_entry(self):
+ pe = create_payment_entry(
+ company=self.company,
+ payment_type="Receive",
+ party_type="Customer",
+ party=self.customer,
+ paid_from=self.debit_to,
+ paid_to=self.cash,
+ paid_amount=200,
+ save=True,
+ )
+ return pe
+
+ def test_01_unreconcile_invoice(self):
+ si1 = self.create_sales_invoice()
+ si2 = self.create_sales_invoice()
+
+ pe = self.create_payment_entry()
+ pe.append(
+ "references",
+ {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100},
+ )
+ pe.append(
+ "references",
+ {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 100},
+ )
+ # Allocation payment against both invoices
+ pe.save().submit()
+
+ # Assert outstanding
+ [doc.reload() for doc in [si1, si2, pe]]
+ self.assertEqual(si1.outstanding_amount, 0)
+ self.assertEqual(si2.outstanding_amount, 0)
+ self.assertEqual(pe.unallocated_amount, 0)
+
+ unreconcile = frappe.get_doc(
+ {
+ "doctype": "Unreconcile Payments",
+ "company": self.company,
+ "voucher_type": pe.doctype,
+ "voucher_no": pe.name,
+ }
+ )
+ unreconcile.add_references()
+ self.assertEqual(len(unreconcile.allocations), 2)
+ allocations = [x.reference_name for x in unreconcile.allocations]
+ self.assertEquals([si1.name, si2.name], allocations)
+ # unreconcile si1
+ for x in unreconcile.allocations:
+ if x.reference_name != si1.name:
+ unreconcile.remove(x)
+ unreconcile.save().submit()
+
+ # Assert outstanding
+ [doc.reload() for doc in [si1, si2, pe]]
+ self.assertEqual(si1.outstanding_amount, 100)
+ self.assertEqual(si2.outstanding_amount, 0)
+ self.assertEqual(len(pe.references), 1)
+ self.assertEqual(pe.unallocated_amount, 100)
+
+ def test_02_unreconcile_one_payment_from_multi_payments(self):
+ """
+ Scenario: 2 payments, both split against 2 different invoices
+ Unreconcile only one payment from one invoice
+ """
+ si1 = self.create_sales_invoice()
+ si2 = self.create_sales_invoice()
+ pe1 = self.create_payment_entry()
+ pe1.paid_amount = 100
+ # Allocate payment against both invoices
+ pe1.append(
+ "references",
+ {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
+ )
+ pe1.append(
+ "references",
+ {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
+ )
+ pe1.save().submit()
+
+ pe2 = self.create_payment_entry()
+ pe2.paid_amount = 100
+ # Allocate payment against both invoices
+ pe2.append(
+ "references",
+ {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
+ )
+ pe2.append(
+ "references",
+ {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
+ )
+ pe2.save().submit()
+
+ # Assert outstanding and unallocated
+ [doc.reload() for doc in [si1, si2, pe1, pe2]]
+ self.assertEqual(si1.outstanding_amount, 0.0)
+ self.assertEqual(si2.outstanding_amount, 0.0)
+ self.assertEqual(pe1.unallocated_amount, 0.0)
+ self.assertEqual(pe2.unallocated_amount, 0.0)
+
+ unreconcile = frappe.get_doc(
+ {
+ "doctype": "Unreconcile Payments",
+ "company": self.company,
+ "voucher_type": pe2.doctype,
+ "voucher_no": pe2.name,
+ }
+ )
+ unreconcile.add_references()
+ self.assertEqual(len(unreconcile.allocations), 2)
+ allocations = [x.reference_name for x in unreconcile.allocations]
+ self.assertEquals([si1.name, si2.name], allocations)
+ # unreconcile si1 from pe2
+ for x in unreconcile.allocations:
+ if x.reference_name != si1.name:
+ unreconcile.remove(x)
+ unreconcile.save().submit()
+
+ # Assert outstanding and unallocated
+ [doc.reload() for doc in [si1, si2, pe1, pe2]]
+ self.assertEqual(si1.outstanding_amount, 50)
+ self.assertEqual(si2.outstanding_amount, 0)
+ self.assertEqual(len(pe1.references), 2)
+ self.assertEqual(len(pe2.references), 1)
+ self.assertEqual(pe1.unallocated_amount, 0)
+ self.assertEqual(pe2.unallocated_amount, 50)
+
+ def test_03_unreconciliation_on_multi_currency_invoice(self):
+ self.create_customer("_Test MC Customer USD", "USD")
+ si1 = self.create_sales_invoice(do_not_submit=True)
+ si1.currency = "USD"
+ si1.debit_to = self.debtors_usd
+ si1.conversion_rate = 80
+ si1.save().submit()
+
+ si2 = self.create_sales_invoice(do_not_submit=True)
+ si2.currency = "USD"
+ si2.debit_to = self.debtors_usd
+ si2.conversion_rate = 80
+ si2.save().submit()
+
+ pe = self.create_payment_entry()
+ pe.paid_from = self.debtors_usd
+ pe.paid_from_account_currency = "USD"
+ pe.source_exchange_rate = 75
+ pe.received_amount = 75 * 200
+ pe.save()
+ # Allocate payment against both invoices
+ pe.append(
+ "references",
+ {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100},
+ )
+ pe.append(
+ "references",
+ {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 100},
+ )
+ pe.save().submit()
+
+ unreconcile = frappe.get_doc(
+ {
+ "doctype": "Unreconcile Payments",
+ "company": self.company,
+ "voucher_type": pe.doctype,
+ "voucher_no": pe.name,
+ }
+ )
+ unreconcile.add_references()
+ self.assertEqual(len(unreconcile.allocations), 2)
+ allocations = [x.reference_name for x in unreconcile.allocations]
+ self.assertEquals([si1.name, si2.name], allocations)
+ # unreconcile si1 from pe
+ for x in unreconcile.allocations:
+ if x.reference_name != si1.name:
+ unreconcile.remove(x)
+ unreconcile.save().submit()
+
+ # Assert outstanding and unallocated
+ [doc.reload() for doc in [si1, si2, pe]]
+ self.assertEqual(si1.outstanding_amount, 100)
+ self.assertEqual(si2.outstanding_amount, 0)
+ self.assertEqual(len(pe.references), 1)
+ self.assertEqual(pe.unallocated_amount, 100)
+
+ # Exc gain/loss JE should've been cancelled as well
+ self.assertEqual(
+ frappe.db.count(
+ "Journal Entry Account",
+ filters={"reference_type": si1.doctype, "reference_name": si1.name, "docstatus": 1},
+ ),
+ 0,
+ )
+
+ def test_04_unreconciliation_on_multi_currency_invoice(self):
+ """
+ 2 payments split against 2 foreign currency invoices
+ """
+ self.create_customer("_Test MC Customer USD", "USD")
+ si1 = self.create_sales_invoice(do_not_submit=True)
+ si1.currency = "USD"
+ si1.debit_to = self.debtors_usd
+ si1.conversion_rate = 80
+ si1.save().submit()
+
+ si2 = self.create_sales_invoice(do_not_submit=True)
+ si2.currency = "USD"
+ si2.debit_to = self.debtors_usd
+ si2.conversion_rate = 80
+ si2.save().submit()
+
+ pe1 = self.create_payment_entry()
+ pe1.paid_from = self.debtors_usd
+ pe1.paid_from_account_currency = "USD"
+ pe1.source_exchange_rate = 75
+ pe1.received_amount = 75 * 100
+ pe1.save()
+ # Allocate payment against both invoices
+ pe1.append(
+ "references",
+ {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
+ )
+ pe1.append(
+ "references",
+ {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
+ )
+ pe1.save().submit()
+
+ pe2 = self.create_payment_entry()
+ pe2.paid_from = self.debtors_usd
+ pe2.paid_from_account_currency = "USD"
+ pe2.source_exchange_rate = 75
+ pe2.received_amount = 75 * 100
+ pe2.save()
+ # Allocate payment against both invoices
+ pe2.append(
+ "references",
+ {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
+ )
+ pe2.append(
+ "references",
+ {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
+ )
+ pe2.save().submit()
+
+ unreconcile = frappe.get_doc(
+ {
+ "doctype": "Unreconcile Payments",
+ "company": self.company,
+ "voucher_type": pe2.doctype,
+ "voucher_no": pe2.name,
+ }
+ )
+ unreconcile.add_references()
+ self.assertEqual(len(unreconcile.allocations), 2)
+ allocations = [x.reference_name for x in unreconcile.allocations]
+ self.assertEquals([si1.name, si2.name], allocations)
+ # unreconcile si1 from pe2
+ for x in unreconcile.allocations:
+ if x.reference_name != si1.name:
+ unreconcile.remove(x)
+ unreconcile.save().submit()
+
+ # Assert outstanding and unallocated
+ [doc.reload() for doc in [si1, si2, pe1, pe2]]
+ self.assertEqual(si1.outstanding_amount, 50)
+ self.assertEqual(si2.outstanding_amount, 0)
+ self.assertEqual(len(pe1.references), 2)
+ self.assertEqual(len(pe2.references), 1)
+ self.assertEqual(pe1.unallocated_amount, 0)
+ self.assertEqual(pe2.unallocated_amount, 50)
+
+ # Exc gain/loss JE from PE1 should be available
+ self.assertEqual(
+ frappe.db.count(
+ "Journal Entry Account",
+ filters={"reference_type": si1.doctype, "reference_name": si1.name, "docstatus": 1},
+ ),
+ 1,
+ )
diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js
new file mode 100644
index 00000000000..c522567637f
--- /dev/null
+++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js
@@ -0,0 +1,41 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Unreconcile Payments", {
+ refresh(frm) {
+ frm.set_query("voucher_type", function() {
+ return {
+ filters: {
+ name: ["in", ["Payment Entry", "Journal Entry"]]
+ }
+ }
+ });
+
+
+ frm.set_query("voucher_no", function(doc) {
+ return {
+ filters: {
+ company: doc.company,
+ docstatus: 1
+ }
+ }
+ });
+
+ },
+ get_allocations: function(frm) {
+ frm.clear_table("allocations");
+ frappe.call({
+ method: "get_allocations_from_payment",
+ doc: frm.doc,
+ callback: function(r) {
+ if (r.message) {
+ r.message.forEach(x => {
+ frm.add_child("allocations", x)
+ })
+ frm.refresh_fields();
+ }
+ }
+ })
+
+ }
+});
diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json
new file mode 100644
index 00000000000..f29e61b6ef6
--- /dev/null
+++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json
@@ -0,0 +1,93 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "format:UNREC-{#####}",
+ "creation": "2023-08-22 10:26:34.421423",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "company",
+ "voucher_type",
+ "voucher_no",
+ "get_allocations",
+ "allocations",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Unreconcile Payments",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ },
+ {
+ "fieldname": "voucher_type",
+ "fieldtype": "Link",
+ "label": "Voucher Type",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "label": "Voucher No",
+ "options": "voucher_type"
+ },
+ {
+ "fieldname": "get_allocations",
+ "fieldtype": "Button",
+ "label": "Get Allocations"
+ },
+ {
+ "fieldname": "allocations",
+ "fieldtype": "Table",
+ "label": "Allocations",
+ "options": "Unreconcile Payment Entries"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2023-08-28 17:42:50.261377",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Unreconcile Payments",
+ "naming_rule": "Expression",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "read": 1,
+ "role": "Accounts Manager",
+ "select": 1,
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "read": 1,
+ "role": "Accounts User",
+ "select": 1,
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py
new file mode 100644
index 00000000000..4f9fb50d463
--- /dev/null
+++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py
@@ -0,0 +1,158 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _, qb
+from frappe.model.document import Document
+from frappe.query_builder import Criterion
+from frappe.query_builder.functions import Abs, Sum
+from frappe.utils.data import comma_and
+
+from erpnext.accounts.utils import (
+ cancel_exchange_gain_loss_journal,
+ unlink_ref_doc_from_payment_entries,
+ update_voucher_outstanding,
+)
+
+
+class UnreconcilePayments(Document):
+ def validate(self):
+ self.supported_types = ["Payment Entry", "Journal Entry"]
+ if not self.voucher_type in self.supported_types:
+ frappe.throw(_("Only {0} are supported").format(comma_and(self.supported_types)))
+
+ @frappe.whitelist()
+ def get_allocations_from_payment(self):
+ allocated_references = []
+ ple = qb.DocType("Payment Ledger Entry")
+ allocated_references = (
+ qb.from_(ple)
+ .select(
+ ple.account,
+ ple.party_type,
+ ple.party,
+ ple.against_voucher_type.as_("reference_doctype"),
+ ple.against_voucher_no.as_("reference_name"),
+ Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
+ ple.account_currency,
+ )
+ .where(
+ (ple.docstatus == 1)
+ & (ple.voucher_type == self.voucher_type)
+ & (ple.voucher_no == self.voucher_no)
+ & (ple.voucher_no != ple.against_voucher_no)
+ )
+ .groupby(ple.against_voucher_type, ple.against_voucher_no)
+ .run(as_dict=True)
+ )
+
+ return allocated_references
+
+ def add_references(self):
+ allocations = self.get_allocations_from_payment()
+
+ for alloc in allocations:
+ self.append("allocations", alloc)
+
+ def on_submit(self):
+ # todo: more granular unreconciliation
+ for alloc in self.allocations:
+ doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name)
+ unlink_ref_doc_from_payment_entries(doc, self.voucher_no)
+ cancel_exchange_gain_loss_journal(doc, self.voucher_type, self.voucher_no)
+ update_voucher_outstanding(
+ alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party
+ )
+ frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)
+
+
+@frappe.whitelist()
+def doc_has_references(doctype: str = None, docname: str = None):
+ if doctype in ["Sales Invoice", "Purchase Invoice"]:
+ return frappe.db.count(
+ "Payment Ledger Entry",
+ filters={"delinked": 0, "against_voucher_no": docname, "amount": ["<", 0]},
+ )
+ else:
+ return frappe.db.count(
+ "Payment Ledger Entry",
+ filters={"delinked": 0, "voucher_no": docname, "against_voucher_no": ["!=", docname]},
+ )
+
+
+@frappe.whitelist()
+def get_linked_payments_for_doc(
+ company: str = None, doctype: str = None, docname: str = None
+) -> list:
+ if company and doctype and docname:
+ _dt = doctype
+ _dn = docname
+ ple = qb.DocType("Payment Ledger Entry")
+ if _dt in ["Sales Invoice", "Purchase Invoice"]:
+ criteria = [
+ (ple.company == company),
+ (ple.delinked == 0),
+ (ple.against_voucher_no == _dn),
+ (ple.amount < 0),
+ ]
+
+ res = (
+ qb.from_(ple)
+ .select(
+ ple.company,
+ ple.voucher_type,
+ ple.voucher_no,
+ Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
+ ple.account_currency,
+ )
+ .where(Criterion.all(criteria))
+ .groupby(ple.voucher_no, ple.against_voucher_no)
+ .having(qb.Field("allocated_amount") > 0)
+ .run(as_dict=True)
+ )
+ return res
+ else:
+ criteria = [
+ (ple.company == company),
+ (ple.delinked == 0),
+ (ple.voucher_no == _dn),
+ (ple.against_voucher_no != _dn),
+ ]
+
+ query = (
+ qb.from_(ple)
+ .select(
+ ple.company,
+ ple.against_voucher_type.as_("voucher_type"),
+ ple.against_voucher_no.as_("voucher_no"),
+ Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
+ ple.account_currency,
+ )
+ .where(Criterion.all(criteria))
+ .groupby(ple.against_voucher_no)
+ )
+ res = query.run(as_dict=True)
+ return res
+ return []
+
+
+@frappe.whitelist()
+def create_unreconcile_doc_for_selection(selections=None):
+ if selections:
+ selections = frappe.json.loads(selections)
+ # assuming each row is a unique voucher
+ for row in selections:
+ unrecon = frappe.new_doc("Unreconcile Payments")
+ unrecon.company = row.get("company")
+ unrecon.voucher_type = row.get("voucher_type")
+ unrecon.voucher_no = row.get("voucher_no")
+ unrecon.add_references()
+
+ # remove unselected references
+ unrecon.allocations = [
+ x
+ for x in unrecon.allocations
+ if x.reference_doctype == row.get("against_voucher_type")
+ and x.reference_name == row.get("against_voucher_no")
+ ]
+ unrecon.save().submit()
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index ae24675ff57..79bfd7833ab 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -434,6 +434,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
for gle in gl_entries:
group_by_value = gle.get(group_by)
+ gle.voucher_type = _(gle.voucher_type)
if gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries):
if not group_by_voucher_consolidated:
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index eed74a5f017..1360f73767b 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -491,14 +491,13 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n
gl_map = doc.build_gl_map()
create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1)
- if voucher_type == "Payment Entry":
- doc.make_advance_gl_entries()
-
# Only update outstanding for newly linked vouchers
for entry in entries:
update_voucher_outstanding(
entry.against_voucher_type, entry.against_voucher, entry.account, entry.party_type, entry.party
)
+ if voucher_type == "Payment Entry":
+ doc.make_advance_gl_entries(entry.against_voucher_type, entry.against_voucher)
frappe.flags.ignore_party_validation = False
@@ -675,7 +674,9 @@ def update_reference_in_payment_entry(
payment_entry.save(ignore_permissions=True)
-def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None:
+def cancel_exchange_gain_loss_journal(
+ parent_doc: dict | object, referenced_dt: str = None, referenced_dn: str = None
+) -> None:
"""
Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any.
"""
@@ -702,76 +703,147 @@ def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None:
as_list=1,
)
for doc in gain_loss_journals:
- frappe.get_doc("Journal Entry", doc[0]).cancel()
+ gain_loss_je = frappe.get_doc("Journal Entry", doc[0])
+ if referenced_dt and referenced_dn:
+ references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts]
+ if (
+ len(references) == 2
+ and (referenced_dt, referenced_dn) in references
+ and (parent_doc.doctype, parent_doc.name) in references
+ ):
+ # only cancel JE generated against parent_doc and referenced_dn
+ gain_loss_je.cancel()
+ else:
+ gain_loss_je.cancel()
-def unlink_ref_doc_from_payment_entries(ref_doc):
- remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name)
- remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name)
-
- frappe.db.sql(
- """update `tabGL Entry`
- set against_voucher_type=null, against_voucher=null,
- modified=%s, modified_by=%s
- where against_voucher_type=%s and against_voucher=%s
- and voucher_no != ifnull(against_voucher, '')""",
- (now(), frappe.session.user, ref_doc.doctype, ref_doc.name),
+def update_accounting_ledgers_after_reference_removal(
+ ref_type: str = None, ref_no: str = None, payment_name: str = None
+):
+ # General Ledger
+ gle = qb.DocType("GL Entry")
+ gle_update_query = (
+ qb.update(gle)
+ .set(gle.against_voucher_type, None)
+ .set(gle.against_voucher, None)
+ .set(gle.modified, now())
+ .set(gle.modified_by, frappe.session.user)
+ .where((gle.against_voucher_type == ref_type) & (gle.against_voucher == ref_no))
)
+ if payment_name:
+ gle_update_query = gle_update_query.where(gle.voucher_no == payment_name)
+ gle_update_query.run()
+
+ # Payment Ledger
ple = qb.DocType("Payment Ledger Entry")
+ ple_update_query = (
+ qb.update(ple)
+ .set(ple.against_voucher_type, ple.voucher_type)
+ .set(ple.against_voucher_no, ple.voucher_no)
+ .set(ple.modified, now())
+ .set(ple.modified_by, frappe.session.user)
+ .where(
+ (ple.against_voucher_type == ref_type)
+ & (ple.against_voucher_no == ref_no)
+ & (ple.delinked == 0)
+ )
+ )
- qb.update(ple).set(ple.against_voucher_type, ple.voucher_type).set(
- ple.against_voucher_no, ple.voucher_no
- ).set(ple.modified, now()).set(ple.modified_by, frappe.session.user).where(
- (ple.against_voucher_type == ref_doc.doctype)
- & (ple.against_voucher_no == ref_doc.name)
- & (ple.delinked == 0)
- ).run()
+ if payment_name:
+ ple_update_query = ple_update_query.where(ple.voucher_no == payment_name)
+ ple_update_query.run()
+
+def remove_ref_from_advance_section(ref_doc: object = None):
+ # TODO: this might need some testing
if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"):
ref_doc.set("advances", [])
-
- frappe.db.sql(
- """delete from `tab{0} Advance` where parent = %s""".format(ref_doc.doctype), ref_doc.name
- )
+ adv_type = qb.DocType(f"{ref_doc.doctype} Advance")
+ qb.from_(adv_type).delete().where(adv_type.parent == ref_doc.name).run()
-def remove_ref_doc_link_from_jv(ref_type, ref_no):
- linked_jv = frappe.db.sql_list(
- """select parent from `tabJournal Entry Account`
- where reference_type=%s and reference_name=%s and docstatus < 2""",
- (ref_type, ref_no),
+def unlink_ref_doc_from_payment_entries(ref_doc: object = None, payment_name: str = None):
+ remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name, payment_name)
+ remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name, payment_name)
+ update_accounting_ledgers_after_reference_removal(ref_doc.doctype, ref_doc.name, payment_name)
+ remove_ref_from_advance_section(ref_doc)
+
+
+def remove_ref_doc_link_from_jv(
+ ref_type: str = None, ref_no: str = None, payment_name: str = None
+):
+ jea = qb.DocType("Journal Entry Account")
+
+ linked_jv = (
+ qb.from_(jea)
+ .select(jea.parent)
+ .where((jea.reference_type == ref_type) & (jea.reference_name == ref_no) & (jea.docstatus.lt(2)))
+ .run(as_list=1)
)
+ linked_jv = convert_to_list(linked_jv)
+ # remove reference only from specified payment
+ linked_jv = [x for x in linked_jv if x == payment_name] if payment_name else linked_jv
if linked_jv:
- frappe.db.sql(
- """update `tabJournal Entry Account`
- set reference_type=null, reference_name = null,
- modified=%s, modified_by=%s
- where reference_type=%s and reference_name=%s
- and docstatus < 2""",
- (now(), frappe.session.user, ref_type, ref_no),
+ update_query = (
+ qb.update(jea)
+ .set(jea.reference_type, None)
+ .set(jea.reference_name, None)
+ .set(jea.modified, now())
+ .set(jea.modified_by, frappe.session.user)
+ .where((jea.reference_type == ref_type) & (jea.reference_name == ref_no))
)
+ if payment_name:
+ update_query = update_query.where(jea.parent == payment_name)
+
+ update_query.run()
+
frappe.msgprint(_("Journal Entries {0} are un-linked").format("\n".join(linked_jv)))
-def remove_ref_doc_link_from_pe(ref_type, ref_no):
- linked_pe = frappe.db.sql_list(
- """select parent from `tabPayment Entry Reference`
- where reference_doctype=%s and reference_name=%s and docstatus < 2""",
- (ref_type, ref_no),
+def convert_to_list(result):
+ """
+ Convert tuple to list
+ """
+ return [x[0] for x in result]
+
+
+def remove_ref_doc_link_from_pe(
+ ref_type: str = None, ref_no: str = None, payment_name: str = None
+):
+ per = qb.DocType("Payment Entry Reference")
+ pay = qb.DocType("Payment Entry")
+
+ linked_pe = (
+ qb.from_(per)
+ .select(per.parent)
+ .where(
+ (per.reference_doctype == ref_type) & (per.reference_name == ref_no) & (per.docstatus.lt(2))
+ )
+ .run(as_list=1)
)
+ linked_pe = convert_to_list(linked_pe)
+ # remove reference only from specified payment
+ linked_pe = [x for x in linked_pe if x == payment_name] if payment_name else linked_pe
if linked_pe:
- frappe.db.sql(
- """update `tabPayment Entry Reference`
- set allocated_amount=0, modified=%s, modified_by=%s
- where reference_doctype=%s and reference_name=%s
- and docstatus < 2""",
- (now(), frappe.session.user, ref_type, ref_no),
+ update_query = (
+ qb.update(per)
+ .set(per.allocated_amount, 0)
+ .set(per.modified, now())
+ .set(per.modified_by, frappe.session.user)
+ .where(
+ (per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no))
+ )
)
+ if payment_name:
+ update_query = update_query.where(per.parent == payment_name)
+
+ update_query.run()
+
for pe in linked_pe:
try:
pe_doc = frappe.get_doc("Payment Entry", pe)
@@ -785,19 +857,13 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no):
msg += _("Please cancel payment entry manually first")
frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error"))
- frappe.db.sql(
- """update `tabPayment Entry` set total_allocated_amount=%s,
- base_total_allocated_amount=%s, unallocated_amount=%s, modified=%s, modified_by=%s
- where name=%s""",
- (
- pe_doc.total_allocated_amount,
- pe_doc.base_total_allocated_amount,
- pe_doc.unallocated_amount,
- now(),
- frappe.session.user,
- pe,
- ),
- )
+ qb.update(pay).set(pay.total_allocated_amount, pe_doc.total_allocated_amount).set(
+ pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount
+ ).set(pay.unallocated_amount, pe_doc.unallocated_amount).set(pay.modified, now()).set(
+ pay.modified_by, frappe.session.user
+ ).where(
+ pay.name == pe
+ ).run()
frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
@@ -1163,8 +1229,13 @@ def get_autoname_with_number(number_value, doc_title, company):
def parse_naming_series_variable(doc, variable):
if variable == "FY":
- date = doc.get("posting_date") or doc.get("transaction_date") or getdate()
- return get_fiscal_year(date=date, company=doc.get("company"))[0]
+ if doc:
+ date = doc.get("posting_date") or doc.get("transaction_date")
+ company = doc.get("company")
+ else:
+ date = getdate()
+ company = None
+ return get_fiscal_year(date=date, company=company)[0]
@frappe.whitelist()
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index b242108a9a9..5b5cc2b0217 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -1173,6 +1173,7 @@
"depends_on": "is_internal_supplier",
"fieldname": "set_from_warehouse",
"fieldtype": "Link",
+ "ignore_user_permissions": 1,
"label": "Set From Warehouse",
"options": "Warehouse"
},
@@ -1273,7 +1274,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2023-06-03 16:19:45.710444",
+ "modified": "2023-09-13 16:21:07.361700",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
index 414f0866ccb..f79b6223bf5 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -878,6 +878,7 @@
"depends_on": "eval:parent.is_internal_supplier",
"fieldname": "from_warehouse",
"fieldtype": "Link",
+ "ignore_user_permissions": 1,
"label": "From Warehouse",
"options": "Warehouse"
},
@@ -902,7 +903,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-08-17 10:17:40.893393",
+ "modified": "2023-09-13 16:22:40.825092",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js
index 372ca56b86b..08dc44c71b4 100644
--- a/erpnext/buying/doctype/supplier/supplier.js
+++ b/erpnext/buying/doctype/supplier/supplier.js
@@ -12,6 +12,7 @@ frappe.ui.form.on("Supplier", {
return {
filters: {
'account_type': 'Payable',
+ 'root_type': 'Liability',
'company': d.company,
"is_group": 0
}
diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py
index 7be1d834a65..ee2ada3b655 100644
--- a/erpnext/buying/doctype/supplier/test_supplier.py
+++ b/erpnext/buying/doctype/supplier/test_supplier.py
@@ -206,6 +206,7 @@ def create_supplier(**args):
{
"doctype": "Supplier",
"supplier_name": args.supplier_name,
+ "default_currency": args.default_currency,
"supplier_group": args.supplier_group or "Services",
"supplier_type": args.supplier_type or "Company",
"tax_withholding_category": args.tax_withholding_category,
diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
index 21241e08603..07187352eb7 100644
--- a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
+++ b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
@@ -7,7 +7,7 @@ import copy
import frappe
from frappe import _
from frappe.query_builder.functions import Coalesce, Sum
-from frappe.utils import date_diff, flt, getdate
+from frappe.utils import cint, date_diff, flt, getdate
def execute(filters=None):
@@ -47,8 +47,10 @@ def get_data(filters):
mr.transaction_date.as_("date"),
mr_item.schedule_date.as_("required_date"),
mr_item.item_code.as_("item_code"),
- Sum(Coalesce(mr_item.stock_qty, 0)).as_("qty"),
- Coalesce(mr_item.stock_uom, "").as_("uom"),
+ Sum(Coalesce(mr_item.qty, 0)).as_("qty"),
+ Sum(Coalesce(mr_item.stock_qty, 0)).as_("stock_qty"),
+ Coalesce(mr_item.uom, "").as_("uom"),
+ Coalesce(mr_item.stock_uom, "").as_("stock_uom"),
Sum(Coalesce(mr_item.ordered_qty, 0)).as_("ordered_qty"),
Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
(Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.received_qty, 0))).as_(
@@ -96,7 +98,7 @@ def get_conditions(filters, query, mr, mr_item):
def update_qty_columns(row_to_update, data_row):
- fields = ["qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"]
+ fields = ["qty", "stock_qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"]
for field in fields:
row_to_update[field] += flt(data_row[field])
@@ -104,16 +106,20 @@ def update_qty_columns(row_to_update, data_row):
def prepare_data(data, filters):
"""Prepare consolidated Report data and Chart data"""
material_request_map, item_qty_map = {}, {}
+ precision = cint(frappe.db.get_default("float_precision")) or 2
for row in data:
# item wise map for charts
if not row["item_code"] in item_qty_map:
item_qty_map[row["item_code"]] = {
- "qty": row["qty"],
- "ordered_qty": row["ordered_qty"],
- "received_qty": row["received_qty"],
- "qty_to_receive": row["qty_to_receive"],
- "qty_to_order": row["qty_to_order"],
+ "qty": flt(row["stock_qty"], precision),
+ "stock_qty": flt(row["stock_qty"], precision),
+ "stock_uom": row["stock_uom"],
+ "uom": row["uom"],
+ "ordered_qty": flt(row["ordered_qty"], precision),
+ "received_qty": flt(row["received_qty"], precision),
+ "qty_to_receive": flt(row["qty_to_receive"], precision),
+ "qty_to_order": flt(row["qty_to_order"], precision),
}
else:
item_entry = item_qty_map[row["item_code"]]
@@ -200,21 +206,34 @@ def get_columns(filters):
{"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100},
{"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 200},
{
- "label": _("Stock UOM"),
+ "label": _("UOM"),
"fieldname": "uom",
"fieldtype": "Data",
"width": 100,
},
+ {
+ "label": _("Stock UOM"),
+ "fieldname": "stock_uom",
+ "fieldtype": "Data",
+ "width": 100,
+ },
]
)
columns.extend(
[
{
- "label": _("Stock Qty"),
+ "label": _("Qty"),
"fieldname": "qty",
"fieldtype": "Float",
- "width": 120,
+ "width": 140,
+ "convertible": "qty",
+ },
+ {
+ "label": _("Qty in Stock UOM"),
+ "fieldname": "stock_qty",
+ "fieldtype": "Float",
+ "width": 140,
"convertible": "qty",
},
{
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 0ca1e94427e..838fe527dfb 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -211,6 +211,37 @@ class AccountsController(TransactionBase):
def before_cancel(self):
validate_einvoice_fields(self)
+ def _remove_references_in_unreconcile(self):
+ upe = frappe.qb.DocType("Unreconcile Payment Entries")
+ rows = (
+ frappe.qb.from_(upe)
+ .select(upe.name, upe.parent)
+ .where((upe.reference_doctype == self.doctype) & (upe.reference_name == self.name))
+ .run(as_dict=True)
+ )
+
+ if rows:
+ references_map = frappe._dict()
+ for x in rows:
+ references_map.setdefault(x.parent, []).append(x.name)
+
+ for doc, rows in references_map.items():
+ unreconcile_doc = frappe.get_doc("Unreconcile Payments", doc)
+ for row in rows:
+ unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0])
+
+ unreconcile_doc.flags.ignore_validate_update_after_submit = True
+ unreconcile_doc.flags.ignore_links = True
+ unreconcile_doc.save(ignore_permissions=True)
+
+ # delete docs upon parent doc deletion
+ unreconcile_docs = frappe.db.get_all("Unreconcile Payments", filters={"voucher_no": self.name})
+ for x in unreconcile_docs:
+ _doc = frappe.get_doc("Unreconcile Payments", x.name)
+ if _doc.docstatus == 1:
+ _doc.cancel()
+ _doc.delete()
+
def on_trash(self):
# delete references in 'Repost Payment Ledger'
rpi = frappe.qb.DocType("Repost Payment Ledger Items")
@@ -218,6 +249,8 @@ class AccountsController(TransactionBase):
(rpi.voucher_type == self.doctype) & (rpi.voucher_no == self.name)
).run()
+ self._remove_references_in_unreconcile()
+
# delete sl and gl entries on deletion of transaction
if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"):
ple = frappe.qb.DocType("Payment Ledger Entry")
diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
deleted file mode 100644
index 7d6b3955cde..00000000000
--- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('LinkedIn Settings', {
- onload: function(frm) {
- if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret) {
- frappe.confirm(
- __('Session not valid. Do you want to login?'),
- function(){
- frm.trigger("login");
- },
- function(){
- window.close();
- }
- );
- }
- frm.dashboard.set_headline(__("For more information, {0}.", [`${__('click here')}`]));
- },
- refresh: function(frm) {
- if (frm.doc.session_status=="Expired"){
- let msg = __("Session not active. Save document to login.");
- frm.dashboard.set_headline_alert(
- `
`
- );
- }
-
- if (frm.doc.session_status=="Active"){
- let d = new Date(frm.doc.modified);
- d.setDate(d.getDate()+60);
- let dn = new Date();
- let days = d.getTime() - dn.getTime();
- days = Math.floor(days/(1000 * 3600 * 24));
- let msg,color;
-
- if (days>0){
- msg = __("Your session will be expire in {0} days.", [days]);
- color = "green";
- }
- else {
- msg = __("Session is expired. Save doc to login.");
- color = "red";
- }
-
- frm.dashboard.set_headline_alert(
- ``
- );
- }
- },
- login: function(frm) {
- if (frm.doc.consumer_key && frm.doc.consumer_secret){
- frappe.dom.freeze();
- frappe.call({
- doc: frm.doc,
- method: "get_authorization_url",
- callback : function(r) {
- window.location.href = r.message;
- }
- }).fail(function() {
- frappe.dom.unfreeze();
- });
- }
- },
- after_save: function(frm) {
- frm.trigger("login");
- }
-});
diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json
deleted file mode 100644
index f882e36c32a..00000000000
--- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json
+++ /dev/null
@@ -1,112 +0,0 @@
-{
- "actions": [],
- "creation": "2020-01-30 13:36:39.492931",
- "doctype": "DocType",
- "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/linkedin-settings",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "account_name",
- "column_break_2",
- "company_id",
- "oauth_details",
- "consumer_key",
- "column_break_5",
- "consumer_secret",
- "user_details_section",
- "access_token",
- "person_urn",
- "session_status"
- ],
- "fields": [
- {
- "fieldname": "account_name",
- "fieldtype": "Data",
- "label": "Account Name",
- "read_only": 1
- },
- {
- "fieldname": "oauth_details",
- "fieldtype": "Section Break",
- "label": "OAuth Credentials"
- },
- {
- "fieldname": "consumer_key",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Consumer Key",
- "reqd": 1
- },
- {
- "fieldname": "consumer_secret",
- "fieldtype": "Password",
- "in_list_view": 1,
- "label": "Consumer Secret",
- "reqd": 1
- },
- {
- "fieldname": "access_token",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Access Token",
- "read_only": 1
- },
- {
- "fieldname": "person_urn",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Person URN",
- "read_only": 1
- },
- {
- "fieldname": "column_break_5",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "user_details_section",
- "fieldtype": "Section Break",
- "label": "User Details"
- },
- {
- "fieldname": "session_status",
- "fieldtype": "Select",
- "hidden": 1,
- "label": "Session Status",
- "options": "Expired\nActive",
- "read_only": 1
- },
- {
- "fieldname": "column_break_2",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "company_id",
- "fieldtype": "Data",
- "label": "Company ID",
- "reqd": 1
- }
- ],
- "issingle": 1,
- "links": [],
- "modified": "2021-02-18 15:19:21.920725",
- "modified_by": "Administrator",
- "module": "CRM",
- "name": "LinkedIn Settings",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
deleted file mode 100644
index 64b3a017b46..00000000000
--- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
+++ /dev/null
@@ -1,208 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from urllib.parse import urlencode
-
-import frappe
-import requests
-from frappe import _
-from frappe.model.document import Document
-from frappe.utils import get_url_to_form
-from frappe.utils.file_manager import get_file_path
-
-
-class LinkedInSettings(Document):
- @frappe.whitelist()
- def get_authorization_url(self):
- params = urlencode(
- {
- "response_type": "code",
- "client_id": self.consumer_key,
- "redirect_uri": "{0}/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?".format(
- frappe.utils.get_url()
- ),
- "scope": "r_emailaddress w_organization_social r_basicprofile r_liteprofile r_organization_social rw_organization_admin w_member_social",
- }
- )
-
- url = "https://www.linkedin.com/oauth/v2/authorization?{}".format(params)
-
- return url
-
- def get_access_token(self, code):
- url = "https://www.linkedin.com/oauth/v2/accessToken"
- body = {
- "grant_type": "authorization_code",
- "code": code,
- "client_id": self.consumer_key,
- "client_secret": self.get_password(fieldname="consumer_secret"),
- "redirect_uri": "{0}/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?".format(
- frappe.utils.get_url()
- ),
- }
- headers = {"Content-Type": "application/x-www-form-urlencoded"}
-
- response = self.http_post(url=url, data=body, headers=headers)
- response = frappe.parse_json(response.content.decode())
- self.db_set("access_token", response["access_token"])
-
- def get_member_profile(self):
- response = requests.get(url="https://api.linkedin.com/v2/me", headers=self.get_headers())
- response = frappe.parse_json(response.content.decode())
-
- frappe.db.set_value(
- self.doctype,
- self.name,
- {
- "person_urn": response["id"],
- "account_name": response["vanityName"],
- "session_status": "Active",
- },
- )
- frappe.local.response["type"] = "redirect"
- frappe.local.response["location"] = get_url_to_form("LinkedIn Settings", "LinkedIn Settings")
-
- def post(self, text, title, media=None):
- if not media:
- return self.post_text(text, title)
- else:
- media_id = self.upload_image(media)
-
- if media_id:
- return self.post_text(text, title, media_id=media_id)
- else:
- self.log_error("LinkedIn: Failed to upload media")
-
- def upload_image(self, media):
- media = get_file_path(media)
- register_url = "https://api.linkedin.com/v2/assets?action=registerUpload"
- body = {
- "registerUploadRequest": {
- "recipes": ["urn:li:digitalmediaRecipe:feedshare-image"],
- "owner": "urn:li:organization:{0}".format(self.company_id),
- "serviceRelationships": [
- {"relationshipType": "OWNER", "identifier": "urn:li:userGeneratedContent"}
- ],
- }
- }
- headers = self.get_headers()
- response = self.http_post(url=register_url, body=body, headers=headers)
-
- if response.status_code == 200:
- response = response.json()
- asset = response["value"]["asset"]
- upload_url = response["value"]["uploadMechanism"][
- "com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest"
- ]["uploadUrl"]
- headers["Content-Type"] = "image/jpeg"
- response = self.http_post(upload_url, headers=headers, data=open(media, "rb"))
- if response.status_code < 200 and response.status_code > 299:
- frappe.throw(
- _("Error While Uploading Image"),
- title="{0} {1}".format(response.status_code, response.reason),
- )
- return None
- return asset
-
- return None
-
- def post_text(self, text, title, media_id=None):
- url = "https://api.linkedin.com/v2/shares"
- headers = self.get_headers()
- headers["X-Restli-Protocol-Version"] = "2.0.0"
- headers["Content-Type"] = "application/json; charset=UTF-8"
-
- body = {
- "distribution": {"linkedInDistributionTarget": {}},
- "owner": "urn:li:organization:{0}".format(self.company_id),
- "subject": title,
- "text": {"text": text},
- }
-
- reference_url = self.get_reference_url(text)
- if reference_url:
- body["content"] = {"contentEntities": [{"entityLocation": reference_url}]}
-
- if media_id:
- body["content"] = {"contentEntities": [{"entity": media_id}], "shareMediaCategory": "IMAGE"}
-
- response = self.http_post(url=url, headers=headers, body=body)
- return response
-
- def http_post(self, url, headers=None, body=None, data=None):
- try:
- response = requests.post(url=url, json=body, data=data, headers=headers)
- if response.status_code not in [201, 200]:
- raise
-
- except Exception as e:
- self.api_error(response)
-
- return response
-
- def get_headers(self):
- return {"Authorization": "Bearer {}".format(self.access_token)}
-
- def get_reference_url(self, text):
- import re
-
- regex_url = r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+"
- urls = re.findall(regex_url, text)
- if urls:
- return urls[0]
-
- def delete_post(self, post_id):
- try:
- response = requests.delete(
- url="https://api.linkedin.com/v2/shares/urn:li:share:{0}".format(post_id),
- headers=self.get_headers(),
- )
- if response.status_code != 200:
- raise
- except Exception:
- self.api_error(response)
-
- def get_post(self, post_id):
- url = "https://api.linkedin.com/v2/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:{0}&shares[0]=urn:li:share:{1}".format(
- self.company_id, post_id
- )
-
- try:
- response = requests.get(url=url, headers=self.get_headers())
- if response.status_code != 200:
- raise
-
- except Exception:
- self.api_error(response)
-
- response = frappe.parse_json(response.content.decode())
- if len(response.elements):
- return response.elements[0]
-
- return None
-
- def api_error(self, response):
- content = frappe.parse_json(response.content.decode())
-
- if response.status_code == 401:
- self.db_set("session_status", "Expired")
- frappe.db.commit()
- frappe.throw(content["message"], title=_("LinkedIn Error - Unauthorized"))
- elif response.status_code == 403:
- frappe.msgprint(_("You didn't have permission to access this API"))
- frappe.throw(content["message"], title=_("LinkedIn Error - Access Denied"))
- else:
- frappe.throw(response.reason, title=response.status_code)
-
-
-@frappe.whitelist(allow_guest=True)
-def callback(code=None, error=None, error_description=None):
- if not error:
- linkedin_settings = frappe.get_doc("LinkedIn Settings")
- linkedin_settings.get_access_token(code)
- linkedin_settings.get_member_profile()
- frappe.db.commit()
- else:
- frappe.local.response["type"] = "redirect"
- frappe.local.response["location"] = get_url_to_form("LinkedIn Settings", "LinkedIn Settings")
diff --git a/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py
deleted file mode 100644
index 09732e405ee..00000000000
--- a/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-# import frappe
-import unittest
-
-
-class TestLinkedInSettings(unittest.TestCase):
- pass
diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js
deleted file mode 100644
index d4ac0bad16c..00000000000
--- a/erpnext/crm/doctype/social_media_post/social_media_post.js
+++ /dev/null
@@ -1,139 +0,0 @@
-// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-frappe.ui.form.on('Social Media Post', {
- validate: function(frm) {
- if (frm.doc.twitter === 0 && frm.doc.linkedin === 0) {
- frappe.throw(__("Select atleast one Social Media Platform to Share on."));
- }
- if (frm.doc.scheduled_time) {
- let scheduled_time = new Date(frm.doc.scheduled_time);
- let date_time = new Date();
- if (scheduled_time.getTime() < date_time.getTime()) {
- frappe.throw(__("Scheduled Time must be a future time."));
- }
- }
- frm.trigger('validate_tweet_length');
- },
-
- text: function(frm) {
- if (frm.doc.text) {
- frm.set_df_property('text', 'description', `${frm.doc.text.length}/280`);
- frm.refresh_field('text');
- frm.trigger('validate_tweet_length');
- }
- },
-
- validate_tweet_length: function(frm) {
- if (frm.doc.text && frm.doc.text.length > 280) {
- frappe.throw(__("Tweet length Must be less than 280."));
- }
- },
-
- onload: function(frm) {
- frm.trigger('make_dashboard');
- },
-
- make_dashboard: function(frm) {
- if (frm.doc.post_status == "Posted") {
- frappe.call({
- doc: frm.doc,
- method: 'get_post',
- freeze: true,
- callback: (r) => {
- if (!r.message) {
- return;
- }
-
- let datasets = [], colors = [];
- if (r.message && r.message.twitter) {
- colors.push('#1DA1F2');
- datasets.push({
- name: 'Twitter',
- values: [r.message.twitter.favorite_count, r.message.twitter.retweet_count]
- });
- }
- if (r.message && r.message.linkedin) {
- colors.push('#0077b5');
- datasets.push({
- name: 'LinkedIn',
- values: [r.message.linkedin.totalShareStatistics.likeCount, r.message.linkedin.totalShareStatistics.shareCount]
- });
- }
-
- if (datasets.length) {
- frm.dashboard.render_graph({
- data: {
- labels: ['Likes', 'Retweets/Shares'],
- datasets: datasets
- },
-
- title: __("Post Metrics"),
- type: 'bar',
- height: 300,
- colors: colors
- });
- }
- }
- });
- }
- },
-
- refresh: function(frm) {
- frm.trigger('text');
-
- if (frm.doc.docstatus === 1) {
- if (!['Posted', 'Deleted'].includes(frm.doc.post_status)) {
- frm.trigger('add_post_btn');
- }
- if (frm.doc.post_status !='Deleted') {
- frm.add_custom_button(__('Delete Post'), function() {
- frappe.confirm(__('Are you sure want to delete the Post from Social Media platforms?'),
- function() {
- frappe.call({
- doc: frm.doc,
- method: 'delete_post',
- freeze: true,
- callback: () => {
- frm.reload_doc();
- }
- });
- }
- );
- });
- }
-
- if (frm.doc.post_status !='Deleted') {
- let html='';
- if (frm.doc.twitter) {
- let color = frm.doc.twitter_post_id ? "green" : "red";
- let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted";
- html += `
- Twitter : ${status}
-
` ;
- }
- if (frm.doc.linkedin) {
- let color = frm.doc.linkedin_post_id ? "green" : "red";
- let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted";
- html += `
- LinkedIn : ${status}
-
` ;
- }
- html = `${html}
`;
- frm.dashboard.set_headline_alert(html);
- }
- }
- },
-
- add_post_btn: function(frm) {
- frm.add_custom_button(__('Post Now'), function() {
- frappe.call({
- doc: frm.doc,
- method: 'post',
- freeze: true,
- callback: function() {
- frm.reload_doc();
- }
- });
- });
- }
-});
diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.json b/erpnext/crm/doctype/social_media_post/social_media_post.json
deleted file mode 100644
index 98e78f949e8..00000000000
--- a/erpnext/crm/doctype/social_media_post/social_media_post.json
+++ /dev/null
@@ -1,208 +0,0 @@
-{
- "actions": [],
- "autoname": "format: CRM-SMP-{YYYY}-{MM}-{DD}-{###}",
- "creation": "2020-01-30 11:53:13.872864",
- "doctype": "DocType",
- "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/social-media-post",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "title",
- "campaign_name",
- "scheduled_time",
- "post_status",
- "column_break_6",
- "twitter",
- "linkedin",
- "twitter_post_id",
- "linkedin_post_id",
- "content",
- "text",
- "column_break_14",
- "tweet_preview",
- "linkedin_section",
- "linkedin_post",
- "column_break_15",
- "attachments_section",
- "image",
- "amended_from"
- ],
- "fields": [
- {
- "fieldname": "text",
- "fieldtype": "Small Text",
- "label": "Tweet",
- "mandatory_depends_on": "eval:doc.twitter ==1"
- },
- {
- "fieldname": "image",
- "fieldtype": "Attach Image",
- "label": "Image"
- },
- {
- "default": "1",
- "fieldname": "twitter",
- "fieldtype": "Check",
- "label": "Twitter"
- },
- {
- "default": "1",
- "fieldname": "linkedin",
- "fieldtype": "Check",
- "label": "LinkedIn"
- },
- {
- "fieldname": "amended_from",
- "fieldtype": "Link",
- "label": "Amended From",
- "no_copy": 1,
- "options": "Social Media Post",
- "print_hide": 1,
- "read_only": 1
- },
- {
- "depends_on": "eval:doc.twitter ==1",
- "fieldname": "content",
- "fieldtype": "Section Break",
- "label": "Twitter"
- },
- {
- "allow_on_submit": 1,
- "fieldname": "post_status",
- "fieldtype": "Select",
- "label": "Post Status",
- "no_copy": 1,
- "options": "\nScheduled\nPosted\nCancelled\nDeleted\nError",
- "read_only": 1
- },
- {
- "allow_on_submit": 1,
- "fieldname": "twitter_post_id",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Twitter Post Id",
- "no_copy": 1,
- "read_only": 1
- },
- {
- "allow_on_submit": 1,
- "fieldname": "linkedin_post_id",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "LinkedIn Post Id",
- "no_copy": 1,
- "read_only": 1
- },
- {
- "fieldname": "campaign_name",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Campaign",
- "options": "Campaign"
- },
- {
- "fieldname": "column_break_6",
- "fieldtype": "Column Break",
- "label": "Share On"
- },
- {
- "fieldname": "column_break_14",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "tweet_preview",
- "fieldtype": "HTML"
- },
- {
- "collapsible": 1,
- "depends_on": "eval:doc.linkedin==1",
- "fieldname": "linkedin_section",
- "fieldtype": "Section Break",
- "label": "LinkedIn"
- },
- {
- "collapsible": 1,
- "fieldname": "attachments_section",
- "fieldtype": "Section Break",
- "label": "Attachments"
- },
- {
- "fieldname": "linkedin_post",
- "fieldtype": "Text",
- "label": "Post",
- "mandatory_depends_on": "eval:doc.linkedin ==1"
- },
- {
- "fieldname": "column_break_15",
- "fieldtype": "Column Break"
- },
- {
- "allow_on_submit": 1,
- "fieldname": "scheduled_time",
- "fieldtype": "Datetime",
- "label": "Scheduled Time",
- "read_only_depends_on": "eval:doc.post_status == \"Posted\""
- },
- {
- "fieldname": "title",
- "fieldtype": "Data",
- "label": "Title",
- "reqd": 1
- }
- ],
- "is_submittable": 1,
- "links": [],
- "modified": "2021-04-14 14:24:59.821223",
- "modified_by": "Administrator",
- "module": "CRM",
- "name": "Social Media Post",
- "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
- },
- {
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Sales User",
- "share": 1,
- "submit": 1,
- "write": 1
- },
- {
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Sales Manager",
- "share": 1,
- "submit": 1,
- "write": 1
- }
- ],
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "title",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.py b/erpnext/crm/doctype/social_media_post/social_media_post.py
deleted file mode 100644
index 3654d29bdc0..00000000000
--- a/erpnext/crm/doctype/social_media_post/social_media_post.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import datetime
-
-import frappe
-from frappe import _
-from frappe.model.document import Document
-
-
-class SocialMediaPost(Document):
- def validate(self):
- if not self.twitter and not self.linkedin:
- frappe.throw(_("Select atleast one Social Media Platform to Share on."))
-
- if self.scheduled_time:
- current_time = frappe.utils.now_datetime()
- scheduled_time = frappe.utils.get_datetime(self.scheduled_time)
- if scheduled_time < current_time:
- frappe.throw(_("Scheduled Time must be a future time."))
-
- if self.text and len(self.text) > 280:
- frappe.throw(_("Tweet length must be less than 280."))
-
- def submit(self):
- if self.scheduled_time:
- self.post_status = "Scheduled"
- super(SocialMediaPost, self).submit()
-
- def on_cancel(self):
- self.db_set("post_status", "Cancelled")
-
- @frappe.whitelist()
- def delete_post(self):
- if self.twitter and self.twitter_post_id:
- twitter = frappe.get_doc("Twitter Settings")
- twitter.delete_tweet(self.twitter_post_id)
-
- if self.linkedin and self.linkedin_post_id:
- linkedin = frappe.get_doc("LinkedIn Settings")
- linkedin.delete_post(self.linkedin_post_id)
-
- self.db_set("post_status", "Deleted")
-
- @frappe.whitelist()
- def get_post(self):
- response = {}
- if self.linkedin and self.linkedin_post_id:
- linkedin = frappe.get_doc("LinkedIn Settings")
- response["linkedin"] = linkedin.get_post(self.linkedin_post_id)
- if self.twitter and self.twitter_post_id:
- twitter = frappe.get_doc("Twitter Settings")
- response["twitter"] = twitter.get_tweet(self.twitter_post_id)
-
- return response
-
- @frappe.whitelist()
- def post(self):
- try:
- if self.twitter and not self.twitter_post_id:
- twitter = frappe.get_doc("Twitter Settings")
- twitter_post = twitter.post(self.text, self.image)
- self.db_set("twitter_post_id", twitter_post.id)
- if self.linkedin and not self.linkedin_post_id:
- linkedin = frappe.get_doc("LinkedIn Settings")
- linkedin_post = linkedin.post(self.linkedin_post, self.title, self.image)
- self.db_set("linkedin_post_id", linkedin_post.headers["X-RestLi-Id"])
- self.db_set("post_status", "Posted")
-
- except Exception:
- self.db_set("post_status", "Error")
- self.log_error("Social posting failed")
-
-
-def process_scheduled_social_media_posts():
- posts = frappe.get_all(
- "Social Media Post",
- filters={"post_status": "Scheduled", "docstatus": 1},
- fields=["name", "scheduled_time"],
- )
- start = frappe.utils.now_datetime()
- end = start + datetime.timedelta(minutes=10)
- for post in posts:
- if post.scheduled_time:
- post_time = frappe.utils.get_datetime(post.scheduled_time)
- if post_time > start and post_time <= end:
- sm_post = frappe.get_doc("Social Media Post", post.name)
- sm_post.post()
diff --git a/erpnext/crm/doctype/social_media_post/social_media_post_list.js b/erpnext/crm/doctype/social_media_post/social_media_post_list.js
deleted file mode 100644
index a8c8272ad08..00000000000
--- a/erpnext/crm/doctype/social_media_post/social_media_post_list.js
+++ /dev/null
@@ -1,11 +0,0 @@
-frappe.listview_settings['Social Media Post'] = {
- add_fields: ["status", "post_status"],
- get_indicator: function(doc) {
- return [__(doc.post_status), {
- "Scheduled": "orange",
- "Posted": "green",
- "Error": "red",
- "Deleted": "red"
- }[doc.post_status]];
- }
-}
diff --git a/erpnext/crm/doctype/social_media_post/test_social_media_post.py b/erpnext/crm/doctype/social_media_post/test_social_media_post.py
deleted file mode 100644
index 75744767dca..00000000000
--- a/erpnext/crm/doctype/social_media_post/test_social_media_post.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-# import frappe
-import unittest
-
-
-class TestSocialMediaPost(unittest.TestCase):
- pass
diff --git a/erpnext/crm/doctype/twitter_settings/__init__.py b/erpnext/crm/doctype/twitter_settings/__init__.py
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py b/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py
deleted file mode 100644
index 9dbce8f8aba..00000000000
--- a/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-# import frappe
-import unittest
-
-
-class TestTwitterSettings(unittest.TestCase):
- pass
diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.js b/erpnext/crm/doctype/twitter_settings/twitter_settings.js
deleted file mode 100644
index c322092d6f3..00000000000
--- a/erpnext/crm/doctype/twitter_settings/twitter_settings.js
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Twitter Settings', {
- onload: function(frm) {
- if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){
- frappe.confirm(
- __('Session not valid, Do you want to login?'),
- function(){
- frm.trigger("login");
- },
- function(){
- window.close();
- }
- );
- }
- frm.dashboard.set_headline(__("For more information, {0}.", [`${__('click here')}`]));
- },
- refresh: function(frm) {
- let msg, color, flag=false;
- if (frm.doc.session_status == "Active") {
- msg = __("Session Active");
- color = 'green';
- flag = true;
- }
- else if(frm.doc.consumer_key && frm.doc.consumer_secret) {
- msg = __("Session Not Active. Save doc to login.");
- color = 'red';
- flag = true;
- }
-
- if (flag) {
- frm.dashboard.set_headline_alert(
- ``
- );
- }
- },
- login: function(frm) {
- if (frm.doc.consumer_key && frm.doc.consumer_secret){
- frappe.dom.freeze();
- frappe.call({
- doc: frm.doc,
- method: "get_authorize_url",
- callback : function(r) {
- window.location.href = r.message;
- }
- }).fail(function() {
- frappe.dom.unfreeze();
- });
- }
- },
- after_save: function(frm) {
- frm.trigger("login");
- }
-});
diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.json b/erpnext/crm/doctype/twitter_settings/twitter_settings.json
deleted file mode 100644
index 8d05877f060..00000000000
--- a/erpnext/crm/doctype/twitter_settings/twitter_settings.json
+++ /dev/null
@@ -1,102 +0,0 @@
-{
- "actions": [],
- "creation": "2020-01-30 10:29:08.562108",
- "doctype": "DocType",
- "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/twitter-settings",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "account_name",
- "profile_pic",
- "oauth_details",
- "consumer_key",
- "column_break_5",
- "consumer_secret",
- "access_token",
- "access_token_secret",
- "session_status"
- ],
- "fields": [
- {
- "fieldname": "account_name",
- "fieldtype": "Data",
- "label": "Account Name",
- "read_only": 1
- },
- {
- "fieldname": "oauth_details",
- "fieldtype": "Section Break",
- "label": "OAuth Credentials"
- },
- {
- "fieldname": "consumer_key",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "API Key",
- "reqd": 1
- },
- {
- "fieldname": "consumer_secret",
- "fieldtype": "Password",
- "in_list_view": 1,
- "label": "API Secret Key",
- "reqd": 1
- },
- {
- "fieldname": "column_break_5",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "profile_pic",
- "fieldtype": "Attach Image",
- "hidden": 1,
- "read_only": 1
- },
- {
- "fieldname": "session_status",
- "fieldtype": "Select",
- "hidden": 1,
- "label": "Session Status",
- "options": "Expired\nActive",
- "read_only": 1
- },
- {
- "fieldname": "access_token",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Access Token",
- "read_only": 1
- },
- {
- "fieldname": "access_token_secret",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Access Token Secret",
- "read_only": 1
- }
- ],
- "image_field": "profile_pic",
- "issingle": 1,
- "links": [],
- "modified": "2021-02-18 15:18:07.900031",
- "modified_by": "Administrator",
- "module": "CRM",
- "name": "Twitter Settings",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py
deleted file mode 100644
index 442aa77a5ff..00000000000
--- a/erpnext/crm/doctype/twitter_settings/twitter_settings.py
+++ /dev/null
@@ -1,141 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import json
-
-import frappe
-import tweepy
-from frappe import _
-from frappe.model.document import Document
-from frappe.utils import get_url_to_form
-from frappe.utils.file_manager import get_file_path
-
-
-class TwitterSettings(Document):
- @frappe.whitelist()
- def get_authorize_url(self):
- callback_url = (
- "{0}/api/method/erpnext.crm.doctype.twitter_settings.twitter_settings.callback?".format(
- frappe.utils.get_url()
- )
- )
- auth = tweepy.OAuth1UserHandler(
- self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url
- )
- try:
- redirect_url = auth.get_authorization_url()
- return redirect_url
- except (tweepy.TweepyException, tweepy.HTTPException) as e:
- frappe.msgprint(_("Error! Failed to get request token."))
- frappe.throw(
- _("Invalid {0} or {1}").format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key"))
- )
-
- def get_access_token(self, oauth_token, oauth_verifier):
- auth = tweepy.OAuth1UserHandler(
- self.consumer_key, self.get_password(fieldname="consumer_secret")
- )
- auth.request_token = {"oauth_token": oauth_token, "oauth_token_secret": oauth_verifier}
-
- try:
- auth.get_access_token(oauth_verifier)
- self.access_token = auth.access_token
- self.access_token_secret = auth.access_token_secret
- api = self.get_api()
- user = api.me()
- profile_pic = (user._json["profile_image_url"]).replace("_normal", "")
-
- frappe.db.set_value(
- self.doctype,
- self.name,
- {
- "access_token": auth.access_token,
- "access_token_secret": auth.access_token_secret,
- "account_name": user._json["screen_name"],
- "profile_pic": profile_pic,
- "session_status": "Active",
- },
- )
-
- frappe.local.response["type"] = "redirect"
- frappe.local.response["location"] = get_url_to_form("Twitter Settings", "Twitter Settings")
- except (tweepy.TweepyException, tweepy.HTTPException) as e:
- frappe.msgprint(_("Error! Failed to get access token."))
- frappe.throw(_("Invalid Consumer Key or Consumer Secret Key"))
-
- def get_api(self):
- # authentication of consumer key and secret
- auth = tweepy.OAuth1UserHandler(
- self.consumer_key, self.get_password(fieldname="consumer_secret")
- )
- # authentication of access token and secret
- auth.set_access_token(self.access_token, self.access_token_secret)
-
- return tweepy.API(auth)
-
- def post(self, text, media=None):
- if not media:
- return self.send_tweet(text)
-
- if media:
- media_id = self.upload_image(media)
- return self.send_tweet(text, media_id)
-
- def upload_image(self, media):
- media = get_file_path(media)
- api = self.get_api()
- media = api.media_upload(media)
-
- return media.media_id
-
- def send_tweet(self, text, media_id=None):
- api = self.get_api()
- try:
- if media_id:
- response = api.update_status(status=text, media_ids=[media_id])
- else:
- response = api.update_status(status=text)
-
- return response
-
- except (tweepy.TweepyException, tweepy.HTTPException) as e:
- self.api_error(e)
-
- def delete_tweet(self, tweet_id):
- api = self.get_api()
- try:
- api.destroy_status(tweet_id)
- except (tweepy.TweepyException, tweepy.HTTPException) as e:
- self.api_error(e)
-
- def get_tweet(self, tweet_id):
- api = self.get_api()
- try:
- response = api.get_status(tweet_id, trim_user=True, include_entities=True)
- except (tweepy.TweepyException, tweepy.HTTPException) as e:
- self.api_error(e)
-
- return response._json
-
- def api_error(self, e):
- content = json.loads(e.response.content)
- content = content["errors"][0]
- if e.response.status_code == 401:
- self.db_set("session_status", "Expired")
- frappe.db.commit()
- frappe.throw(
- content["message"],
- title=_("Twitter Error {0} : {1}").format(e.response.status_code, e.response.reason),
- )
-
-
-@frappe.whitelist(allow_guest=True)
-def callback(oauth_token=None, oauth_verifier=None):
- if oauth_token and oauth_verifier:
- twitter_settings = frappe.get_single("Twitter Settings")
- twitter_settings.get_access_token(oauth_token, oauth_verifier)
- frappe.db.commit()
- else:
- frappe.local.response["type"] = "redirect"
- frappe.local.response["location"] = get_url_to_form("Twitter Settings", "Twitter Settings")
diff --git a/erpnext/crm/workspace/crm/crm.json b/erpnext/crm/workspace/crm/crm.json
index b107df76f8f..4b5b9af714b 100644
--- a/erpnext/crm/workspace/crm/crm.json
+++ b/erpnext/crm/workspace/crm/crm.json
@@ -122,131 +122,6 @@
"onboard": 0,
"type": "Link"
},
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Campaign",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Campaign",
- "link_count": 0,
- "link_to": "Campaign",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Email Campaign",
- "link_count": 0,
- "link_to": "Email Campaign",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Social Media Post",
- "link_count": 0,
- "link_to": "Social Media Post",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "SMS Center",
- "link_count": 0,
- "link_to": "SMS Center",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "SMS Log",
- "link_count": 0,
- "link_to": "SMS Log",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Email Group",
- "link_count": 0,
- "link_to": "Email Group",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Settings",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "CRM Settings",
- "link_count": 0,
- "link_to": "CRM Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "SMS Settings",
- "link_count": 0,
- "link_to": "SMS Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Twitter Settings",
- "link_count": 0,
- "link_to": "Twitter Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "LinkedIn Settings",
- "link_count": 0,
- "link_to": "LinkedIn Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
{
"hidden": 0,
"is_query_report": 0,
@@ -450,9 +325,101 @@
"link_type": "DocType",
"onboard": 0,
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Settings",
+ "link_count": 2,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "CRM Settings",
+ "link_count": 0,
+ "link_to": "CRM Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "SMS Settings",
+ "link_count": 0,
+ "link_to": "SMS Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Campaign",
+ "link_count": 5,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Campaign",
+ "link_count": 0,
+ "link_to": "Campaign",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Email Campaign",
+ "link_count": 0,
+ "link_to": "Email Campaign",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "SMS Center",
+ "link_count": 0,
+ "link_to": "SMS Center",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "SMS Log",
+ "link_count": 0,
+ "link_to": "SMS Log",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Email Group",
+ "link_count": 0,
+ "link_to": "Email Group",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2023-05-26 16:49:04.298122",
+ "modified": "2023-09-14 12:11:03.968048",
"modified_by": "Administrator",
"module": "CRM",
"name": "CRM",
@@ -463,7 +430,7 @@
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
- "sequence_id": 10.0,
+ "sequence_id": 17.0,
"shortcuts": [
{
"color": "Blue",
diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py
index 85d9a6585ce..c66ae1d6009 100644
--- a/erpnext/e_commerce/shopping_cart/cart.py
+++ b/erpnext/e_commerce/shopping_cart/cart.py
@@ -631,7 +631,6 @@ def get_applicable_shipping_rules(party=None, quotation=None):
shipping_rules = get_shipping_rules(quotation)
if shipping_rules:
- rule_label_map = frappe.db.get_values("Shipping Rule", shipping_rules, "label")
# we need this in sorted order as per the position of the rule in the settings page
return [[rule, rule] for rule in shipping_rules]
diff --git a/erpnext/erpnext_integrations/connectors/__init__.py b/erpnext/erpnext_integrations/connectors/__init__.py
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py
deleted file mode 100644
index 2b2da7b971b..00000000000
--- a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py
+++ /dev/null
@@ -1,256 +0,0 @@
-import base64
-import hashlib
-import hmac
-import json
-
-import frappe
-from frappe import _
-from frappe.utils import cstr
-
-
-def verify_request():
- woocommerce_settings = frappe.get_doc("Woocommerce Settings")
- sig = base64.b64encode(
- hmac.new(
- woocommerce_settings.secret.encode("utf8"), frappe.request.data, hashlib.sha256
- ).digest()
- )
-
- if (
- frappe.request.data
- and not sig == frappe.get_request_header("X-Wc-Webhook-Signature", "").encode()
- ):
- frappe.throw(_("Unverified Webhook Data"))
- frappe.set_user(woocommerce_settings.creation_user)
-
-
-@frappe.whitelist(allow_guest=True)
-def order(*args, **kwargs):
- try:
- _order(*args, **kwargs)
- except Exception:
- error_message = (
- frappe.get_traceback() + "\n\n Request Data: \n" + json.loads(frappe.request.data).__str__()
- )
- frappe.log_error("WooCommerce Error", error_message)
- raise
-
-
-def _order(*args, **kwargs):
- woocommerce_settings = frappe.get_doc("Woocommerce Settings")
- if frappe.flags.woocomm_test_order_data:
- order = frappe.flags.woocomm_test_order_data
- event = "created"
- # Ignore the test ping issued during WooCommerce webhook configuration
- # Ref: https://github.com/woocommerce/woocommerce/issues/15642
- if frappe.request.data.decode("utf-8").startswith("webhook_id="):
- return "success"
- elif frappe.request and frappe.request.data:
- verify_request()
- try:
- order = json.loads(frappe.request.data)
- except ValueError:
- # woocommerce returns 'webhook_id=value' for the first request which is not JSON
- order = frappe.request.data
- event = frappe.get_request_header("X-Wc-Webhook-Event")
-
- else:
- return "success"
-
- if event == "created":
- sys_lang = frappe.get_single("System Settings").language or "en"
- raw_billing_data = order.get("billing")
- raw_shipping_data = order.get("shipping")
- customer_name = raw_billing_data.get("first_name") + " " + raw_billing_data.get("last_name")
- link_customer_and_address(raw_billing_data, raw_shipping_data, customer_name)
- link_items(order.get("line_items"), woocommerce_settings, sys_lang)
- create_sales_order(order, woocommerce_settings, customer_name, sys_lang)
-
-
-def link_customer_and_address(raw_billing_data, raw_shipping_data, customer_name):
- customer_woo_com_email = raw_billing_data.get("email")
- customer_exists = frappe.get_value("Customer", {"woocommerce_email": customer_woo_com_email})
- if not customer_exists:
- # Create Customer
- customer = frappe.new_doc("Customer")
- else:
- # Edit Customer
- customer = frappe.get_doc("Customer", {"woocommerce_email": customer_woo_com_email})
- old_name = customer.customer_name
-
- customer.customer_name = customer_name
- customer.woocommerce_email = customer_woo_com_email
- customer.flags.ignore_mandatory = True
- customer.save()
-
- if customer_exists:
- # Fixes https://github.com/frappe/erpnext/issues/33708
- if old_name != customer_name:
- frappe.rename_doc("Customer", old_name, customer_name)
- for address_type in (
- "Billing",
- "Shipping",
- ):
- try:
- address = frappe.get_doc(
- "Address", {"woocommerce_email": customer_woo_com_email, "address_type": address_type}
- )
- rename_address(address, customer)
- except (
- frappe.DoesNotExistError,
- frappe.DuplicateEntryError,
- frappe.ValidationError,
- ):
- pass
- else:
- create_address(raw_billing_data, customer, "Billing")
- create_address(raw_shipping_data, customer, "Shipping")
- create_contact(raw_billing_data, customer)
-
-
-def create_contact(data, customer):
- email = data.get("email", None)
- phone = data.get("phone", None)
-
- if not email and not phone:
- return
-
- contact = frappe.new_doc("Contact")
- contact.first_name = data.get("first_name")
- contact.last_name = data.get("last_name")
- contact.is_primary_contact = 1
- contact.is_billing_contact = 1
-
- if phone:
- contact.add_phone(phone, is_primary_mobile_no=1, is_primary_phone=1)
-
- if email:
- contact.add_email(email, is_primary=1)
-
- contact.append("links", {"link_doctype": "Customer", "link_name": customer.name})
-
- contact.flags.ignore_mandatory = True
- contact.save()
-
-
-def create_address(raw_data, customer, address_type):
- address = frappe.new_doc("Address")
-
- address.address_line1 = raw_data.get("address_1", "Not Provided")
- address.address_line2 = raw_data.get("address_2", "Not Provided")
- address.city = raw_data.get("city", "Not Provided")
- address.woocommerce_email = customer.woocommerce_email
- address.address_type = address_type
- address.country = frappe.get_value("Country", {"code": raw_data.get("country", "IN").lower()})
- address.state = raw_data.get("state")
- address.pincode = raw_data.get("postcode")
- address.phone = raw_data.get("phone")
- address.email_id = customer.woocommerce_email
- address.append("links", {"link_doctype": "Customer", "link_name": customer.name})
-
- address.flags.ignore_mandatory = True
- address.save()
-
-
-def rename_address(address, customer):
- old_address_title = address.name
- new_address_title = customer.name + "-" + address.address_type
- address.address_title = customer.customer_name
- address.save()
-
- frappe.rename_doc("Address", old_address_title, new_address_title)
-
-
-def link_items(items_list, woocommerce_settings, sys_lang):
- for item_data in items_list:
- item_woo_com_id = cstr(item_data.get("product_id"))
-
- if not frappe.db.get_value("Item", {"woocommerce_id": item_woo_com_id}, "name"):
- # Create Item
- item = frappe.new_doc("Item")
- item.item_code = _("woocommerce - {0}", sys_lang).format(item_woo_com_id)
- item.stock_uom = woocommerce_settings.uom or _("Nos", sys_lang)
- item.item_group = _("WooCommerce Products", sys_lang)
-
- item.item_name = item_data.get("name")
- item.woocommerce_id = item_woo_com_id
- item.flags.ignore_mandatory = True
- item.save()
-
-
-def create_sales_order(order, woocommerce_settings, customer_name, sys_lang):
- new_sales_order = frappe.new_doc("Sales Order")
- new_sales_order.customer = customer_name
-
- new_sales_order.po_no = new_sales_order.woocommerce_id = order.get("id")
- new_sales_order.naming_series = woocommerce_settings.sales_order_series or "SO-WOO-"
-
- created_date = order.get("date_created").split("T")
- new_sales_order.transaction_date = created_date[0]
- delivery_after = woocommerce_settings.delivery_after_days or 7
- new_sales_order.delivery_date = frappe.utils.add_days(created_date[0], delivery_after)
-
- new_sales_order.company = woocommerce_settings.company
-
- set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_lang)
- new_sales_order.flags.ignore_mandatory = True
- new_sales_order.insert()
- new_sales_order.submit()
-
- frappe.db.commit()
-
-
-def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_lang):
- company_abbr = frappe.db.get_value("Company", woocommerce_settings.company, "abbr")
-
- default_warehouse = _("Stores - {0}", sys_lang).format(company_abbr)
- if not frappe.db.exists("Warehouse", default_warehouse) and not woocommerce_settings.warehouse:
- frappe.throw(_("Please set Warehouse in Woocommerce Settings"))
-
- for item in order.get("line_items"):
- woocomm_item_id = item.get("product_id")
- found_item = frappe.get_doc("Item", {"woocommerce_id": cstr(woocomm_item_id)})
-
- ordered_items_tax = item.get("total_tax")
-
- new_sales_order.append(
- "items",
- {
- "item_code": found_item.name,
- "item_name": found_item.item_name,
- "description": found_item.item_name,
- "delivery_date": new_sales_order.delivery_date,
- "uom": woocommerce_settings.uom or _("Nos", sys_lang),
- "qty": item.get("quantity"),
- "rate": item.get("price"),
- "warehouse": woocommerce_settings.warehouse or default_warehouse,
- },
- )
-
- add_tax_details(
- new_sales_order, ordered_items_tax, "Ordered Item tax", woocommerce_settings.tax_account
- )
-
- # shipping_details = order.get("shipping_lines") # used for detailed order
-
- add_tax_details(
- new_sales_order, order.get("shipping_tax"), "Shipping Tax", woocommerce_settings.f_n_f_account
- )
- add_tax_details(
- new_sales_order,
- order.get("shipping_total"),
- "Shipping Total",
- woocommerce_settings.f_n_f_account,
- )
-
-
-def add_tax_details(sales_order, price, desc, tax_account_head):
- sales_order.append(
- "taxes",
- {
- "charge_type": "Actual",
- "account_head": tax_account_head,
- "tax_amount": price,
- "description": desc,
- },
- )
diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/__init__.py b/erpnext/erpnext_integrations/doctype/woocommerce_settings/__init__.py
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/test_woocommerce_settings.py b/erpnext/erpnext_integrations/doctype/woocommerce_settings/test_woocommerce_settings.py
deleted file mode 100644
index 9945823bf73..00000000000
--- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/test_woocommerce_settings.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestWoocommerceSettings(unittest.TestCase):
- pass
diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.js b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.js
deleted file mode 100644
index d7a3d36a5f1..00000000000
--- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.js
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Woocommerce Settings', {
- refresh (frm) {
- frm.trigger("add_button_generate_secret");
- frm.trigger("check_enabled");
- frm.set_query("tax_account", ()=>{
- return {
- "filters": {
- "company": frappe.defaults.get_default("company"),
- "is_group": 0
- }
- };
- });
- },
-
- enable_sync (frm) {
- frm.trigger("check_enabled");
- },
-
- add_button_generate_secret(frm) {
- frm.add_custom_button(__('Generate Secret'), () => {
- frappe.confirm(
- __("Apps using current key won't be able to access, are you sure?"),
- () => {
- frappe.call({
- type:"POST",
- method:"erpnext.erpnext_integrations.doctype.woocommerce_settings.woocommerce_settings.generate_secret",
- }).done(() => {
- frm.reload_doc();
- }).fail(() => {
- frappe.msgprint(__("Could not generate Secret"));
- });
- }
- );
- });
- },
-
- check_enabled (frm) {
- frm.set_df_property("woocommerce_server_url", "reqd", frm.doc.enable_sync);
- frm.set_df_property("api_consumer_key", "reqd", frm.doc.enable_sync);
- frm.set_df_property("api_consumer_secret", "reqd", frm.doc.enable_sync);
- }
-});
-
-frappe.ui.form.on("Woocommerce Settings", "onload", function () {
- frappe.call({
- method: "erpnext.erpnext_integrations.doctype.woocommerce_settings.woocommerce_settings.get_series",
- callback: function (r) {
- $.each(r.message, function (key, value) {
- set_field_options(key, value);
- });
- }
- });
-});
diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.json b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.json
deleted file mode 100644
index 956ae09cbd6..00000000000
--- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.json
+++ /dev/null
@@ -1,175 +0,0 @@
-{
- "creation": "2018-02-12 15:10:05.495713",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "enable_sync",
- "sb_00",
- "woocommerce_server_url",
- "secret",
- "cb_00",
- "api_consumer_key",
- "api_consumer_secret",
- "sb_accounting_details",
- "tax_account",
- "column_break_10",
- "f_n_f_account",
- "defaults_section",
- "creation_user",
- "warehouse",
- "sales_order_series",
- "column_break_14",
- "company",
- "delivery_after_days",
- "uom",
- "endpoints",
- "endpoint"
- ],
- "fields": [
- {
- "default": "0",
- "fieldname": "enable_sync",
- "fieldtype": "Check",
- "label": "Enable Sync"
- },
- {
- "fieldname": "sb_00",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "woocommerce_server_url",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Woocommerce Server URL"
- },
- {
- "fieldname": "secret",
- "fieldtype": "Code",
- "label": "Secret",
- "read_only": 1
- },
- {
- "fieldname": "cb_00",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "api_consumer_key",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "API consumer key"
- },
- {
- "fieldname": "api_consumer_secret",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "API consumer secret"
- },
- {
- "fieldname": "sb_accounting_details",
- "fieldtype": "Section Break",
- "label": "Accounting Details"
- },
- {
- "fieldname": "tax_account",
- "fieldtype": "Link",
- "label": "Tax Account",
- "options": "Account",
- "reqd": 1
- },
- {
- "fieldname": "column_break_10",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "f_n_f_account",
- "fieldtype": "Link",
- "label": "Freight and Forwarding Account",
- "options": "Account",
- "reqd": 1
- },
- {
- "fieldname": "defaults_section",
- "fieldtype": "Section Break",
- "label": "Defaults"
- },
- {
- "description": "The user that will be used to create Customers, Items and Sales Orders. This user should have the relevant permissions.",
- "fieldname": "creation_user",
- "fieldtype": "Link",
- "label": "Creation User",
- "options": "User",
- "reqd": 1
- },
- {
- "description": "This warehouse will be used to create Sales Orders. The fallback warehouse is \"Stores\".",
- "fieldname": "warehouse",
- "fieldtype": "Link",
- "label": "Warehouse",
- "options": "Warehouse"
- },
- {
- "fieldname": "column_break_14",
- "fieldtype": "Column Break"
- },
- {
- "description": "The fallback series is \"SO-WOO-\".",
- "fieldname": "sales_order_series",
- "fieldtype": "Select",
- "label": "Sales Order Series"
- },
- {
- "description": "This is the default UOM used for items and Sales orders. The fallback UOM is \"Nos\".",
- "fieldname": "uom",
- "fieldtype": "Link",
- "label": "UOM",
- "options": "UOM"
- },
- {
- "fieldname": "endpoints",
- "fieldtype": "Section Break",
- "label": "Endpoints"
- },
- {
- "fieldname": "endpoint",
- "fieldtype": "Code",
- "label": "Endpoint",
- "read_only": 1
- },
- {
- "description": "This company will be used to create Sales Orders.",
- "fieldname": "company",
- "fieldtype": "Link",
- "label": "Company",
- "options": "Company",
- "reqd": 1
- },
- {
- "description": "This is the default offset (days) for the Delivery Date in Sales Orders. The fallback offset is 7 days from the order placement date.",
- "fieldname": "delivery_after_days",
- "fieldtype": "Int",
- "label": "Delivery After (Days)"
- }
- ],
- "issingle": 1,
- "modified": "2019-11-04 00:45:21.232096",
- "modified_by": "Administrator",
- "module": "ERPNext Integrations",
- "name": "Woocommerce Settings",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py
deleted file mode 100644
index 4aa98aab56b..00000000000
--- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py
+++ /dev/null
@@ -1,87 +0,0 @@
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from urllib.parse import urlparse
-
-import frappe
-from frappe import _
-from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
-from frappe.model.document import Document
-from frappe.utils.nestedset import get_root_of
-
-
-class WoocommerceSettings(Document):
- def validate(self):
- self.validate_settings()
- self.create_delete_custom_fields()
- self.create_webhook_url()
-
- def create_delete_custom_fields(self):
- if self.enable_sync:
- create_custom_fields(
- {
- ("Customer", "Sales Order", "Item", "Address"): dict(
- fieldname="woocommerce_id",
- label="Woocommerce ID",
- fieldtype="Data",
- read_only=1,
- print_hide=1,
- ),
- ("Customer", "Address"): dict(
- fieldname="woocommerce_email",
- label="Woocommerce Email",
- fieldtype="Data",
- read_only=1,
- print_hide=1,
- ),
- }
- )
-
- if not frappe.get_value("Item Group", {"name": _("WooCommerce Products")}):
- item_group = frappe.new_doc("Item Group")
- item_group.item_group_name = _("WooCommerce Products")
- item_group.parent_item_group = get_root_of("Item Group")
- item_group.insert()
-
- def validate_settings(self):
- if self.enable_sync:
- if not self.secret:
- self.set("secret", frappe.generate_hash())
-
- if not self.woocommerce_server_url:
- frappe.throw(_("Please enter Woocommerce Server URL"))
-
- if not self.api_consumer_key:
- frappe.throw(_("Please enter API Consumer Key"))
-
- if not self.api_consumer_secret:
- frappe.throw(_("Please enter API Consumer Secret"))
-
- def create_webhook_url(self):
- endpoint = "/api/method/erpnext.erpnext_integrations.connectors.woocommerce_connection.order"
-
- try:
- url = frappe.request.url
- except RuntimeError:
- # for CI Test to work
- url = "http://localhost:8000"
-
- server_url = "{uri.scheme}://{uri.netloc}".format(uri=urlparse(url))
-
- delivery_url = server_url + endpoint
- self.endpoint = delivery_url
-
-
-@frappe.whitelist()
-def generate_secret():
- woocommerce_settings = frappe.get_doc("Woocommerce Settings")
- woocommerce_settings.secret = frappe.generate_hash()
- woocommerce_settings.save()
-
-
-@frappe.whitelist()
-def get_series():
- return {
- "sales_order_series": frappe.get_meta("Sales Order").get_options("naming_series") or "SO-WOO-",
- }
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 41db6b3a725..c7398cce999 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -423,9 +423,6 @@ scheduler_events = {
"erpnext.stock.reorder_item.reorder_item",
],
},
- "all": [
- "erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts",
- ],
"hourly": [
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.projects.doctype.project.project.project_status_update_reminder",
diff --git a/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py
index d8d9f8fbc69..ee5886c1cb0 100644
--- a/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py
+++ b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py
@@ -191,7 +191,7 @@ class TestBOMCreator(FrappeTestCase):
)
doc = make_bom_creator(
- name="Bicycle BOM",
+ name="Bicycle BOM Test",
company="_Test Company",
item_code=final_product,
qty=1,
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index b3d6d3e80a4..d0ee2e4dc4e 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -340,5 +340,6 @@ erpnext.patches.v15_0.remove_exotel_integration
erpnext.patches.v14_0.single_to_multi_dunning
execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0)
erpnext.patches.v15_0.correct_asset_value_if_je_with_workflow
+erpnext.patches.v15_0.delete_woocommerce_settings_doctype
# below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
diff --git a/erpnext/patches/v15_0/delete_woocommerce_settings_doctype.py b/erpnext/patches/v15_0/delete_woocommerce_settings_doctype.py
new file mode 100644
index 00000000000..fb92ca55d17
--- /dev/null
+++ b/erpnext/patches/v15_0/delete_woocommerce_settings_doctype.py
@@ -0,0 +1,5 @@
+import frappe
+
+
+def execute():
+ frappe.delete_doc("DocType", "Woocommerce Settings", ignore_missing=True)
diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js
index 966a9e1f9b3..0e1b23b0eae 100644
--- a/erpnext/public/js/erpnext.bundle.js
+++ b/erpnext/public/js/erpnext.bundle.js
@@ -16,7 +16,8 @@ import "./utils/customer_quick_entry";
import "./utils/supplier_quick_entry";
import "./call_popup/call_popup";
import "./utils/dimension_tree_filter";
-import "./utils/ledger_preview.js"
+import "./utils/ledger_preview.js";
+import "./utils/unreconcile.js";
import "./utils/barcode_scanner";
import "./telephony";
import "./templates/call_link.html";
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 89750f8446c..d435711cf52 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -769,6 +769,9 @@ erpnext.utils.update_child_items = function(opts) {
dialog.show();
}
+
+
+
erpnext.utils.map_current_doc = function(opts) {
function _map() {
if($.isArray(cur_frm.doc.items) && cur_frm.doc.items.length > 0) {
@@ -1097,4 +1100,4 @@ function attach_selector_button(inner_text, append_loction, context, grid_row) {
$btn.on("click", function() {
context.show_serial_batch_selector(grid_row.frm, grid_row.doc, "", "", true);
});
-}
\ No newline at end of file
+}
diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js
new file mode 100644
index 00000000000..bbdd51d6e54
--- /dev/null
+++ b/erpnext/public/js/utils/unreconcile.js
@@ -0,0 +1,127 @@
+frappe.provide('erpnext.accounts');
+
+erpnext.accounts.unreconcile_payments = {
+ add_unreconcile_btn(frm) {
+ if (frm.doc.docstatus == 1) {
+ if(((frm.doc.doctype == "Journal Entry") && (frm.doc.voucher_type != "Journal Entry"))
+ || !["Purchase Invoice", "Sales Invoice", "Journal Entry", "Payment Entry"].includes(frm.doc.doctype)
+ ) {
+ return;
+ }
+
+ frappe.call({
+ "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_references",
+ "args": {
+ "doctype": frm.doc.doctype,
+ "docname": frm.doc.name
+ },
+ callback: function(r) {
+ if (r.message) {
+ frm.add_custom_button(__("Un-Reconcile"), function() {
+ erpnext.accounts.unreconcile_payments.build_unreconcile_dialog(frm);
+ }, __('Actions'));
+ }
+ }
+ });
+ }
+ },
+
+ build_selection_map(frm, selections) {
+ // assuming each row is an individual voucher
+ // pass this to server side method that creates unreconcile doc for each row
+ let selection_map = [];
+ if (['Sales Invoice', 'Purchase Invoice'].includes(frm.doc.doctype)) {
+ selection_map = selections.map(function(elem) {
+ return {
+ company: elem.company,
+ voucher_type: elem.voucher_type,
+ voucher_no: elem.voucher_no,
+ against_voucher_type: frm.doc.doctype,
+ against_voucher_no: frm.doc.name
+ };
+ });
+ } else if (['Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) {
+ selection_map = selections.map(function(elem) {
+ return {
+ company: elem.company,
+ voucher_type: frm.doc.doctype,
+ voucher_no: frm.doc.name,
+ against_voucher_type: elem.voucher_type,
+ against_voucher_no: elem.voucher_no,
+ };
+ });
+ }
+ return selection_map;
+ },
+
+ build_unreconcile_dialog(frm) {
+ if (['Sales Invoice', 'Purchase Invoice', 'Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) {
+ let child_table_fields = [
+ { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1},
+ { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 },
+ { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Currency", in_list_view: 1, read_only: 1 , options: "account_currency"},
+ { label: __("Currency"), fieldname: "account_currency", fieldtype: "Currency", read_only: 1},
+ ]
+ let unreconcile_dialog_fields = [
+ {
+ label: __('Allocations'),
+ fieldname: 'allocations',
+ fieldtype: 'Table',
+ read_only: 1,
+ fields: child_table_fields,
+ },
+ ];
+
+ // get linked payments
+ frappe.call({
+ "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc",
+ "args": {
+ "company": frm.doc.company,
+ "doctype": frm.doc.doctype,
+ "docname": frm.doc.name
+ },
+ callback: function(r) {
+ if (r.message) {
+ // populate child table with allocations
+ unreconcile_dialog_fields[0].data = r.message;
+ unreconcile_dialog_fields[0].get_data = function(){ return r.message};
+
+ let d = new frappe.ui.Dialog({
+ title: 'Un-Reconcile Allocations',
+ fields: unreconcile_dialog_fields,
+ size: 'large',
+ cannot_add_rows: true,
+ primary_action_label: 'Un-Reconcile',
+ primary_action(values) {
+
+ let selected_allocations = values.allocations.filter(x=>x.__checked);
+ if (selected_allocations.length > 0) {
+ let selection_map = erpnext.accounts.unreconcile_payments.build_selection_map(frm, selected_allocations);
+ erpnext.accounts.unreconcile_payments.create_unreconcile_docs(selection_map);
+ d.hide();
+
+ } else {
+ frappe.msgprint("No Selection");
+ }
+ }
+ });
+
+ d.show();
+ }
+ }
+ });
+ }
+ },
+
+ create_unreconcile_docs(selection_map) {
+ frappe.call({
+ "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.create_unreconcile_doc_for_selection",
+ "args": {
+ "selections": selection_map
+ },
+ });
+ }
+
+
+
+}
diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js
index 60f0941559a..e274a52690b 100644
--- a/erpnext/selling/doctype/customer/customer.js
+++ b/erpnext/selling/doctype/customer/customer.js
@@ -23,6 +23,7 @@ frappe.ui.form.on("Customer", {
let d = locals[cdt][cdn];
let filters = {
'account_type': 'Receivable',
+ 'root_type': 'Asset',
'company': d.company,
"is_group": 0
};
diff --git a/erpnext/setup/doctype/authorization_control/authorization_control.py b/erpnext/setup/doctype/authorization_control/authorization_control.py
index 5e77c6fa816..fd5a2012c7d 100644
--- a/erpnext/setup/doctype/authorization_control/authorization_control.py
+++ b/erpnext/setup/doctype/authorization_control/authorization_control.py
@@ -10,7 +10,7 @@ from erpnext.utilities.transaction_base import TransactionBase
class AuthorizationControl(TransactionBase):
- def get_appr_user_role(self, det, doctype_name, total, based_on, condition, item, company):
+ def get_appr_user_role(self, det, doctype_name, total, based_on, condition, master_name, company):
amt_list, appr_users, appr_roles = [], [], []
users, roles = "", ""
if det:
@@ -47,11 +47,11 @@ class AuthorizationControl(TransactionBase):
frappe.msgprint(_("Not authroized since {0} exceeds limits").format(_(based_on)))
frappe.throw(_("Can be approved by {0}").format(comma_or(appr_roles + appr_users)))
- def validate_auth_rule(self, doctype_name, total, based_on, cond, company, item=""):
+ def validate_auth_rule(self, doctype_name, total, based_on, cond, company, master_name=""):
chk = 1
add_cond1, add_cond2 = "", ""
- if based_on == "Itemwise Discount":
- add_cond1 += " and master_name = " + frappe.db.escape(cstr(item))
+ if based_on in ["Itemwise Discount", "Item Group wise Discount"]:
+ add_cond1 += " and master_name = " + frappe.db.escape(cstr(master_name))
itemwise_exists = frappe.db.sql(
"""select value from `tabAuthorization Rule`
where transaction = %s and value <= %s
@@ -71,11 +71,11 @@ class AuthorizationControl(TransactionBase):
if itemwise_exists:
self.get_appr_user_role(
- itemwise_exists, doctype_name, total, based_on, cond + add_cond1, item, company
+ itemwise_exists, doctype_name, total, based_on, cond + add_cond1, master_name, company
)
chk = 0
if chk == 1:
- if based_on == "Itemwise Discount":
+ if based_on in ["Itemwise Discount", "Item Group wise Discount"]:
add_cond2 += " and ifnull(master_name,'') = ''"
appr = frappe.db.sql(
@@ -95,7 +95,9 @@ class AuthorizationControl(TransactionBase):
(doctype_name, total, based_on),
)
- self.get_appr_user_role(appr, doctype_name, total, based_on, cond + add_cond2, item, company)
+ self.get_appr_user_role(
+ appr, doctype_name, total, based_on, cond + add_cond2, master_name, company
+ )
def bifurcate_based_on_type(self, doctype_name, total, av_dis, based_on, doc_obj, val, company):
add_cond = ""
@@ -123,6 +125,12 @@ class AuthorizationControl(TransactionBase):
self.validate_auth_rule(
doctype_name, t.discount_percentage, based_on, add_cond, company, t.item_code
)
+ elif based_on == "Item Group wise Discount":
+ if doc_obj:
+ for t in doc_obj.get("items"):
+ self.validate_auth_rule(
+ doctype_name, t.discount_percentage, based_on, add_cond, company, t.item_group
+ )
else:
self.validate_auth_rule(doctype_name, auth_value, based_on, add_cond, company)
@@ -148,6 +156,7 @@ class AuthorizationControl(TransactionBase):
"Average Discount",
"Customerwise Discount",
"Itemwise Discount",
+ "Item Group wise Discount",
]
# Check for authorization set for individual user
@@ -166,7 +175,7 @@ class AuthorizationControl(TransactionBase):
# Remove user specific rules from global authorization rules
for r in based_on:
- if r in final_based_on and r != "Itemwise Discount":
+ if r in final_based_on and not r in ["Itemwise Discount", "Item Group wise Discount"]:
final_based_on.remove(r)
# Check for authorization set on particular roles
@@ -194,7 +203,7 @@ class AuthorizationControl(TransactionBase):
# Remove role specific rules from global authorization rules
for r in based_on:
- if r in final_based_on and r != "Itemwise Discount":
+ if r in final_based_on and not r in ["Itemwise Discount", "Item Group wise Discount"]:
final_based_on.remove(r)
# Check for global authorization
diff --git a/erpnext/setup/doctype/authorization_rule/authorization_rule.js b/erpnext/setup/doctype/authorization_rule/authorization_rule.js
index 3f6afcae7f5..f00ed3ecd0d 100644
--- a/erpnext/setup/doctype/authorization_rule/authorization_rule.js
+++ b/erpnext/setup/doctype/authorization_rule/authorization_rule.js
@@ -12,6 +12,9 @@ frappe.ui.form.on("Authorization Rule", {
} else if(frm.doc.based_on==="Itemwise Discount") {
unhide_field("master_name");
frm.set_value("customer_or_item", "Item");
+ } else if(frm.doc.based_on==="Item Group wise Discount") {
+ unhide_field("master_name");
+ frm.set_value("customer_or_item", "Item Group");
} else {
frm.set_value("customer_or_item", "");
frm.set_value("master_name", "");
@@ -81,6 +84,13 @@ cur_frm.fields_dict['master_name'].get_query = function(doc) {
doctype: "Item",
query: "erpnext.controllers.queries.item_query"
}
+ else if (doc.based_on==="Item Group wise Discount")
+ return {
+ doctype: "Item Group",
+ filters: {
+ "is_group": 0
+ }
+ }
else
return {
filters: [
diff --git a/erpnext/setup/doctype/authorization_rule/authorization_rule.json b/erpnext/setup/doctype/authorization_rule/authorization_rule.json
index d3b8887c37b..d750c7bb182 100644
--- a/erpnext/setup/doctype/authorization_rule/authorization_rule.json
+++ b/erpnext/setup/doctype/authorization_rule/authorization_rule.json
@@ -46,7 +46,7 @@
"label": "Based On",
"oldfieldname": "based_on",
"oldfieldtype": "Select",
- "options": "\nGrand Total\nAverage Discount\nCustomerwise Discount\nItemwise Discount\nNot Applicable",
+ "options": "\nGrand Total\nAverage Discount\nCustomerwise Discount\nItemwise Discount\nItem Group wise Discount\nNot Applicable",
"reqd": 1
},
{
@@ -54,14 +54,14 @@
"fieldtype": "Select",
"hidden": 1,
"label": "Customer or Item",
- "options": "Customer\nItem",
+ "options": "Customer\nItem\nItem Group",
"read_only": 1
},
{
"fieldname": "master_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
- "label": "Customer / Item Name",
+ "label": "Customer / Item / Item Group",
"oldfieldname": "master_name",
"oldfieldtype": "Link",
"options": "customer_or_item"
@@ -162,7 +162,7 @@
"icon": "fa fa-shield",
"idx": 1,
"links": [],
- "modified": "2022-07-01 11:19:45.643991",
+ "modified": "2023-09-11 10:29:02.863193",
"modified_by": "Administrator",
"module": "Setup",
"name": "Authorization Rule",
diff --git a/erpnext/setup/doctype/authorization_rule/authorization_rule.py b/erpnext/setup/doctype/authorization_rule/authorization_rule.py
index 44bd826fc6e..9e64e55ff46 100644
--- a/erpnext/setup/doctype/authorization_rule/authorization_rule.py
+++ b/erpnext/setup/doctype/authorization_rule/authorization_rule.py
@@ -47,6 +47,7 @@ class AuthorizationRule(Document):
"Average Discount",
"Customerwise Discount",
"Itemwise Discount",
+ "Item Group wise Discount",
]:
frappe.throw(
_("Cannot set authorization on basis of Discount for {0}").format(self.transaction)
@@ -55,8 +56,6 @@ class AuthorizationRule(Document):
frappe.throw(_("Discount must be less than 100"))
elif self.based_on == "Customerwise Discount" and not self.master_name:
frappe.throw(_("Customer required for 'Customerwise Discount'"))
- else:
- self.based_on = "Not Applicable"
def validate(self):
self.check_duplicate_entry()
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index fa207ecdd1c..4973dab505e 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -200,8 +200,8 @@ erpnext.company.setup_queries = function(frm) {
$.each([
["default_bank_account", {"account_type": "Bank"}],
["default_cash_account", {"account_type": "Cash"}],
- ["default_receivable_account", {"account_type": "Receivable"}],
- ["default_payable_account", {"account_type": "Payable"}],
+ ["default_receivable_account", { "root_type": "Asset", "account_type": "Receivable" }],
+ ["default_payable_account", { "root_type": "Liability", "account_type": "Payable" }],
["default_expense_account", {"root_type": "Expense"}],
["default_income_account", {"root_type": "Income"}],
["round_off_account", {"root_type": "Expense"}],
diff --git a/erpnext/setup/doctype/customer_group/customer_group.js b/erpnext/setup/doctype/customer_group/customer_group.js
index 49a90f959d0..3c81b0283ca 100644
--- a/erpnext/setup/doctype/customer_group/customer_group.js
+++ b/erpnext/setup/doctype/customer_group/customer_group.js
@@ -30,6 +30,7 @@ frappe.ui.form.on("Customer Group", {
frm.set_query('account', 'accounts', function (doc, cdt, cdn) {
return {
filters: {
+ 'root_type': 'Asset',
"account_type": 'Receivable',
"company": locals[cdt][cdn].company,
"is_group": 0
diff --git a/erpnext/setup/doctype/supplier_group/supplier_group.js b/erpnext/setup/doctype/supplier_group/supplier_group.js
index b2acfd73559..33629297ffd 100644
--- a/erpnext/setup/doctype/supplier_group/supplier_group.js
+++ b/erpnext/setup/doctype/supplier_group/supplier_group.js
@@ -30,6 +30,7 @@ frappe.ui.form.on("Supplier Group", {
frm.set_query('account', 'accounts', function (doc, cdt, cdn) {
return {
filters: {
+ 'root_type': 'Liability',
'account_type': 'Payable',
'company': locals[cdt][cdn].company,
"is_group": 0
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index 6a9e2414445..e0d49192eb1 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -1253,6 +1253,7 @@
"depends_on": "eval: doc.is_internal_customer",
"fieldname": "set_target_warehouse",
"fieldtype": "Link",
+ "ignore_user_permissions": 1,
"in_standard_filter": 1,
"label": "Set Target Warehouse",
"no_copy": 1,
@@ -1400,7 +1401,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2023-06-16 14:58:55.066602",
+ "modified": "2023-09-04 14:15:28.363184",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py
index e66c23324da..d4a574da73f 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py
@@ -11,10 +11,12 @@ def get_data():
},
"internal_links": {
"Sales Order": ["items", "against_sales_order"],
- "Sales Invoice": ["items", "against_sales_invoice"],
"Material Request": ["items", "material_request"],
"Purchase Order": ["items", "purchase_order"],
},
+ "internal_and_external_links": {
+ "Sales Invoice": ["items", "against_sales_invoice"],
+ },
"transactions": [
{"label": _("Related"), "items": ["Sales Invoice", "Packing Slip", "Delivery Trip"]},
{"label": _("Reference"), "items": ["Sales Order", "Shipment", "Quality Inspection"]},
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 60aefddf4c8..04eff54c43f 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -603,7 +603,7 @@ class PurchaseReceipt(BuyingController):
account=provisional_account,
cost_center=item.cost_center,
debit=0.0,
- credit=multiplication_factor * item.amount,
+ credit=multiplication_factor * item.base_amount,
remarks=remarks,
against_account=expense_account,
account_currency=credit_currency,
@@ -617,7 +617,7 @@ class PurchaseReceipt(BuyingController):
gl_entries=gl_entries,
account=expense_account,
cost_center=item.cost_center,
- debit=multiplication_factor * item.amount,
+ debit=multiplication_factor * item.base_amount,
credit=0.0,
remarks=remarks,
against_account=provisional_account,
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 6134bfa1f23..b7712ee5ce2 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -2017,6 +2017,49 @@ class TestPurchaseReceipt(FrappeTestCase):
ste7.reload()
self.assertEqual(ste7.items[0].valuation_rate, valuation_rate)
+ def test_purchase_receipt_provisional_accounting(self):
+ # Step - 1: Create Supplier with Default Currency as USD
+ from erpnext.buying.doctype.supplier.test_supplier import create_supplier
+
+ supplier = create_supplier(default_currency="USD")
+
+ # Step - 2: Setup Company for Provisional Accounting
+ from erpnext.accounts.doctype.account.test_account import create_account
+
+ provisional_account = create_account(
+ account_name="Provision Account",
+ parent_account="Current Liabilities - _TC",
+ company="_Test Company",
+ )
+ company = frappe.get_doc("Company", "_Test Company")
+ company.enable_provisional_accounting_for_non_stock_items = 1
+ company.default_provisional_account = provisional_account
+ company.save()
+
+ # Step - 3: Create Non-Stock Item
+ item = make_item(properties={"is_stock_item": 0})
+
+ # Step - 4: Create Purchase Receipt
+ pr = make_purchase_receipt(
+ qty=2,
+ item_code=item.name,
+ company=company.name,
+ supplier=supplier.name,
+ currency=supplier.default_currency,
+ )
+
+ # Test - 1: Total and Base Total should not be the same as the currency is different
+ self.assertNotEqual(flt(pr.total, 2), flt(pr.base_total, 2))
+ self.assertEqual(flt(pr.total * pr.conversion_rate, 2), flt(pr.base_total, 2))
+
+ # Test - 2: Sum of Debit or Credit should be equal to Purchase Receipt Base Total
+ amount = frappe.db.get_value("GL Entry", {"docstatus": 1, "voucher_no": pr.name}, ["sum(debit)"])
+ expected_amount = pr.base_total
+ self.assertEqual(amount, expected_amount)
+
+ company.enable_provisional_accounting_for_non_stock_items = 0
+ company.save()
+
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier