diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index d28c3a8687..145118957b 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -94,7 +94,7 @@ class JournalEntry(AccountsController):
unlink_ref_doc_from_payment_entries(self)
unlink_ref_doc_from_salary_slip(self.name)
- self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
+ self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
self.make_gl_entries(1)
self.update_advance_paid()
self.update_expense_claim()
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index a3a7be2958..a10a810d1d 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -95,7 +95,7 @@ class PaymentEntry(AccountsController):
self.set_status()
def on_cancel(self):
- self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
+ self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
self.make_gl_entries(cancel=1)
self.update_expense_claim()
self.update_outstanding_amounts()
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/__init__.py b/erpnext/accounts/doctype/payment_ledger_entry/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.js b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.js
new file mode 100644
index 0000000000..5a7be8e5ab
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Payment Ledger Entry', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json
new file mode 100644
index 0000000000..d96107678f
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json
@@ -0,0 +1,180 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "format:PLE-{YY}-{MM}-{######}",
+ "creation": "2022-05-09 19:35:03.334361",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "posting_date",
+ "company",
+ "account_type",
+ "account",
+ "party_type",
+ "party",
+ "due_date",
+ "cost_center",
+ "finance_book",
+ "voucher_type",
+ "voucher_no",
+ "against_voucher_type",
+ "against_voucher_no",
+ "amount",
+ "account_currency",
+ "amount_in_account_currency",
+ "delinked"
+ ],
+ "fields": [
+ {
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "label": "Posting Date"
+ },
+ {
+ "fieldname": "account_type",
+ "fieldtype": "Select",
+ "label": "Account Type",
+ "options": "Receivable\nPayable"
+ },
+ {
+ "fieldname": "account",
+ "fieldtype": "Link",
+ "label": "Account",
+ "options": "Account"
+ },
+ {
+ "fieldname": "party_type",
+ "fieldtype": "Link",
+ "label": "Party Type",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "party",
+ "fieldtype": "Dynamic Link",
+ "label": "Party",
+ "options": "party_type"
+ },
+ {
+ "fieldname": "voucher_type",
+ "fieldtype": "Link",
+ "in_standard_filter": 1,
+ "label": "Voucher Type",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Voucher No",
+ "options": "voucher_type"
+ },
+ {
+ "fieldname": "against_voucher_type",
+ "fieldtype": "Link",
+ "in_standard_filter": 1,
+ "label": "Against Voucher Type",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "against_voucher_no",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Against Voucher No",
+ "options": "against_voucher_type"
+ },
+ {
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Amount",
+ "options": "Company:company:default_currency"
+ },
+ {
+ "fieldname": "account_currency",
+ "fieldtype": "Link",
+ "label": "Currency",
+ "options": "Currency"
+ },
+ {
+ "fieldname": "amount_in_account_currency",
+ "fieldtype": "Currency",
+ "label": "Amount in Account Currency",
+ "options": "account_currency"
+ },
+ {
+ "default": "0",
+ "fieldname": "delinked",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "DeLinked"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ },
+ {
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "options": "Cost Center"
+ },
+ {
+ "fieldname": "due_date",
+ "fieldtype": "Date",
+ "label": "Due Date"
+ },
+ {
+ "fieldname": "finance_book",
+ "fieldtype": "Link",
+ "label": "Finance Book",
+ "options": "Finance Book"
+ }
+ ],
+ "in_create": 1,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2022-05-19 18:04:44.609115",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Payment Ledger Entry",
+ "naming_rule": "Expression",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Auditor",
+ "share": 1
+ }
+ ],
+ "search_fields": "voucher_no, against_voucher_no",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py
new file mode 100644
index 0000000000..43e19f4ae7
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py
@@ -0,0 +1,22 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+
+
+class PaymentLedgerEntry(Document):
+ def validate_account(self):
+ valid_account = frappe.db.get_list(
+ "Account",
+ "name",
+ filters={"name": self.account, "account_type": self.account_type, "company": self.company},
+ ignore_permissions=True,
+ )
+ if not valid_account:
+ frappe.throw(_("{0} account is not of type {1}").format(self.account, self.account_type))
+
+ def validate(self):
+ self.validate_account()
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py
new file mode 100644
index 0000000000..a71b19e092
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py
@@ -0,0 +1,408 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import frappe
+from frappe import qb
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import nowdate
+
+from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+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.stock.doctype.item.test_item import create_item
+
+
+class TestPaymentLedgerEntry(FrappeTestCase):
+ def setUp(self):
+ self.ple = qb.DocType("Payment Ledger Entry")
+ self.create_company()
+ self.create_item()
+ self.create_customer()
+ self.clear_old_entries()
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def create_company(self):
+ company_name = "_Test Payment Ledger"
+ company = None
+ if frappe.db.exists("Company", company_name):
+ company = frappe.get_doc("Company", company_name)
+ else:
+ company = frappe.get_doc(
+ {
+ "doctype": "Company",
+ "company_name": company_name,
+ "country": "India",
+ "default_currency": "INR",
+ "create_chart_of_accounts_based_on": "Standard Template",
+ "chart_of_accounts": "Standard",
+ }
+ )
+ company = company.save()
+
+ self.company = company.name
+ self.cost_center = company.cost_center
+ self.warehouse = "All Warehouses - _PL"
+ self.income_account = "Sales - _PL"
+ self.expense_account = "Cost of Goods Sold - _PL"
+ self.debit_to = "Debtors - _PL"
+ self.creditors = "Creditors - _PL"
+
+ # create bank account
+ if frappe.db.exists("Account", "HDFC - _PL"):
+ self.bank = "HDFC - _PL"
+ else:
+ bank_acc = frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": "HDFC",
+ "parent_account": "Bank Accounts - _PL",
+ "company": self.company,
+ }
+ )
+ bank_acc.save()
+ self.bank = bank_acc.name
+
+ def create_item(self):
+ item_name = "_Test PL Item"
+ item = create_item(
+ item_code=item_name, is_stock_item=0, company=self.company, warehouse=self.warehouse
+ )
+ self.item = item if isinstance(item, str) else item.item_code
+
+ def create_customer(self):
+ name = "_Test PL Customer"
+ if frappe.db.exists("Customer", name):
+ self.customer = name
+ else:
+ customer = frappe.new_doc("Customer")
+ customer.customer_name = name
+ customer.type = "Individual"
+ customer.save()
+ self.customer = customer.name
+
+ def create_sales_invoice(
+ self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
+ ):
+ """
+ Helper function to populate default values in sales invoice
+ """
+ sinv = create_sales_invoice(
+ qty=qty,
+ rate=rate,
+ company=self.company,
+ customer=self.customer,
+ item_code=self.item,
+ item_name=self.item,
+ cost_center=self.cost_center,
+ warehouse=self.warehouse,
+ debit_to=self.debit_to,
+ parent_cost_center=self.cost_center,
+ update_stock=0,
+ currency="INR",
+ is_pos=0,
+ is_return=0,
+ return_against=None,
+ income_account=self.income_account,
+ expense_account=self.expense_account,
+ do_not_save=do_not_save,
+ do_not_submit=do_not_submit,
+ )
+ return sinv
+
+ def create_payment_entry(self, amount=100, posting_date=nowdate()):
+ """
+ Helper function to populate default values in payment entry
+ """
+ payment = create_payment_entry(
+ company=self.company,
+ payment_type="Receive",
+ party_type="Customer",
+ party=self.customer,
+ paid_from=self.debit_to,
+ paid_to=self.bank,
+ paid_amount=amount,
+ )
+ payment.posting_date = posting_date
+ return payment
+
+ def clear_old_entries(self):
+ doctype_list = [
+ "GL Entry",
+ "Payment Ledger Entry",
+ "Sales Invoice",
+ "Purchase Invoice",
+ "Payment Entry",
+ "Journal Entry",
+ ]
+ for doctype in doctype_list:
+ qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
+
+ def create_journal_entry(
+ self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None
+ ):
+ je = frappe.new_doc("Journal Entry")
+ je.posting_date = posting_date or nowdate()
+ je.company = self.company
+ je.user_remark = "test"
+ if not cost_center:
+ cost_center = self.cost_center
+ je.set(
+ "accounts",
+ [
+ {
+ "account": acc1,
+ "cost_center": cost_center,
+ "debit_in_account_currency": amount if amount > 0 else 0,
+ "credit_in_account_currency": abs(amount) if amount < 0 else 0,
+ },
+ {
+ "account": acc2,
+ "cost_center": cost_center,
+ "credit_in_account_currency": amount if amount > 0 else 0,
+ "debit_in_account_currency": abs(amount) if amount < 0 else 0,
+ },
+ ],
+ )
+ return je
+
+ def test_payment_against_invoice(self):
+ transaction_date = nowdate()
+ amount = 100
+ ple = self.ple
+
+ # full payment using PE
+ si1 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
+ pe1 = get_payment_entry(si1.doctype, si1.name).save().submit()
+
+ pl_entries = (
+ qb.from_(ple)
+ .select(
+ ple.voucher_type,
+ ple.voucher_no,
+ ple.against_voucher_type,
+ ple.against_voucher_no,
+ ple.amount,
+ ple.delinked,
+ )
+ .where((ple.against_voucher_type == si1.doctype) & (ple.against_voucher_no == si1.name))
+ .orderby(ple.creation)
+ .run(as_dict=True)
+ )
+
+ expected_values = [
+ {
+ "voucher_type": si1.doctype,
+ "voucher_no": si1.name,
+ "against_voucher_type": si1.doctype,
+ "against_voucher_no": si1.name,
+ "amount": amount,
+ "delinked": 0,
+ },
+ {
+ "voucher_type": pe1.doctype,
+ "voucher_no": pe1.name,
+ "against_voucher_type": si1.doctype,
+ "against_voucher_no": si1.name,
+ "amount": -amount,
+ "delinked": 0,
+ },
+ ]
+ self.assertEqual(pl_entries[0], expected_values[0])
+ self.assertEqual(pl_entries[1], expected_values[1])
+
+ def test_partial_payment_against_invoice(self):
+ ple = self.ple
+ transaction_date = nowdate()
+ amount = 100
+
+ # partial payment of invoice using PE
+ si2 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
+ pe2 = get_payment_entry(si2.doctype, si2.name)
+ pe2.get("references")[0].allocated_amount = 50
+ pe2.get("references")[0].outstanding_amount = 50
+ pe2 = pe2.save().submit()
+
+ pl_entries = (
+ qb.from_(ple)
+ .select(
+ ple.voucher_type,
+ ple.voucher_no,
+ ple.against_voucher_type,
+ ple.against_voucher_no,
+ ple.amount,
+ ple.delinked,
+ )
+ .where((ple.against_voucher_type == si2.doctype) & (ple.against_voucher_no == si2.name))
+ .orderby(ple.creation)
+ .run(as_dict=True)
+ )
+
+ expected_values = [
+ {
+ "voucher_type": si2.doctype,
+ "voucher_no": si2.name,
+ "against_voucher_type": si2.doctype,
+ "against_voucher_no": si2.name,
+ "amount": amount,
+ "delinked": 0,
+ },
+ {
+ "voucher_type": pe2.doctype,
+ "voucher_no": pe2.name,
+ "against_voucher_type": si2.doctype,
+ "against_voucher_no": si2.name,
+ "amount": -50,
+ "delinked": 0,
+ },
+ ]
+ self.assertEqual(pl_entries[0], expected_values[0])
+ self.assertEqual(pl_entries[1], expected_values[1])
+
+ def test_cr_note_against_invoice(self):
+ ple = self.ple
+ transaction_date = nowdate()
+ amount = 100
+
+ # reconcile against return invoice
+ si3 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
+ cr_note1 = self.create_sales_invoice(
+ qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
+ )
+ cr_note1.is_return = 1
+ cr_note1.return_against = si3.name
+ cr_note1 = cr_note1.save().submit()
+
+ pl_entries = (
+ qb.from_(ple)
+ .select(
+ ple.voucher_type,
+ ple.voucher_no,
+ ple.against_voucher_type,
+ ple.against_voucher_no,
+ ple.amount,
+ ple.delinked,
+ )
+ .where((ple.against_voucher_type == si3.doctype) & (ple.against_voucher_no == si3.name))
+ .orderby(ple.creation)
+ .run(as_dict=True)
+ )
+
+ expected_values = [
+ {
+ "voucher_type": si3.doctype,
+ "voucher_no": si3.name,
+ "against_voucher_type": si3.doctype,
+ "against_voucher_no": si3.name,
+ "amount": amount,
+ "delinked": 0,
+ },
+ {
+ "voucher_type": cr_note1.doctype,
+ "voucher_no": cr_note1.name,
+ "against_voucher_type": si3.doctype,
+ "against_voucher_no": si3.name,
+ "amount": -amount,
+ "delinked": 0,
+ },
+ ]
+ self.assertEqual(pl_entries[0], expected_values[0])
+ self.assertEqual(pl_entries[1], expected_values[1])
+
+ def test_je_against_inv_and_note(self):
+ ple = self.ple
+ transaction_date = nowdate()
+ amount = 100
+
+ # reconcile against return invoice using JE
+ si4 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
+ cr_note2 = self.create_sales_invoice(
+ qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
+ )
+ cr_note2.is_return = 1
+ cr_note2 = cr_note2.save().submit()
+ je1 = self.create_journal_entry(
+ self.debit_to, self.debit_to, amount, posting_date=transaction_date
+ )
+ je1.get("accounts")[0].party_type = je1.get("accounts")[1].party_type = "Customer"
+ je1.get("accounts")[0].party = je1.get("accounts")[1].party = self.customer
+ je1.get("accounts")[0].reference_type = cr_note2.doctype
+ je1.get("accounts")[0].reference_name = cr_note2.name
+ je1.get("accounts")[1].reference_type = si4.doctype
+ je1.get("accounts")[1].reference_name = si4.name
+ je1 = je1.save().submit()
+
+ pl_entries_for_invoice = (
+ qb.from_(ple)
+ .select(
+ ple.voucher_type,
+ ple.voucher_no,
+ ple.against_voucher_type,
+ ple.against_voucher_no,
+ ple.amount,
+ ple.delinked,
+ )
+ .where((ple.against_voucher_type == si4.doctype) & (ple.against_voucher_no == si4.name))
+ .orderby(ple.creation)
+ .run(as_dict=True)
+ )
+
+ expected_values = [
+ {
+ "voucher_type": si4.doctype,
+ "voucher_no": si4.name,
+ "against_voucher_type": si4.doctype,
+ "against_voucher_no": si4.name,
+ "amount": amount,
+ "delinked": 0,
+ },
+ {
+ "voucher_type": je1.doctype,
+ "voucher_no": je1.name,
+ "against_voucher_type": si4.doctype,
+ "against_voucher_no": si4.name,
+ "amount": -amount,
+ "delinked": 0,
+ },
+ ]
+ self.assertEqual(pl_entries_for_invoice[0], expected_values[0])
+ self.assertEqual(pl_entries_for_invoice[1], expected_values[1])
+
+ pl_entries_for_crnote = (
+ qb.from_(ple)
+ .select(
+ ple.voucher_type,
+ ple.voucher_no,
+ ple.against_voucher_type,
+ ple.against_voucher_no,
+ ple.amount,
+ ple.delinked,
+ )
+ .where(
+ (ple.against_voucher_type == cr_note2.doctype) & (ple.against_voucher_no == cr_note2.name)
+ )
+ .orderby(ple.creation)
+ .run(as_dict=True)
+ )
+
+ expected_values = [
+ {
+ "voucher_type": cr_note2.doctype,
+ "voucher_no": cr_note2.name,
+ "against_voucher_type": cr_note2.doctype,
+ "against_voucher_no": cr_note2.name,
+ "amount": -amount,
+ "delinked": 0,
+ },
+ {
+ "voucher_type": je1.doctype,
+ "voucher_no": je1.name,
+ "against_voucher_type": cr_note2.doctype,
+ "against_voucher_no": cr_note2.name,
+ "amount": amount,
+ "delinked": 0,
+ },
+ ]
+ self.assertEqual(pl_entries_for_crnote[0], expected_values[0])
+ self.assertEqual(pl_entries_for_crnote[1], expected_values[1])
diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
index 8e0e62d5f8..3b938ea1ca 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
+++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
@@ -78,6 +78,8 @@ class TestPeriodClosingVoucher(unittest.TestCase):
expense_account="Cost of Goods Sold - TPC",
rate=400,
debit_to="Debtors - TPC",
+ currency="USD",
+ customer="_Test Customer USD",
)
create_sales_invoice(
company=company,
@@ -86,6 +88,8 @@ class TestPeriodClosingVoucher(unittest.TestCase):
expense_account="Cost of Goods Sold - TPC",
rate=200,
debit_to="Debtors - TPC",
+ currency="USD",
+ customer="_Test Customer USD",
)
pcv = self.make_period_closing_voucher(submit=False)
@@ -119,14 +123,17 @@ class TestPeriodClosingVoucher(unittest.TestCase):
surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1")
- create_sales_invoice(
+ si = create_sales_invoice(
company=company,
income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC",
cost_center=cost_center,
rate=400,
debit_to="Debtors - TPC",
+ currency="USD",
+ customer="_Test Customer USD",
)
+
jv = make_journal_entry(
account1="Cash - TPC",
account2="Sales - TPC",
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
index 572410fc66..98f3420d87 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
@@ -102,7 +102,9 @@ frappe.ui.form.on('POS Closing Entry', {
});
},
- before_save: function(frm) {
+ before_save: async function(frm) {
+ frappe.dom.freeze(__('Processing Sales! Please Wait...'));
+
frm.set_value("grand_total", 0);
frm.set_value("net_total", 0);
frm.set_value("total_quantity", 0);
@@ -112,17 +114,23 @@ frappe.ui.form.on('POS Closing Entry', {
row.expected_amount = row.opening_amount;
}
- for (let row of frm.doc.pos_transactions) {
- frappe.db.get_doc("POS Invoice", row.pos_invoice).then(doc => {
- frm.doc.grand_total += flt(doc.grand_total);
- frm.doc.net_total += flt(doc.net_total);
- frm.doc.total_quantity += flt(doc.total_qty);
- refresh_payments(doc, frm);
- refresh_taxes(doc, frm);
- refresh_fields(frm);
- set_html_data(frm);
- });
+ const pos_inv_promises = frm.doc.pos_transactions.map(
+ row => frappe.db.get_doc("POS Invoice", row.pos_invoice)
+ );
+
+ const pos_invoices = await Promise.all(pos_inv_promises);
+
+ for (let doc of pos_invoices) {
+ frm.doc.grand_total += flt(doc.grand_total);
+ frm.doc.net_total += flt(doc.net_total);
+ frm.doc.total_quantity += flt(doc.total_qty);
+ refresh_payments(doc, frm);
+ refresh_taxes(doc, frm);
+ refresh_fields(frm);
+ set_html_data(frm);
}
+
+ frappe.dom.unfreeze();
}
});
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 94246e135b..9649f80dee 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -96,6 +96,7 @@ class POSInvoice(SalesInvoice):
)
def on_cancel(self):
+ self.ignore_linked_doctypes = "Payment Ledger Entry"
# run on cancel method of selling controller
super(SalesInvoice, self).on_cancel()
if not self.is_return and self.loyalty_program:
diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
index 4cf19b4454..3bd0cd2e83 100644
--- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
@@ -752,7 +752,7 @@ class TestPricingRule(unittest.TestCase):
title="_Test Pricing Rule with Min Qty - 2",
)
- si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1, currency="USD")
+ si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1)
item = si.items[0]
item.stock_qty = 1
si.save()
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index a1d86e2219..e6da6669ac 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -1418,7 +1418,12 @@ class PurchaseInvoice(BuyingController):
frappe.db.set(self, "status", "Cancelled")
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
- self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
+ self.ignore_linked_doctypes = (
+ "GL Entry",
+ "Stock Ledger Entry",
+ "Repost Item Valuation",
+ "Payment Ledger Entry",
+ )
self.update_advance_tax_references(cancel=1)
def update_project(self):
diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json
index b46d2e32f2..c36efb89a3 100644
--- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json
+++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json
@@ -1,10 +1,12 @@
{
+ "actions": [],
"allow_import": 1,
"allow_rename": 1,
"creation": "2013-01-10 16:34:08",
"description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Consider Tax or Charge for: In this section you can specify if the tax / charge is only for valuation (not a part of total) or only for total (does not add value to the item) or for both.\n10. Add or Deduct: Whether you want to add or deduct the tax.",
"doctype": "DocType",
"document_type": "Setup",
+ "engine": "InnoDB",
"field_order": [
"title",
"is_default",
@@ -74,7 +76,8 @@
],
"icon": "fa fa-money",
"idx": 1,
- "modified": "2019-11-25 13:05:26.220275",
+ "links": [],
+ "modified": "2022-05-16 16:15:29.059370",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges Template",
@@ -103,6 +106,10 @@
"role": "Purchase User"
}
],
+ "show_title_field_in_link": 1,
+ "sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
+ "title_field": "title",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index e30289a62c..9dde85fe12 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -861,27 +861,44 @@ frappe.ui.form.on('Sales Invoice', {
set_timesheet_data: function(frm, timesheets) {
frm.clear_table("timesheets")
- timesheets.forEach(timesheet => {
+ timesheets.forEach(async (timesheet) => {
if (frm.doc.currency != timesheet.currency) {
- frappe.call({
- method: "erpnext.setup.utils.get_exchange_rate",
- args: {
- from_currency: timesheet.currency,
- to_currency: frm.doc.currency
- },
- callback: function(r) {
- if (r.message) {
- exchange_rate = r.message;
- frm.events.append_time_log(frm, timesheet, exchange_rate);
- }
- }
- });
+ const exchange_rate = await frm.events.get_exchange_rate(
+ frm, timesheet.currency, frm.doc.currency
+ )
+ frm.events.append_time_log(frm, timesheet, exchange_rate)
} else {
frm.events.append_time_log(frm, timesheet, 1.0);
}
});
},
+ async get_exchange_rate(frm, from_currency, to_currency) {
+ if (
+ frm.exchange_rates
+ && frm.exchange_rates[from_currency]
+ && frm.exchange_rates[from_currency][to_currency]
+ ) {
+ return frm.exchange_rates[from_currency][to_currency];
+ }
+
+ return frappe.call({
+ method: "erpnext.setup.utils.get_exchange_rate",
+ args: {
+ from_currency,
+ to_currency
+ },
+ callback: function(r) {
+ if (r.message) {
+ // cache exchange rates
+ frm.exchange_rates = frm.exchange_rates || {};
+ frm.exchange_rates[from_currency] = frm.exchange_rates[from_currency] || {};
+ frm.exchange_rates[from_currency][to_currency] = r.message;
+ }
+ }
+ });
+ },
+
append_time_log: function(frm, time_log, exchange_rate) {
const row = frm.add_child("timesheets");
row.activity_type = time_log.activity_type;
@@ -892,7 +909,7 @@ frappe.ui.form.on('Sales Invoice', {
row.billing_hours = time_log.billing_hours;
row.billing_amount = flt(time_log.billing_amount) * flt(exchange_rate);
row.timesheet_detail = time_log.name;
- row.project_name = time_log.project_name;
+ row.project_name = time_log.project_name;
frm.refresh_field("timesheets");
frm.trigger("calculate_timesheet_totals");
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index f0880c19e3..a580d45acc 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -396,7 +396,12 @@ class SalesInvoice(SellingController):
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.unlink_sales_invoice_from_timesheets()
- self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
+ self.ignore_linked_doctypes = (
+ "GL Entry",
+ "Stock Ledger Entry",
+ "Repost Item Valuation",
+ "Payment Ledger Entry",
+ )
def update_status_updater_args(self):
if cint(self.update_stock):
diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json
index 19781bdffa..408ecbf36d 100644
--- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json
+++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"allow_rename": 1,
"creation": "2013-01-10 16:34:09",
@@ -77,7 +78,8 @@
],
"icon": "fa fa-money",
"idx": 1,
- "modified": "2019-11-25 13:06:03.279099",
+ "links": [],
+ "modified": "2022-05-16 16:14:52.061672",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges Template",
@@ -113,7 +115,10 @@
"write": 1
}
],
+ "show_title_field_in_link": 1,
"sort_field": "modified",
"sort_order": "ASC",
+ "states": [],
+ "title_field": "title",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 1598d914e2..b0513f16a5 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -14,6 +14,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
+from erpnext.accounts.utils import create_payment_ledger_entry
class ClosedAccountingPeriod(frappe.ValidationError):
@@ -34,6 +35,7 @@ def make_gl_entries(
validate_disabled_accounts(gl_map)
gl_map = process_gl_map(gl_map, merge_entries)
if gl_map and len(gl_map) > 1:
+ create_payment_ledger_entry(gl_map)
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
# Post GL Map proccess there may no be any GL Entries
elif gl_map:
@@ -479,6 +481,7 @@ def make_reverse_gl_entries(
).run(as_dict=1)
if gl_entries:
+ create_payment_ledger_entry(gl_entries, cancel=1)
validate_accounting_period(gl_entries)
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index db741d97e1..f4a44bd362 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -897,3 +897,18 @@ def get_default_contact(doctype, name):
return None
else:
return None
+
+
+def add_party_account(party_type, party, company, account):
+ doc = frappe.get_doc(party_type, party)
+ account_exists = False
+ for d in doc.get("accounts"):
+ if d.account == account:
+ account_exists = True
+
+ if not account_exists:
+ accounts = {"company": company, "account": account}
+
+ doc.append("accounts", accounts)
+
+ doc.save()
diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py
index f3ccc868c4..c41d0d10ff 100644
--- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py
+++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py
@@ -198,10 +198,12 @@ def get_loan_entries(filters):
amount_field = (loan_doc.disbursed_amount).as_("credit")
posting_date = (loan_doc.disbursement_date).as_("posting_date")
account = loan_doc.disbursement_account
+ salary_condition = loan_doc.docstatus == 1
else:
amount_field = (loan_doc.amount_paid).as_("debit")
posting_date = (loan_doc.posting_date).as_("posting_date")
account = loan_doc.payment_account
+ salary_condition = loan_doc.repay_from_salary == 0
query = (
frappe.qb.from_(loan_doc)
@@ -214,14 +216,12 @@ def get_loan_entries(filters):
posting_date,
)
.where(loan_doc.docstatus == 1)
+ .where(salary_condition)
.where(account == filters.get("account"))
.where(posting_date <= getdate(filters.get("report_date")))
.where(ifnull(loan_doc.clearance_date, "4000-01-01") > getdate(filters.get("report_date")))
)
- if doctype == "Loan Repayment":
- query.where(loan_doc.repay_from_salary == 0)
-
entries = query.run(as_dict=1)
loan_docs.extend(entries)
@@ -267,15 +267,17 @@ def get_loan_amount(filters):
amount_field = Sum(loan_doc.disbursed_amount)
posting_date = (loan_doc.disbursement_date).as_("posting_date")
account = loan_doc.disbursement_account
+ salary_condition = loan_doc.docstatus == 1
else:
amount_field = Sum(loan_doc.amount_paid)
posting_date = (loan_doc.posting_date).as_("posting_date")
account = loan_doc.payment_account
-
+ salary_condition = loan_doc.repay_from_salary == 0
amount = (
frappe.qb.from_(loan_doc)
.select(amount_field)
.where(loan_doc.docstatus == 1)
+ .where(salary_condition)
.where(account == filters.get("account"))
.where(posting_date > getdate(filters.get("report_date")))
.where(ifnull(loan_doc.clearance_date, "4000-01-01") <= getdate(filters.get("report_date")))
diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py
index 74926b90ff..75e983afc0 100644
--- a/erpnext/accounts/report/cash_flow/cash_flow.py
+++ b/erpnext/accounts/report/cash_flow/cash_flow.py
@@ -262,7 +262,10 @@ def get_report_summary(summary_data, currency):
def get_chart_data(columns, data):
labels = [d.get("label") for d in columns[2:]]
datasets = [
- {"name": account.get("account").replace("'", ""), "values": [account.get("total")]}
+ {
+ "name": account.get("account").replace("'", ""),
+ "values": [account.get(d.get("fieldname")) for d in columns[2:]],
+ }
for account in data
if account.get("parent_account") == None and account.get("currency")
]
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 405922e16e..1869cc7b29 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -7,7 +7,7 @@ from typing import List, Tuple
import frappe
import frappe.defaults
-from frappe import _, throw
+from frappe import _, qb, throw
from frappe.model.meta import get_field_precision
from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate
@@ -15,6 +15,7 @@ import erpnext
# imported to enable erpnext.accounts.utils.get_account_currency
from erpnext.accounts.doctype.account.account import get_account_currency # noqa
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.utils import get_stock_value_on
@@ -1345,3 +1346,102 @@ def check_and_delete_linked_reports(report):
if icons:
for icon in icons:
frappe.delete_doc("Desktop Icon", icon)
+
+
+def create_payment_ledger_entry(gl_entries, cancel=0):
+ if gl_entries:
+ ple = None
+
+ # companies
+ account = qb.DocType("Account")
+ companies = list(set([x.company for x in gl_entries]))
+
+ # receivable/payable account
+ accounts_with_types = (
+ qb.from_(account)
+ .select(account.name, account.account_type)
+ .where(
+ (account.account_type.isin(["Receivable", "Payable"]) & (account.company.isin(companies)))
+ )
+ .run(as_dict=True)
+ )
+ receivable_or_payable_accounts = [y.name for y in accounts_with_types]
+
+ def get_account_type(account):
+ for entry in accounts_with_types:
+ if entry.name == account:
+ return entry.account_type
+
+ dr_or_cr = 0
+ account_type = None
+ for gle in gl_entries:
+ if gle.account in receivable_or_payable_accounts:
+ account_type = get_account_type(gle.account)
+ if account_type == "Receivable":
+ dr_or_cr = gle.debit - gle.credit
+ dr_or_cr_account_currency = gle.debit_in_account_currency - gle.credit_in_account_currency
+ elif account_type == "Payable":
+ dr_or_cr = gle.credit - gle.debit
+ dr_or_cr_account_currency = gle.credit_in_account_currency - gle.debit_in_account_currency
+
+ if cancel:
+ dr_or_cr *= -1
+ dr_or_cr_account_currency *= -1
+
+ ple = frappe.get_doc(
+ {
+ "doctype": "Payment Ledger Entry",
+ "posting_date": gle.posting_date,
+ "company": gle.company,
+ "account_type": account_type,
+ "account": gle.account,
+ "party_type": gle.party_type,
+ "party": gle.party,
+ "cost_center": gle.cost_center,
+ "finance_book": gle.finance_book,
+ "due_date": gle.due_date,
+ "voucher_type": gle.voucher_type,
+ "voucher_no": gle.voucher_no,
+ "against_voucher_type": gle.against_voucher_type
+ if gle.against_voucher_type
+ else gle.voucher_type,
+ "against_voucher_no": gle.against_voucher if gle.against_voucher else gle.voucher_no,
+ "currency": gle.currency,
+ "amount": dr_or_cr,
+ "amount_in_account_currency": dr_or_cr_account_currency,
+ "delinked": True if cancel else False,
+ }
+ )
+
+ dimensions_and_defaults = get_dimensions()
+ if dimensions_and_defaults:
+ for dimension in dimensions_and_defaults[0]:
+ ple.set(dimension.fieldname, gle.get(dimension.fieldname))
+
+ if cancel:
+ delink_original_entry(ple)
+ ple.flags.ignore_permissions = 1
+ ple.submit()
+
+
+def delink_original_entry(pl_entry):
+ if pl_entry:
+ ple = qb.DocType("Payment Ledger Entry")
+ query = (
+ qb.update(ple)
+ .set(ple.delinked, True)
+ .set(ple.modified, now())
+ .set(ple.modified_by, frappe.session.user)
+ .where(
+ (ple.company == pl_entry.company)
+ & (ple.account_type == pl_entry.account_type)
+ & (ple.account == pl_entry.account)
+ & (ple.party_type == pl_entry.party_type)
+ & (ple.party == pl_entry.party)
+ & (ple.voucher_type == pl_entry.voucher_type)
+ & (ple.voucher_no == pl_entry.voucher_no)
+ & (ple.against_voucher_type == pl_entry.against_voucher_type)
+ & (ple.against_voucher_no == pl_entry.against_voucher_no)
+ )
+ )
+ query.run()
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 9189f18373..44426ba43d 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -323,6 +323,7 @@ class PurchaseOrder(BuyingController):
update_linked_doc(self.doctype, self.name, self.inter_company_order_reference)
def on_cancel(self):
+ self.ignore_linked_doctypes = "Payment Ledger Entry"
super(PurchaseOrder, self).on_cancel()
if self.is_against_so():
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index c9c2ab17a9..056084b7e8 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -34,6 +34,7 @@ from erpnext.accounts.doctype.pricing_rule.utils import (
from erpnext.accounts.party import (
get_party_account,
get_party_account_currency,
+ get_party_gle_currency,
validate_party_frozen_disabled,
)
from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year
@@ -168,6 +169,7 @@ class AccountsController(TransactionBase):
self.validate_party()
self.validate_currency()
+ self.validate_party_account_currency()
if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
@@ -1447,6 +1449,27 @@ class AccountsController(TransactionBase):
# at quotation / sales order level and we shouldn't stop someone
# from creating a sales invoice if sales order is already created
+ def validate_party_account_currency(self):
+ if self.doctype not in ("Sales Invoice", "Purchase Invoice"):
+ return
+
+ if self.is_opening == "Yes":
+ return
+
+ party_type, party = self.get_party()
+ party_gle_currency = get_party_gle_currency(party_type, party, self.company)
+ party_account = (
+ self.get("debit_to") if self.doctype == "Sales Invoice" else self.get("credit_to")
+ )
+ party_account_currency = get_account_currency(party_account)
+
+ if not party_gle_currency and (party_account_currency != self.currency):
+ frappe.throw(
+ _("Party Account {0} currency and document currency should be same").format(
+ frappe.bold(party_account)
+ )
+ )
+
def delink_advance_entries(self, linked_doc_name):
total_allocated_amount = 0
for adv in self.advances:
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 813ac17ca0..1c4bbbc3fc 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -487,6 +487,7 @@ communication_doctypes = ["Customer", "Supplier"]
accounting_dimension_doctypes = [
"GL Entry",
+ "Payment Ledger Entry",
"Sales Invoice",
"Purchase Invoice",
"Payment Entry",
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py
index 89d86c1bc7..589763c0a9 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.py
@@ -105,7 +105,7 @@ class ExpenseClaim(AccountsController):
def on_cancel(self):
self.update_task_and_project()
- self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
+ self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
if self.payable_account:
self.make_gl_entries(cancel=True)
diff --git a/erpnext/hr/doctype/job_opening/job_opening.py b/erpnext/hr/doctype/job_opening/job_opening.py
index c71407d71d..ce7caa33c6 100644
--- a/erpnext/hr/doctype/job_opening/job_opening.py
+++ b/erpnext/hr/doctype/job_opening/job_opening.py
@@ -6,6 +6,7 @@
import frappe
from frappe import _
+from frappe.utils import get_link_to_form
from frappe.website.website_generator import WebsiteGenerator
from erpnext.hr.doctype.staffing_plan.staffing_plan import (
@@ -33,26 +34,32 @@ class JobOpening(WebsiteGenerator):
self.staffing_plan = staffing_plan[0].name
self.planned_vacancies = staffing_plan[0].vacancies
elif not self.planned_vacancies:
- planned_vacancies = frappe.db.sql(
- """
- select vacancies from `tabStaffing Plan Detail`
- where parent=%s and designation=%s""",
- (self.staffing_plan, self.designation),
+ self.planned_vacancies = frappe.db.get_value(
+ "Staffing Plan Detail",
+ {"parent": self.staffing_plan, "designation": self.designation},
+ "vacancies",
)
- self.planned_vacancies = planned_vacancies[0][0] if planned_vacancies else None
if self.staffing_plan and self.planned_vacancies:
staffing_plan_company = frappe.db.get_value("Staffing Plan", self.staffing_plan, "company")
- lft, rgt = frappe.get_cached_value("Company", staffing_plan_company, ["lft", "rgt"])
- designation_counts = get_designation_counts(self.designation, self.company)
+ designation_counts = get_designation_counts(self.designation, self.company, self.name)
current_count = designation_counts["employee_count"] + designation_counts["job_openings"]
- if self.planned_vacancies <= current_count:
+ number_of_positions = frappe.db.get_value(
+ "Staffing Plan Detail",
+ {"parent": self.staffing_plan, "designation": self.designation},
+ "number_of_positions",
+ )
+
+ if number_of_positions <= current_count:
frappe.throw(
_(
- "Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}"
- ).format(self.designation, self.staffing_plan)
+ "Job Openings for the designation {0} are already open or the hiring is complete as per the Staffing Plan {1}"
+ ).format(
+ frappe.bold(self.designation), get_link_to_form("Staffing Plan", self.staffing_plan)
+ ),
+ title=_("Vacancies fulfilled"),
)
def get_context(self, context):
diff --git a/erpnext/hr/doctype/job_opening/test_job_opening.py b/erpnext/hr/doctype/job_opening/test_job_opening.py
index a72a6eb338..e991054f62 100644
--- a/erpnext/hr/doctype/job_opening/test_job_opening.py
+++ b/erpnext/hr/doctype/job_opening/test_job_opening.py
@@ -3,8 +3,77 @@
import unittest
-# test_records = frappe.get_test_records('Job Opening')
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_days, getdate
+
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.staffing_plan.test_staffing_plan import make_company
-class TestJobOpening(unittest.TestCase):
- pass
+class TestJobOpening(FrappeTestCase):
+ def setUp(self):
+ frappe.db.delete("Staffing Plan")
+ frappe.db.delete("Staffing Plan Detail")
+ frappe.db.delete("Job Opening")
+
+ make_company("_Test Opening Company", "_TOC")
+ frappe.db.delete("Employee", {"company": "_Test Opening Company"})
+
+ def test_vacancies_fulfilled(self):
+ make_employee(
+ "test_job_opening@example.com", company="_Test Opening Company", designation="Designer"
+ )
+
+ staffing_plan = frappe.get_doc(
+ {
+ "doctype": "Staffing Plan",
+ "company": "_Test Opening Company",
+ "name": "Test",
+ "from_date": getdate(),
+ "to_date": add_days(getdate(), 10),
+ }
+ )
+
+ staffing_plan.append(
+ "staffing_details",
+ {"designation": "Designer", "vacancies": 1, "estimated_cost_per_position": 50000},
+ )
+ staffing_plan.insert()
+ staffing_plan.submit()
+
+ self.assertEqual(staffing_plan.staffing_details[0].number_of_positions, 2)
+
+ # allows creating 1 job opening as per vacancy
+ opening_1 = get_job_opening()
+ opening_1.insert()
+
+ # vacancies as per staffing plan already fulfilled via job opening and existing employee count
+ opening_2 = get_job_opening(job_title="Designer New")
+ self.assertRaises(frappe.ValidationError, opening_2.insert)
+
+ # allows updating existing job opening
+ opening_1.status = "Closed"
+ opening_1.save()
+
+
+def get_job_opening(**args):
+ args = frappe._dict(args)
+
+ opening = frappe.db.exists("Job Opening", {"job_title": args.job_title or "Designer"})
+ if opening:
+ return frappe.get_doc("Job Opening", opening)
+
+ opening = frappe.get_doc(
+ {
+ "doctype": "Job Opening",
+ "job_title": "Designer",
+ "designation": "Designer",
+ "company": "_Test Opening Company",
+ "status": "Open",
+ }
+ )
+
+ opening.update(args)
+
+ return opening
diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py
index 0f655e3e0f..7c0f0db197 100644
--- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py
+++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py
@@ -7,7 +7,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate, nowdate
-from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves
+from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
from erpnext.hr.utils import set_employee_name, validate_active_employee
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import (
@@ -107,7 +107,10 @@ class LeaveEncashment(Document):
self.leave_balance = (
allocation.total_leaves_allocated
- allocation.carry_forwarded_leaves_count
- - get_unused_leaves(self.employee, self.leave_type, allocation.from_date, self.encashment_date)
+ # adding this because the function returns a -ve number
+ + get_leaves_for_period(
+ self.employee, self.leave_type, allocation.from_date, self.encashment_date
+ )
)
encashable_days = self.leave_balance - frappe.db.get_value(
@@ -126,14 +129,25 @@ class LeaveEncashment(Document):
return True
def get_leave_allocation(self):
- leave_allocation = frappe.db.sql(
- """select name, to_date, total_leaves_allocated, carry_forwarded_leaves_count from `tabLeave Allocation` where '{0}'
- between from_date and to_date and docstatus=1 and leave_type='{1}'
- and employee= '{2}'""".format(
- self.encashment_date or getdate(nowdate()), self.leave_type, self.employee
- ),
- as_dict=1,
- ) # nosec
+ date = self.encashment_date or getdate()
+
+ LeaveAllocation = frappe.qb.DocType("Leave Allocation")
+ leave_allocation = (
+ frappe.qb.from_(LeaveAllocation)
+ .select(
+ LeaveAllocation.name,
+ LeaveAllocation.from_date,
+ LeaveAllocation.to_date,
+ LeaveAllocation.total_leaves_allocated,
+ LeaveAllocation.carry_forwarded_leaves_count,
+ )
+ .where(
+ ((LeaveAllocation.from_date <= date) & (date <= LeaveAllocation.to_date))
+ & (LeaveAllocation.docstatus == 1)
+ & (LeaveAllocation.leave_type == self.leave_type)
+ & (LeaveAllocation.employee == self.employee)
+ )
+ ).run(as_dict=True)
return leave_allocation[0] if leave_allocation else None
diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py
index 83eb969feb..d06b6a3764 100644
--- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py
+++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py
@@ -4,26 +4,42 @@
import unittest
import frappe
-from frappe.utils import add_months, today
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_days, get_year_ending, get_year_start, getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period
from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
create_assignment_for_multiple_employees,
)
+from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
+ make_holiday_list,
+ make_leave_application,
+)
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
-test_dependencies = ["Leave Type"]
+test_records = frappe.get_test_records("Leave Type")
-class TestLeaveEncashment(unittest.TestCase):
+class TestLeaveEncashment(FrappeTestCase):
def setUp(self):
- frappe.db.sql("""delete from `tabLeave Period`""")
- frappe.db.sql("""delete from `tabLeave Policy Assignment`""")
- frappe.db.sql("""delete from `tabLeave Allocation`""")
- frappe.db.sql("""delete from `tabLeave Ledger Entry`""")
- frappe.db.sql("""delete from `tabAdditional Salary`""")
+ frappe.db.delete("Leave Period")
+ frappe.db.delete("Leave Policy Assignment")
+ frappe.db.delete("Leave Allocation")
+ frappe.db.delete("Leave Ledger Entry")
+ frappe.db.delete("Additional Salary")
+ frappe.db.delete("Leave Encashment")
+
+ if not frappe.db.exists("Leave Type", "_Test Leave Type Encashment"):
+ frappe.get_doc(test_records[2]).insert()
+
+ date = getdate()
+ year_start = getdate(get_year_start(date))
+ year_end = getdate(get_year_ending(date))
+
+ make_holiday_list("_Test Leave Encashment", year_start, year_end)
# create the leave policy
leave_policy = create_leave_policy(
@@ -32,9 +48,9 @@ class TestLeaveEncashment(unittest.TestCase):
leave_policy.submit()
# create employee, salary structure and assignment
- self.employee = make_employee("test_employee_encashment@example.com")
+ self.employee = make_employee("test_employee_encashment@example.com", company="_Test Company")
- self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
+ self.leave_period = create_leave_period(year_start, year_end, "_Test Company")
data = {
"assignment_based_on": "Leave Period",
@@ -53,27 +69,15 @@ class TestLeaveEncashment(unittest.TestCase):
other_details={"leave_encashment_amount_per_day": 50},
)
- def tearDown(self):
- for dt in [
- "Leave Period",
- "Leave Allocation",
- "Leave Ledger Entry",
- "Additional Salary",
- "Leave Encashment",
- "Salary Structure",
- "Leave Policy",
- ]:
- frappe.db.sql("delete from `tab%s`" % dt)
-
+ @set_holiday_list("_Test Leave Encashment", "_Test Company")
def test_leave_balance_value_and_amount(self):
- frappe.db.sql("""delete from `tabLeave Encashment`""")
leave_encashment = frappe.get_doc(
dict(
doctype="Leave Encashment",
employee=self.employee,
leave_type="_Test Leave Type Encashment",
leave_period=self.leave_period.name,
- payroll_date=today(),
+ encashment_date=self.leave_period.to_date,
currency="INR",
)
).insert()
@@ -88,15 +92,46 @@ class TestLeaveEncashment(unittest.TestCase):
add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0]
self.assertTrue(add_sal)
- def test_creation_of_leave_ledger_entry_on_submit(self):
- frappe.db.sql("""delete from `tabLeave Encashment`""")
+ @set_holiday_list("_Test Leave Encashment", "_Test Company")
+ def test_leave_balance_value_with_leaves_and_amount(self):
+ date = self.leave_period.from_date
+ leave_application = make_leave_application(
+ self.employee, date, add_days(date, 3), "_Test Leave Type Encashment"
+ )
+ leave_application.reload()
+
leave_encashment = frappe.get_doc(
dict(
doctype="Leave Encashment",
employee=self.employee,
leave_type="_Test Leave Type Encashment",
leave_period=self.leave_period.name,
- payroll_date=today(),
+ encashment_date=self.leave_period.to_date,
+ currency="INR",
+ )
+ ).insert()
+
+ self.assertEqual(leave_encashment.leave_balance, 10 - leave_application.total_leave_days)
+ # encashable days threshold is 5, total leaves are 6, so encashable days = 6-5 = 1
+ # with charge of 50 per day
+ self.assertEqual(leave_encashment.encashable_days, leave_encashment.leave_balance - 5)
+ self.assertEqual(leave_encashment.encashment_amount, 50)
+
+ leave_encashment.submit()
+
+ # assert links
+ add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0]
+ self.assertTrue(add_sal)
+
+ @set_holiday_list("_Test Leave Encashment", "_Test Company")
+ def test_creation_of_leave_ledger_entry_on_submit(self):
+ leave_encashment = frappe.get_doc(
+ dict(
+ doctype="Leave Encashment",
+ employee=self.employee,
+ leave_type="_Test Leave Type Encashment",
+ leave_period=self.leave_period.name,
+ encashment_date=self.leave_period.to_date,
currency="INR",
)
).insert()
diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py
index ce7e50f7f4..82472dec41 100644
--- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py
+++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py
@@ -172,27 +172,24 @@ class StaffingPlan(Document):
@frappe.whitelist()
-def get_designation_counts(designation, company):
+def get_designation_counts(designation, company, job_opening=None):
if not designation:
return False
- employee_counts = {}
company_set = get_descendants_of("Company", company)
company_set.append(company)
- employee_counts["employee_count"] = frappe.db.get_value(
- "Employee",
- filters={"designation": designation, "status": "Active", "company": ("in", company_set)},
- fieldname=["count(name)"],
+ employee_count = frappe.db.count(
+ "Employee", {"designation": designation, "status": "Active", "company": ("in", company_set)}
)
- employee_counts["job_openings"] = frappe.db.get_value(
- "Job Opening",
- filters={"designation": designation, "status": "Open", "company": ("in", company_set)},
- fieldname=["count(name)"],
- )
+ filters = {"designation": designation, "status": "Open", "company": ("in", company_set)}
+ if job_opening:
+ filters["name"] = ("!=", job_opening)
- return employee_counts
+ job_openings = frappe.db.count("Job Opening", filters)
+
+ return {"employee_count": employee_count, "job_openings": job_openings}
@frappe.whitelist()
diff --git a/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py b/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py
index a3adbbd56a..ac69c21979 100644
--- a/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py
+++ b/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py
@@ -85,13 +85,16 @@ def _set_up():
make_company()
-def make_company():
- if frappe.db.exists("Company", "_Test Company 10"):
+def make_company(name=None, abbr=None):
+ if not name:
+ name = "_Test Company 10"
+
+ if frappe.db.exists("Company", name):
return
company = frappe.new_doc("Company")
- company.company_name = "_Test Company 10"
- company.abbr = "_TC10"
+ company.company_name = name
+ company.abbr = abbr or "_TC10"
company.parent_company = "_Test Company 3"
company.default_currency = "INR"
company.country = "Pakistan"
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index 8614fcb9cd..dcbdf8a81e 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -448,8 +448,6 @@ class LoanRepayment(AccountsController):
"remarks": remarks,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date),
- "party_type": self.applicant_type if self.repay_from_salary else "",
- "party": self.applicant if self.repay_from_salary else "",
}
)
)
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 8a7634e24e..3d96f9c9c7 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -499,15 +499,11 @@ cur_frm.cscript.qty = function(doc) {
cur_frm.cscript.rate = function(doc, cdt, cdn) {
var d = locals[cdt][cdn];
- var scrap_items = false;
-
- if(cdt == 'BOM Scrap Item') {
- scrap_items = true;
- }
+ const is_scrap_item = cdt == "BOM Scrap Item";
if (d.bom_no) {
frappe.msgprint(__("You cannot change the rate if BOM is mentioned against any Item."));
- get_bom_material_detail(doc, cdt, cdn, scrap_items);
+ get_bom_material_detail(doc, cdt, cdn, is_scrap_item);
} else {
erpnext.bom.calculate_rm_cost(doc);
erpnext.bom.calculate_scrap_materials_cost(doc);
diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json
index 3406215cbb..0a8ae7b4a7 100644
--- a/erpnext/manufacturing/doctype/bom_item/bom_item.json
+++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json
@@ -33,7 +33,6 @@
"amount",
"base_amount",
"section_break_18",
- "scrap",
"qty_consumed_per_unit",
"section_break_27",
"has_variants",
@@ -223,15 +222,6 @@
"fieldname": "section_break_18",
"fieldtype": "Section Break"
},
- {
- "columns": 1,
- "fieldname": "scrap",
- "fieldtype": "Float",
- "label": "Scrap %",
- "oldfieldname": "scrap",
- "oldfieldtype": "Currency",
- "print_hide": 1
- },
{
"fieldname": "qty_consumed_per_unit",
"fieldtype": "Float",
@@ -298,7 +288,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2022-01-24 16:57:57.020232",
+ "modified": "2022-05-19 02:32:43.785470",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Item",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index a98fc94868..0a9fd8a099 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -42,6 +42,10 @@ class JobCardCancelError(frappe.ValidationError):
pass
+class JobCardOverTransferError(frappe.ValidationError):
+ pass
+
+
class JobCard(Document):
def onload(self):
excess_transfer = frappe.db.get_single_value(
@@ -522,23 +526,50 @@ class JobCard(Document):
},
)
- def set_transferred_qty_in_job_card(self, ste_doc):
+ def set_transferred_qty_in_job_card_item(self, ste_doc):
+ from frappe.query_builder.functions import Sum
+
+ def _validate_over_transfer(row, transferred_qty):
+ "Block over transfer of items if not allowed in settings."
+ required_qty = frappe.db.get_value("Job Card Item", row.job_card_item, "required_qty")
+ is_excess = flt(transferred_qty) > flt(required_qty)
+ if is_excess:
+ frappe.throw(
+ _(
+ "Row #{0}: Cannot transfer more than Required Qty {1} for Item {2} against Job Card {3}"
+ ).format(
+ row.idx, frappe.bold(required_qty), frappe.bold(row.item_code), ste_doc.job_card
+ ),
+ title=_("Excess Transfer"),
+ exc=JobCardOverTransferError,
+ )
+
for row in ste_doc.items:
if not row.job_card_item:
continue
- qty = frappe.db.sql(
- """ SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se
- WHERE sed.job_card_item = %s and se.docstatus = 1 and sed.parent = se.name and
- se.purpose = 'Material Transfer for Manufacture'
- """,
- (row.job_card_item),
- )[0][0]
+ sed = frappe.qb.DocType("Stock Entry Detail")
+ se = frappe.qb.DocType("Stock Entry")
+ transferred_qty = (
+ frappe.qb.from_(sed)
+ .join(se)
+ .on(sed.parent == se.name)
+ .select(Sum(sed.qty))
+ .where(
+ (sed.job_card_item == row.job_card_item)
+ & (se.docstatus == 1)
+ & (se.purpose == "Material Transfer for Manufacture")
+ )
+ ).run()[0][0]
- frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(qty))
+ allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
+ if not allow_excess:
+ _validate_over_transfer(row, transferred_qty)
+
+ frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty))
def set_transferred_qty(self, update_status=False):
- "Set total FG Qty for which RM was transferred."
+ "Set total FG Qty in Job Card for which RM was transferred."
if not self.items:
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
@@ -866,6 +897,7 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta
target.set("time_logs", [])
target.set("employee", [])
target.set("items", [])
+ target.set("sub_operations", [])
target.set_sub_operations()
target.get_required_items()
target.validate_time_logs()
diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py
index 4647ddf05f..7f3c7fefe9 100644
--- a/erpnext/manufacturing/doctype/job_card/test_job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py
@@ -1,15 +1,25 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-import frappe
-from frappe.tests.utils import FrappeTestCase
-from frappe.utils import random_string
-from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError
+from typing import Literal
+
+import frappe
+from frappe.tests.utils import FrappeTestCase, change_settings
+from frappe.utils import random_string
+from frappe.utils.data import add_to_date, now
+
+from erpnext.manufacturing.doctype.job_card.job_card import (
+ JobCardOverTransferError,
+ OperationMismatchError,
+ OverlapError,
+ make_corrective_job_card,
+)
from erpnext.manufacturing.doctype.job_card.job_card import (
make_stock_entry as make_stock_entry_from_jc,
)
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
+from erpnext.manufacturing.doctype.work_order.work_order import WorkOrder
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@@ -17,34 +27,36 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
class TestJobCard(FrappeTestCase):
def setUp(self):
make_bom_for_jc_tests()
+ self.transfer_material_against: Literal["Work Order", "Job Card"] = "Work Order"
+ self.source_warehouse = None
+ self._work_order = None
- transfer_material_against, source_warehouse = None, None
+ @property
+ def work_order(self) -> WorkOrder:
+ """Work Order lazily created for tests."""
+ if not self._work_order:
+ self._work_order = make_wo_order_test_record(
+ item="_Test FG Item 2",
+ qty=2,
+ transfer_material_against=self.transfer_material_against,
+ source_warehouse=self.source_warehouse,
+ )
+ return self._work_order
- tests_that_skip_setup = ("test_job_card_material_transfer_correctness",)
- tests_that_transfer_against_jc = (
- "test_job_card_multiple_materials_transfer",
- "test_job_card_excess_material_transfer",
- "test_job_card_partial_material_transfer",
- )
-
- if self._testMethodName in tests_that_skip_setup:
- return
-
- if self._testMethodName in tests_that_transfer_against_jc:
- transfer_material_against = "Job Card"
- source_warehouse = "Stores - _TC"
-
- self.work_order = make_wo_order_test_record(
- item="_Test FG Item 2",
- qty=2,
- transfer_material_against=transfer_material_against,
- source_warehouse=source_warehouse,
- )
+ def generate_required_stock(self, work_order: WorkOrder) -> None:
+ """Create twice the stock for all required items in work order."""
+ for item in work_order.required_items:
+ make_stock_entry(
+ item_code=item.item_code,
+ target=item.source_warehouse or self.source_warehouse,
+ qty=item.required_qty * 2,
+ basic_rate=100,
+ )
def tearDown(self):
frappe.db.rollback()
- def test_job_card(self):
+ def test_job_card_operations(self):
job_cards = frappe.get_all(
"Job Card", filters={"work_order": self.work_order.name}, fields=["operation_id", "name"]
@@ -58,9 +70,6 @@ class TestJobCard(FrappeTestCase):
doc.operation_id = "Test Data"
self.assertRaises(OperationMismatchError, doc.save)
- for d in job_cards:
- frappe.delete_doc("Job Card", d.name)
-
def test_job_card_with_different_work_station(self):
job_cards = frappe.get_all(
"Job Card",
@@ -96,19 +105,11 @@ class TestJobCard(FrappeTestCase):
)
self.assertEqual(completed_qty, job_card.for_quantity)
- doc.cancel()
-
- for d in job_cards:
- frappe.delete_doc("Job Card", d.name)
-
def test_job_card_overlap(self):
wo2 = make_wo_order_test_record(item="_Test FG Item 2", qty=2)
- jc1_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
- jc2_name = frappe.db.get_value("Job Card", {"work_order": wo2.name})
-
- jc1 = frappe.get_doc("Job Card", jc1_name)
- jc2 = frappe.get_doc("Job Card", jc2_name)
+ jc1 = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
+ jc2 = frappe.get_last_doc("Job Card", {"work_order": wo2.name})
employee = "_T-Employee-00001" # from test records
@@ -137,10 +138,10 @@ class TestJobCard(FrappeTestCase):
def test_job_card_multiple_materials_transfer(self):
"Test transferring RMs separately against Job Card with multiple RMs."
- make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=100)
- make_stock_entry(
- item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=6, basic_rate=100
- )
+ self.transfer_material_against = "Job Card"
+ self.source_warehouse = "Stores - _TC"
+
+ self.generate_required_stock(self.work_order)
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
job_card = frappe.get_doc("Job Card", job_card_name)
@@ -165,16 +166,58 @@ class TestJobCard(FrappeTestCase):
# transfer was made for 2 fg qty in first transfer Stock Entry
self.assertEqual(transfer_entry_2.fg_completed_qty, 0)
+ @change_settings("Manufacturing Settings", {"job_card_excess_transfer": 1})
def test_job_card_excess_material_transfer(self):
"Test transferring more than required RM against Job Card."
- make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100)
- make_stock_entry(
- item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100
+ self.transfer_material_against = "Job Card"
+ self.source_warehouse = "Stores - _TC"
+
+ self.generate_required_stock(self.work_order)
+
+ job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
+ self.assertEqual(job_card.status, "Open")
+
+ # fully transfer both RMs
+ transfer_entry_1 = make_stock_entry_from_jc(job_card.name)
+ transfer_entry_1.insert()
+ transfer_entry_1.submit()
+
+ # transfer extra qty of both RM due to previously damaged RM
+ transfer_entry_2 = make_stock_entry_from_jc(job_card.name)
+ # deliberately change 'For Quantity'
+ transfer_entry_2.fg_completed_qty = 1
+ transfer_entry_2.items[0].qty = 5
+ transfer_entry_2.items[1].qty = 3
+ transfer_entry_2.insert()
+ transfer_entry_2.submit()
+
+ job_card.reload()
+ self.assertGreater(job_card.transferred_qty, job_card.for_quantity)
+
+ # Check if 'For Quantity' is negative
+ # as 'transferred_qty' > Qty to Manufacture
+ transfer_entry_3 = make_stock_entry_from_jc(job_card.name)
+ self.assertEqual(transfer_entry_3.fg_completed_qty, 0)
+
+ job_card.append(
+ "time_logs",
+ {"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 2},
)
+ job_card.save()
+ job_card.submit()
+
+ # JC is Completed with excess transfer
+ self.assertEqual(job_card.status, "Completed")
+
+ @change_settings("Manufacturing Settings", {"job_card_excess_transfer": 0})
+ def test_job_card_excess_material_transfer_block(self):
+
+ self.transfer_material_against = "Job Card"
+ self.source_warehouse = "Stores - _TC"
+
+ self.generate_required_stock(self.work_order)
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
- job_card = frappe.get_doc("Job Card", job_card_name)
- self.assertEqual(job_card.status, "Open")
# fully transfer both RMs
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
@@ -188,39 +231,19 @@ class TestJobCard(FrappeTestCase):
transfer_entry_2.items[0].qty = 5
transfer_entry_2.items[1].qty = 3
transfer_entry_2.insert()
- transfer_entry_2.submit()
-
- job_card.reload()
- self.assertGreater(job_card.transferred_qty, job_card.for_quantity)
-
- # Check if 'For Quantity' is negative
- # as 'transferred_qty' > Qty to Manufacture
- transfer_entry_3 = make_stock_entry_from_jc(job_card_name)
- self.assertEqual(transfer_entry_3.fg_completed_qty, 0)
-
- job_card.append(
- "time_logs",
- {"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 2},
- )
- job_card.save()
- job_card.submit()
-
- # JC is Completed with excess transfer
- self.assertEqual(job_card.status, "Completed")
+ self.assertRaises(JobCardOverTransferError, transfer_entry_2.submit)
def test_job_card_partial_material_transfer(self):
"Test partial material transfer against Job Card"
+ self.transfer_material_against = "Job Card"
+ self.source_warehouse = "Stores - _TC"
- make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100)
- make_stock_entry(
- item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100
- )
+ self.generate_required_stock(self.work_order)
- job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
- job_card = frappe.get_doc("Job Card", job_card_name)
+ job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
# partially transfer
- transfer_entry = make_stock_entry_from_jc(job_card_name)
+ transfer_entry = make_stock_entry_from_jc(job_card.name)
transfer_entry.fg_completed_qty = 1
transfer_entry.get_items()
transfer_entry.insert()
@@ -232,7 +255,7 @@ class TestJobCard(FrappeTestCase):
self.assertEqual(transfer_entry.items[1].qty, 3)
# transfer remaining
- transfer_entry_2 = make_stock_entry_from_jc(job_card_name)
+ transfer_entry_2 = make_stock_entry_from_jc(job_card.name)
self.assertEqual(transfer_entry_2.fg_completed_qty, 1)
self.assertEqual(transfer_entry_2.items[0].qty, 5)
@@ -277,7 +300,49 @@ class TestJobCard(FrappeTestCase):
self.assertEqual(transfer_entry.items[0].item_code, "_Test Item")
self.assertEqual(transfer_entry.items[0].qty, 2)
- # rollback via tearDown method
+ @change_settings(
+ "Manufacturing Settings", {"add_corrective_operation_cost_in_finished_good_valuation": 1}
+ )
+ def test_corrective_costing(self):
+ job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
+
+ job_card.append(
+ "time_logs",
+ {"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 2},
+ )
+ job_card.submit()
+
+ self.work_order.reload()
+ original_cost = self.work_order.total_operating_cost
+
+ # Create a corrective operation against it
+ corrective_action = frappe.get_doc(
+ doctype="Operation", is_corrective_operation=1, name=frappe.generate_hash()
+ ).insert()
+
+ corrective_job_card = make_corrective_job_card(
+ job_card.name, operation=corrective_action.name, for_operation=job_card.operation
+ )
+ corrective_job_card.hour_rate = 100
+ corrective_job_card.insert()
+ corrective_job_card.append(
+ "time_logs",
+ {
+ "from_time": add_to_date(now(), hours=2),
+ "to_time": add_to_date(now(), hours=2, minutes=30),
+ "completed_qty": 2,
+ },
+ )
+ corrective_job_card.submit()
+
+ self.work_order.reload()
+ cost_after_correction = self.work_order.total_operating_cost
+ self.assertGreater(cost_after_correction, original_cost)
+
+ corrective_job_card.cancel()
+ self.work_order.reload()
+ cost_after_cancel = self.work_order.total_operating_cost
+ self.assertEqual(cost_after_cancel, original_cost)
def create_bom_with_multiple_operations():
diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
index ac2f61c5de..2aa31be0f0 100644
--- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
+++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
@@ -21,7 +21,7 @@ def get_exploded_items(bom, data, indent=0, qty=1):
exploded_items = frappe.get_all(
"BOM Item",
filters={"parent": bom},
- fields=["qty", "bom_no", "qty", "scrap", "item_code", "item_name", "description", "uom"],
+ fields=["qty", "bom_no", "qty", "item_code", "item_name", "description", "uom"],
)
for item in exploded_items:
@@ -37,7 +37,6 @@ def get_exploded_items(bom, data, indent=0, qty=1):
"qty": item.qty * qty,
"uom": item.uom,
"description": item.description,
- "scrap": item.scrap,
}
)
if item.bom_no:
@@ -64,5 +63,4 @@ def get_columns():
"fieldname": "description",
"width": 150,
},
- {"label": _("Scrap"), "fieldtype": "data", "fieldname": "scrap", "width": 100},
]
diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py
index 0a79130f1b..c324172372 100644
--- a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py
+++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py
@@ -34,8 +34,8 @@ def get_data(filters):
if filters.get(field):
query_filters[field] = ("in", filters.get(field))
- query_filters["report_date"] = (">=", filters.get("from_date"))
- query_filters["report_date"] = ("<=", filters.get("to_date"))
+
+ query_filters["report_date"] = ["between", [filters.get("from_date"), filters.get("to_date")]]
return frappe.get_all(
"Quality Inspection", fields=fields, filters=query_filters, order_by="report_date asc"
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 4d9a7e06bf..8c0ebe7a90 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -359,7 +359,7 @@ erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
erpnext.patches.v13_0.update_accounts_in_loan_docs
erpnext.patches.v14_0.update_batch_valuation_flag
erpnext.patches.v14_0.delete_non_profit_doctypes
-erpnext.patches.v14_0.update_employee_advance_status
+erpnext.patches.v13_0.update_employee_advance_status
erpnext.patches.v13_0.add_cost_center_in_loans
erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022
diff --git a/erpnext/patches/v14_0/update_employee_advance_status.py b/erpnext/patches/v13_0/update_employee_advance_status.py
similarity index 100%
rename from erpnext/patches/v14_0/update_employee_advance_status.py
rename to erpnext/patches/v13_0/update_employee_advance_status.py
diff --git a/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py
new file mode 100644
index 0000000000..c2267aa9af
--- /dev/null
+++ b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py
@@ -0,0 +1,38 @@
+import frappe
+from frappe import qb
+
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
+ get_dimensions,
+ make_dimension_in_accounting_doctypes,
+)
+from erpnext.accounts.utils import create_payment_ledger_entry
+
+
+def create_accounting_dimension_fields():
+ dimensions_and_defaults = get_dimensions()
+ if dimensions_and_defaults:
+ for dimension in dimensions_and_defaults[0]:
+ make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"])
+
+
+def execute():
+ # create accounting dimension fields in Payment Ledger
+ create_accounting_dimension_fields()
+
+ gl = qb.DocType("GL Entry")
+ accounts = frappe.db.get_list(
+ "Account", "name", filters={"account_type": ["in", ["Receivable", "Payable"]]}, as_list=True
+ )
+ gl_entries = []
+ if accounts:
+ # get all gl entries on receivable/payable accounts
+ gl_entries = (
+ qb.from_(gl)
+ .select("*")
+ .where(gl.account.isin(accounts))
+ .where(gl.is_cancelled == 0)
+ .run(as_dict=True)
+ )
+ if gl_entries:
+ # create payment ledger entries for the accounts receivable/payable
+ create_payment_ledger_entry(gl_entries, 0)
diff --git a/erpnext/payroll/doctype/gratuity/gratuity.json b/erpnext/payroll/doctype/gratuity/gratuity.json
index 1fd1cecaaa..c540baf7e6 100644
--- a/erpnext/payroll/doctype/gratuity/gratuity.json
+++ b/erpnext/payroll/doctype/gratuity/gratuity.json
@@ -76,9 +76,8 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
- "options": "Draft\nUnpaid\nPaid",
- "read_only": 1,
- "reqd": 1
+ "options": "Draft\nUnpaid\nPaid\nSubmitted\nCancelled",
+ "read_only": 1
},
{
"depends_on": "eval: !doc.pay_via_salary_slip",
@@ -194,7 +193,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-02-02 14:00:45.536152",
+ "modified": "2022-05-27 13:56:14.349183",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Gratuity",
diff --git a/erpnext/payroll/doctype/gratuity/gratuity_list.js b/erpnext/payroll/doctype/gratuity/gratuity_list.js
new file mode 100644
index 0000000000..20e3d5b4e5
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity/gratuity_list.js
@@ -0,0 +1,12 @@
+frappe.listview_settings["Gratuity"] = {
+ get_indicator: function(doc) {
+ let status_color = {
+ "Draft": "red",
+ "Submitted": "blue",
+ "Cancelled": "red",
+ "Paid": "green",
+ "Unpaid": "orange",
+ };
+ return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
+ }
+};
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py
index aa03d80d63..1155a06edd 100644
--- a/erpnext/payroll/doctype/gratuity/test_gratuity.py
+++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py
@@ -4,57 +4,69 @@
import unittest
import frappe
-from frappe.utils import add_days, flt, get_datetime, getdate
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_days, add_months, floor, flt, get_datetime, get_first_day, getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.expense_claim.test_expense_claim import get_payable_account
+from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
from erpnext.payroll.doctype.gratuity.gratuity import get_last_salary_slip
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
make_deduction_salary_component,
make_earning_salary_component,
make_employee_salary_slip,
+ make_holiday_list,
)
+from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule
test_dependencies = ["Salary Component", "Salary Slip", "Account"]
-class TestGratuity(unittest.TestCase):
+class TestGratuity(FrappeTestCase):
def setUp(self):
frappe.db.delete("Gratuity")
+ frappe.db.delete("Salary Slip")
frappe.db.delete("Additional Salary", {"ref_doctype": "Gratuity"})
make_earning_salary_component(
setup=True, test_tax=True, company_list=["_Test Company"], include_flexi_benefits=True
)
make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"])
+ make_holiday_list()
+ @set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
def test_get_last_salary_slip_should_return_none_for_new_employee(self):
new_employee = make_employee("new_employee@salary.com", company="_Test Company")
salary_slip = get_last_salary_slip(new_employee)
- assert salary_slip is None
+ self.assertIsNone(salary_slip)
- def test_check_gratuity_amount_based_on_current_slab_and_additional_salary_creation(self):
- employee, sal_slip = create_employee_and_get_last_salary_slip()
+ @set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
+ def test_gratuity_based_on_current_slab_via_additional_salary(self):
+ """
+ Range | Fraction
+ 5-0 | 1
+ """
+ doj = add_days(getdate(), -(6 * 365))
+ relieving_date = getdate()
+
+ employee = make_employee(
+ "test_employee_gratuity@salary.com",
+ company="_Test Company",
+ date_of_joining=doj,
+ relieving_date=relieving_date,
+ )
+ sal_slip = create_salary_slip("test_employee_gratuity@salary.com")
rule = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)")
gratuity = create_gratuity(pay_via_salary_slip=1, employee=employee, rule=rule.name)
# work experience calculation
- date_of_joining, relieving_date = frappe.db.get_value(
- "Employee", employee, ["date_of_joining", "relieving_date"]
- )
- employee_total_workings_days = (
- get_datetime(relieving_date) - get_datetime(date_of_joining)
- ).days
+ employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(doj)).days
+ experience = floor(employee_total_workings_days / rule.total_working_days_per_year)
+ self.assertEqual(gratuity.current_work_experience, experience)
- experience = employee_total_workings_days / rule.total_working_days_per_year
- gratuity.reload()
- from math import floor
-
- self.assertEqual(floor(experience), gratuity.current_work_experience)
-
- # amount Calculation
+ # amount calculation
component_amount = frappe.get_all(
"Salary Detail",
filters={
@@ -64,20 +76,44 @@ class TestGratuity(unittest.TestCase):
"salary_component": "Basic Salary",
},
fields=["amount"],
+ limit=1,
)
-
- """ 5 - 0 fraction is 1 """
-
gratuity_amount = component_amount[0].amount * experience
- gratuity.reload()
-
self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2))
# additional salary creation (Pay via salary slip)
self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gratuity.name}))
- def test_check_gratuity_amount_based_on_all_previous_slabs(self):
- employee, sal_slip = create_employee_and_get_last_salary_slip()
+ # gratuity should be marked "Paid" on the next salary slip submission
+ salary_slip = make_salary_slip("Test Gratuity", employee=employee)
+ salary_slip.posting_date = getdate()
+ salary_slip.insert()
+ salary_slip.submit()
+
+ gratuity.reload()
+ self.assertEqual(gratuity.status, "Paid")
+
+ @set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
+ def test_gratuity_based_on_all_previous_slabs_via_payment_entry(self):
+ """
+ Range | Fraction
+ 0-1 | 0
+ 1-5 | 0.7
+ 5-0 | 1
+ """
+ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+
+ doj = add_days(getdate(), -(6 * 365))
+ relieving_date = getdate()
+
+ employee = make_employee(
+ "test_employee_gratuity@salary.com",
+ company="_Test Company",
+ date_of_joining=doj,
+ relieving_date=relieving_date,
+ )
+
+ sal_slip = create_salary_slip("test_employee_gratuity@salary.com")
rule = get_gratuity_rule("Rule Under Limited Contract (UAE)")
set_mode_of_payment_account()
@@ -86,22 +122,11 @@ class TestGratuity(unittest.TestCase):
)
# work experience calculation
- date_of_joining, relieving_date = frappe.db.get_value(
- "Employee", employee, ["date_of_joining", "relieving_date"]
- )
- employee_total_workings_days = (
- get_datetime(relieving_date) - get_datetime(date_of_joining)
- ).days
+ employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(doj)).days
+ experience = floor(employee_total_workings_days / rule.total_working_days_per_year)
+ self.assertEqual(gratuity.current_work_experience, experience)
- experience = employee_total_workings_days / rule.total_working_days_per_year
-
- gratuity.reload()
-
- from math import floor
-
- self.assertEqual(floor(experience), gratuity.current_work_experience)
-
- # amount Calculation
+ # amount calculation
component_amount = frappe.get_all(
"Salary Detail",
filters={
@@ -111,35 +136,22 @@ class TestGratuity(unittest.TestCase):
"salary_component": "Basic Salary",
},
fields=["amount"],
+ limit=1,
)
- """ range | Fraction
- 0-1 | 0
- 1-5 | 0.7
- 5-0 | 1
- """
-
gratuity_amount = ((0 * 1) + (4 * 0.7) + (1 * 1)) * component_amount[0].amount
- gratuity.reload()
-
self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2))
self.assertEqual(gratuity.status, "Unpaid")
- from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+ pe = get_payment_entry("Gratuity", gratuity.name)
+ pe.reference_no = "123467"
+ pe.reference_date = getdate()
+ pe.submit()
- pay_entry = get_payment_entry("Gratuity", gratuity.name)
- pay_entry.reference_no = "123467"
- pay_entry.reference_date = getdate()
- pay_entry.save()
- pay_entry.submit()
gratuity.reload()
-
self.assertEqual(gratuity.status, "Paid")
self.assertEqual(flt(gratuity.paid_amount, 2), flt(gratuity.amount, 2))
- def tearDown(self):
- frappe.db.rollback()
-
def get_gratuity_rule(name):
rule = frappe.db.exists("Gratuity Rule", name)
@@ -149,7 +161,6 @@ def get_gratuity_rule(name):
rule.applicable_earnings_component = []
rule.append("applicable_earnings_component", {"salary_component": "Basic Salary"})
rule.save()
- rule.reload()
return rule
@@ -204,23 +215,17 @@ def create_account():
).insert(ignore_permissions=True)
-def create_employee_and_get_last_salary_slip():
- employee = make_employee("test_employee@salary.com", company="_Test Company")
- frappe.db.set_value("Employee", employee, "relieving_date", getdate())
- frappe.db.set_value("Employee", employee, "date_of_joining", add_days(getdate(), -(6 * 365)))
+def create_salary_slip(employee):
if not frappe.db.exists("Salary Slip", {"employee": employee}):
- salary_slip = make_employee_salary_slip("test_employee@salary.com", "Monthly")
+ posting_date = get_first_day(add_months(getdate(), -1))
+ salary_slip = make_employee_salary_slip(
+ employee, "Monthly", "Test Gratuity", posting_date=posting_date
+ )
+ salary_slip.start_date = posting_date
+ salary_slip.end_date = None
salary_slip.submit()
salary_slip = salary_slip.name
else:
salary_slip = get_last_salary_slip(employee)
- if not frappe.db.get_value("Employee", "test_employee@salary.com", "holiday_list"):
- from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
-
- make_holiday_list()
- frappe.db.set_value(
- "Company", "_Test Company", "default_holiday_list", "Salary Slip Test Holiday List"
- )
-
- return employee, salary_slip
+ return salary_slip
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index 54d56f9612..473fb0d7c7 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -16,6 +16,7 @@ from frappe.utils import (
comma_and,
date_diff,
flt,
+ get_link_to_form,
getdate,
)
@@ -45,6 +46,7 @@ class PayrollEntry(Document):
def before_submit(self):
self.validate_employee_details()
+ self.validate_payroll_payable_account()
if self.validate_attendance:
if self.validate_employee_attendance():
frappe.throw(_("Cannot Submit, Employees left to mark attendance"))
@@ -66,6 +68,14 @@ class PayrollEntry(Document):
if len(emp_with_sal_slip):
frappe.throw(_("Salary Slip already exists for {0}").format(comma_and(emp_with_sal_slip)))
+ def validate_payroll_payable_account(self):
+ if frappe.db.get_value("Account", self.payroll_payable_account, "account_type"):
+ frappe.throw(
+ _(
+ "Account type cannot be set for payroll payable account {0}, please remove and try again"
+ ).format(frappe.bold(get_link_to_form("Account", self.payroll_payable_account)))
+ )
+
def on_cancel(self):
frappe.delete_doc(
"Salary Slip",
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 6a7f72b013..f4f84155af 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -116,10 +116,10 @@ class SalarySlip(TransactionBase):
self.update_payment_status_for_gratuity()
def update_payment_status_for_gratuity(self):
- add_salary = frappe.db.get_all(
+ additional_salary = frappe.db.get_all(
"Additional Salary",
filters={
- "payroll_date": ("BETWEEN", [self.start_date, self.end_date]),
+ "payroll_date": ("between", [self.start_date, self.end_date]),
"employee": self.employee,
"ref_doctype": "Gratuity",
"docstatus": 1,
@@ -128,10 +128,10 @@ class SalarySlip(TransactionBase):
limit=1,
)
- if len(add_salary):
+ if additional_salary:
status = "Paid" if self.docstatus == 1 else "Unpaid"
- if add_salary[0].name in [data.additional_salary for data in self.earnings]:
- frappe.db.set_value("Gratuity", add_salary.ref_docname, "status", status)
+ if additional_salary[0].name in [entry.additional_salary for entry in self.earnings]:
+ frappe.db.set_value("Gratuity", additional_salary[0].ref_docname, "status", status)
def on_cancel(self):
self.set_status()
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index 1bc3741922..60ba2d9a07 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -997,7 +997,7 @@ class TestSalarySlip(unittest.TestCase):
return [no_of_days_in_month[1], no_of_holidays_in_month]
-def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
+def make_employee_salary_slip(user, payroll_frequency, salary_structure=None, posting_date=None):
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
if not salary_structure:
@@ -1008,7 +1008,11 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
)
salary_structure_doc = make_salary_structure(
- salary_structure, payroll_frequency, employee=employee.name, company=employee.company
+ salary_structure,
+ payroll_frequency,
+ employee=employee.name,
+ company=employee.company,
+ from_date=posting_date,
)
salary_slip_name = frappe.db.get_value(
"Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}
@@ -1018,7 +1022,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
salary_slip = make_salary_slip(salary_structure_doc.name, employee=employee.name)
salary_slip.employee_name = employee.employee_name
salary_slip.payroll_frequency = payroll_frequency
- salary_slip.posting_date = nowdate()
+ salary_slip.posting_date = posting_date or nowdate()
salary_slip.insert()
else:
salary_slip = frappe.get_doc("Salary Slip", salary_slip_name)
diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json
index 1cda0a08c4..1790da44d6 100644
--- a/erpnext/projects/doctype/project/project.json
+++ b/erpnext/projects/doctype/project/project.json
@@ -234,7 +234,7 @@
},
{
"fieldname": "actual_start_date",
- "fieldtype": "Data",
+ "fieldtype": "Date",
"label": "Actual Start Date (via Time Sheet)",
"read_only": 1
},
@@ -458,7 +458,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 4,
- "modified": "2022-01-29 13:58:27.712714",
+ "modified": "2022-05-25 22:45:06.108499",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",
@@ -504,4 +504,4 @@
"timeline_field": "customer",
"title_field": "project_name",
"track_seen": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 3dd11f69a7..16b0b4a866 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -789,11 +789,23 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) {
$.each(this.frm.doc['payments'] || [], function(index, data) {
if(data.default && payment_status && total_amount_to_pay > 0) {
- let base_amount = flt(total_amount_to_pay, precision("base_amount", data));
+ let base_amount, amount;
+
+ if (me.frm.doc.party_account_currency == me.frm.doc.currency) {
+ // if customer/supplier currency is same as company currency
+ // total_amount_to_pay is already in customer/supplier currency
+ // so base_amount has to be calculated using total_amount_to_pay
+ base_amount = flt(total_amount_to_pay * me.frm.doc.conversion_rate, precision("base_amount", data));
+ amount = flt(total_amount_to_pay, precision("amount", data));
+ } else {
+ base_amount = flt(total_amount_to_pay, precision("base_amount", data));
+ amount = flt(total_amount_to_pay / me.frm.doc.conversion_rate, precision("amount", data));
+ }
+
frappe.model.set_value(data.doctype, data.name, "base_amount", base_amount);
- let amount = flt(total_amount_to_pay / me.frm.doc.conversion_rate, precision("amount", data));
frappe.model.set_value(data.doctype, data.name, "amount", amount);
payment_status = false;
+
} else if(me.frm.doc.paid_amount) {
frappe.model.set_value(data.doctype, data.name, "amount", 0.0);
}
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 05a401bdee..d11205a1ad 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -944,7 +944,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
} else {
// company currency and doc currency is same
// this will prevent unnecessary conversion rate triggers
- this.frm.set_value("conversion_rate", 1.0);
+ if(this.frm.doc.currency === this.get_company_currency()) {
+ this.frm.set_value("conversion_rate", 1.0);
+ } else {
+ this.conversion_rate();
+ }
}
}
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
index ea56d07d6d..ef24ce791c 100644
--- a/erpnext/regional/india/e_invoice/einvoice.js
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -11,7 +11,7 @@ erpnext.setup_einvoice_actions = (doctype) => {
if (!invoice_eligible) return;
- const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc;
+ const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, qrcode_image, __unsaved } = frm.doc;
const add_custom_button = (label, action) => {
if (!frm.custom_buttons[label]) {
@@ -149,84 +149,70 @@ erpnext.setup_einvoice_actions = (doctype) => {
}
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
- const fields = [
- {
- "label": "Reason",
- "fieldname": "reason",
- "fieldtype": "Select",
- "reqd": 1,
- "default": "1-Duplicate",
- "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
- },
- {
- "label": "Remark",
- "fieldname": "remark",
- "fieldtype": "Data",
- "reqd": 1
- }
- ];
const action = () => {
- const d = new frappe.ui.Dialog({
- title: __('Cancel E-Way Bill'),
- fields: fields,
- primary_action: function() {
- const data = d.get_values();
- frappe.call({
- method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
- args: {
- doctype,
- docname: name,
- eway_bill: ewaybill,
- reason: data.reason.split('-')[0],
- remark: data.remark
- },
- freeze: true,
- callback: () => {
- frappe.show_alert({
- message: __('E-Way Bill Cancelled successfully'),
- indicator: 'green'
- }, 7);
- frm.reload_doc();
- d.hide();
- },
- error: () => {
- frappe.show_alert({
- message: __('E-Way Bill was not Cancelled'),
- indicator: 'red'
- }, 7);
- d.hide();
- }
- });
- },
- primary_action_label: __('Submit')
- });
- d.show();
- };
- add_custom_button(__("Cancel E-Way Bill"), action);
- }
+ let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
+ message += '
';
+ message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
- if (irn && !irn_cancelled) {
- const action = () => {
const dialog = frappe.msgprint({
- title: __("Generate QRCode"),
- message: __("Generate and attach QR Code using IRN?"),
+ title: __('Update E-Way Bill Cancelled Status?'),
+ message: message,
+ indicator: 'orange',
primary_action: {
action: function() {
frappe.call({
- method: 'erpnext.regional.india.e_invoice.utils.generate_qrcode',
+ method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
args: { doctype, docname: name },
freeze: true,
- callback: () => frm.reload_doc() || dialog.hide(),
- error: () => dialog.hide()
+ callback: () => frm.reload_doc() && dialog.hide()
});
}
},
primary_action_label: __('Yes')
});
+ };
+ add_custom_button(__("Cancel E-Way Bill"), action);
+ }
+
+ if (irn && !irn_cancelled) {
+ let is_qrcode_attached = false;
+ if (qrcode_image && frm.attachments) {
+ let attachments = frm.attachments.get_attachments();
+ if (attachments.length != 0) {
+ for (let i = 0; i < attachments.length; i++) {
+ if (attachments[i].file_url == qrcode_image) {
+ is_qrcode_attached = true;
+ break;
+ }
+ }
+ }
+ }
+ if (!is_qrcode_attached) {
+ const action = () => {
+ if (frm.doc.__unsaved) {
+ frappe.throw(__('Please save the document to generate QRCode.'));
+ }
+ const dialog = frappe.msgprint({
+ title: __("Generate QRCode"),
+ message: __("Generate and attach QR Code using IRN?"),
+ primary_action: {
+ action: function() {
+ frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.generate_qrcode',
+ args: { doctype, docname: name },
+ freeze: true,
+ callback: () => frm.reload_doc() || dialog.hide(),
+ error: () => dialog.hide()
+ });
+ }
+ },
+ primary_action_label: __('Yes')
+ });
dialog.show();
};
add_custom_button(__("Generate QRCode"), action);
}
+ }
}
});
};
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index ed1002a129..e5a1a59e42 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -649,6 +649,8 @@ def make_einvoice(invoice):
try:
einvoice = safe_json_load(einvoice)
einvoice = santize_einvoice_fields(einvoice)
+ except json.JSONDecodeError:
+ raise
except Exception:
show_link_to_error_log(invoice, einvoice)
@@ -765,7 +767,9 @@ def safe_json_load(json_string):
frappe.throw(
_(
"Error in input data. Please check for any special characters near following input:
{}"
- ).format(snippet)
+ ).format(snippet),
+ title=_("Invalid JSON"),
+ exc=e,
)
@@ -797,7 +801,8 @@ class GSPConnector:
self.irn_details_url = self.base_url + "/enriched/ei/api/invoice/irn"
self.generate_irn_url = self.base_url + "/enriched/ei/api/invoice"
self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin"
- self.cancel_ewaybill_url = self.base_url + "/enriched/ei/api/ewayapi"
+ # cancel_ewaybill_url will only work if user have bought ewb api from adaequare.
+ self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB"
self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill"
self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image"
@@ -1005,13 +1010,32 @@ class GSPConnector:
return failed
def fetch_and_attach_qrcode_from_irn(self):
- qrcode = self.get_qrcode_from_irn(self.invoice.irn)
- if qrcode:
- qrcode_file = self.create_qr_code_file(qrcode)
- frappe.db.set_value("Sales Invoice", self.invoice.name, "qrcode_image", qrcode_file.file_url)
- frappe.msgprint(_("QR Code attached to the invoice"), alert=True)
+ is_qrcode_file_attached = self.invoice.qrcode_image and frappe.db.exists(
+ "File",
+ {
+ "attached_to_doctype": "Sales Invoice",
+ "attached_to_name": self.invoice.name,
+ "file_url": self.invoice.qrcode_image,
+ "attached_to_field": "qrcode_image",
+ },
+ )
+ if not is_qrcode_file_attached:
+ if self.invoice.signed_qr_code:
+ self.attach_qrcode_image()
+ frappe.db.set_value(
+ "Sales Invoice", self.invoice.name, "qrcode_image", self.invoice.qrcode_image
+ )
+ frappe.msgprint(_("QR Code attached to the invoice."), alert=True)
+ else:
+ qrcode = self.get_qrcode_from_irn(self.invoice.irn)
+ if qrcode:
+ qrcode_file = self.create_qr_code_file(qrcode)
+ frappe.db.set_value("Sales Invoice", self.invoice.name, "qrcode_image", qrcode_file.file_url)
+ frappe.msgprint(_("QR Code attached to the invoice."), alert=True)
+ else:
+ frappe.msgprint(_("QR Code not found for the IRN"), alert=True)
else:
- frappe.msgprint(_("QR Code not found for the IRN"), alert=True)
+ frappe.msgprint(_("QR Code is already Attached"), indicator="green", alert=True)
def get_qrcode_from_irn(self, irn):
import requests
@@ -1185,6 +1209,7 @@ class GSPConnector:
headers = self.get_headers()
data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4)
headers["username"] = headers["user_name"]
+ del headers["user_name"]
try:
res = self.make_request("post", self.cancel_ewaybill_url, headers, data)
if res.get("success"):
@@ -1275,7 +1300,6 @@ class GSPConnector:
def attach_qrcode_image(self):
qrcode = self.invoice.signed_qr_code
-
qr_image = io.BytesIO()
url = qrcreate(qrcode, error="L")
url.png(qr_image, scale=2, quiet_zone=1)
@@ -1358,9 +1382,13 @@ def generate_eway_bill(doctype, docname, **kwargs):
@frappe.whitelist()
-def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
- gsp_connector = GSPConnector(doctype, docname)
- gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
+def cancel_eway_bill(doctype, docname):
+ # NOTE: cancel_eway_bill api is disabled by Adequare.
+ # gsp_connector = GSPConnector(doctype, docname)
+ # gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
+
+ frappe.db.set_value(doctype, docname, "ewaybill", "")
+ frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1)
@frappe.whitelist()
diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js
index 5f6dcdeb92..88973e36b6 100644
--- a/erpnext/regional/india/taxes.js
+++ b/erpnext/regional/india/taxes.js
@@ -22,6 +22,7 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
'shipping_address': frm.doc.shipping_address || '',
'shipping_address_name': frm.doc.shipping_address_name || '',
'customer_address': frm.doc.customer_address || '',
+ 'company_address': frm.doc.company_address,
'supplier_address': frm.doc.supplier_address,
'customer': frm.doc.customer,
'supplier': frm.doc.supplier,
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index fd0fe26e88..0bdbe56de6 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -448,7 +448,7 @@ class Gstr1Report(object):
hsn_code = self.item_hsn_map.get(item_code)
tax_rate = 0
taxable_value = items.get(item_code)
- for rates in hsn_wise_tax_rate.get(hsn_code):
+ for rates in hsn_wise_tax_rate.get(hsn_code, []):
if taxable_value > rates.get("minimum_taxable_value"):
tax_rate = rates.get("tax_rate")
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index b463213f50..7522e92a8a 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -232,7 +232,7 @@ class SalesOrder(SellingController):
update_coupon_code_count(self.coupon_code, "used")
def on_cancel(self):
- self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
+ self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
super(SalesOrder, self).on_cancel()
# Cannot cancel closed SO
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
index cb22fb6a80..91f4a5e50a 100644
--- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
@@ -187,8 +187,9 @@ def get_so_with_invoices(filters):
.on(soi.parent == so.name)
.join(ps)
.on(ps.parent == so.name)
+ .select(so.name)
+ .distinct()
.select(
- so.name,
so.customer,
so.transaction_date.as_("submitted"),
ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"),
diff --git a/erpnext/setup/doctype/naming_series/naming_series.js b/erpnext/setup/doctype/naming_series/naming_series.js
index 861b2b3983..0fb72abba6 100644
--- a/erpnext/setup/doctype/naming_series/naming_series.js
+++ b/erpnext/setup/doctype/naming_series/naming_series.js
@@ -54,5 +54,35 @@ frappe.ui.form.on("Naming Series", {
frm.events.get_doc_and_prefix(frm);
}
});
- }
+ },
+
+ naming_series_to_check(frm) {
+ frappe.call({
+ method: "preview_series",
+ doc: frm.doc,
+ callback: function(r) {
+ if (!r.exc) {
+ frm.set_value("preview", r.message);
+ } else {
+ frm.set_value("preview", __("Failed to generate preview of series"));
+ }
+ }
+ });
+ },
+
+ add_series(frm) {
+ const series = frm.doc.naming_series_to_check;
+
+ if (!series) {
+ frappe.show_alert(__("Please type a valid series."));
+ return;
+ }
+
+ if (!frm.doc.set_options.includes(series)) {
+ const current_series = frm.doc.set_options;
+ frm.set_value("set_options", `${current_series}\n${series}`);
+ } else {
+ frappe.show_alert(__("Series already added to transaction."));
+ }
+ },
});
diff --git a/erpnext/setup/doctype/naming_series/naming_series.json b/erpnext/setup/doctype/naming_series/naming_series.json
index f936dcf3c9..c65a6f0ae4 100644
--- a/erpnext/setup/doctype/naming_series/naming_series.json
+++ b/erpnext/setup/doctype/naming_series/naming_series.json
@@ -1,360 +1,132 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2013-01-25 11:35:08",
- "custom": 0,
- "description": "Set prefix for numbering series on your transactions",
- "docstatus": 0,
- "doctype": "DocType",
- "editable_grid": 0,
+ "actions": [],
+ "creation": "2022-05-26 03:12:49.087648",
+ "description": "Set prefix for numbering series on your transactions",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "setup_series",
+ "select_doc_for_series",
+ "help_html",
+ "naming_series_to_check",
+ "preview",
+ "add_series",
+ "set_options",
+ "user_must_always_select",
+ "update",
+ "column_break_13",
+ "update_series",
+ "prefix",
+ "current_value",
+ "update_series_start"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "Set prefix for numbering series on your transactions",
- "fieldname": "setup_series",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Setup Series",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "description": "Set prefix for numbering series on your transactions",
+ "fieldname": "setup_series",
+ "fieldtype": "Section Break",
+ "label": "Setup Series"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "select_doc_for_series",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Select Transaction",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "select_doc_for_series",
+ "fieldtype": "Select",
+ "label": "Select Transaction"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "select_doc_for_series",
- "fieldname": "help_html",
- "fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Help HTML",
- "length": 0,
- "no_copy": 0,
- "options": "
.YYYY. - Year in 4 digits.YY. - Year in 2 digits.MM. - Month.DD. - Day of month.WW. - Week of the year.FY. - Fiscal Year.{fieldname}. - fieldname on the document e.g.\n branch\n {{ supplier_salutation if supplier_salutation else ''}} {{ supplier_name }},
{{ message }}
-{{_("The Request for Quotation can be accessed by clicking on the following button")}}:
-- -
{{_("Regards")}},
-{{ user_fullname }}
{{_("Please click on the following button to set your new password")}}:
-- -
- + + {{_("Set Password") }} + +
+ {{_("Regards")}},
+ {{ user_fullname }}
+