Merge branch 'develop' into fix-rfq-template
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index d28c3a8..1451189 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -94,7 +94,7 @@
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 a3a7be2..a10a810 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -95,7 +95,7 @@
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 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_ledger_entry/__init__.py
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 0000000..5a7be8e
--- /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 0000000..d961076
--- /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 0000000..43e19f4
--- /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 0000000..a71b19e
--- /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/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 94246e1..9649f80 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -96,6 +96,7 @@
)
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/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index a1d86e2..e6da666 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -1418,7 +1418,12 @@
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/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index f0880c1..a580d45 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -396,7 +396,12 @@
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/general_ledger.py b/erpnext/accounts/general_ledger.py
index 1598d91..b0513f1 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -14,6 +14,7 @@
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 @@
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 @@
).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/utils.py b/erpnext/accounts/utils.py
index 405922e..1869cc7 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -7,7 +7,7 @@
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 @@
# 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 @@
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 9189f18..44426ba 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -323,6 +323,7 @@
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/hooks.py b/erpnext/hooks.py
index 813ac17..1c4bbbc 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -487,6 +487,7 @@
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 89d86c1..589763c 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.py
@@ -105,7 +105,7 @@
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/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py
index 0f655e3..7c0f0db 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.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 @@
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 @@
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 83eb969..d06b6a3 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 @@
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 @@
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 @@
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/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 8a7634e..3d96f9c 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -499,15 +499,11 @@
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 3406215..0a8ae7b 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",
@@ -224,15 +223,6 @@
"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",
"hidden": 1,
@@ -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 a98fc94..b13e4e0 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -866,6 +866,7 @@
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 4647ddf..25a03ea 100644
--- a/erpnext/manufacturing/doctype/job_card/test_job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py
@@ -1,15 +1,24 @@
# 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 (
+ 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 +26,36 @@
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 +69,6 @@
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 +104,11 @@
)
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 +137,10 @@
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)
@@ -167,22 +167,21 @@
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"
- 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.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 = 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)
+ 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
@@ -195,7 +194,7 @@
# Check if 'For Quantity' is negative
# as 'transferred_qty' > Qty to Manufacture
- transfer_entry_3 = make_stock_entry_from_jc(job_card_name)
+ transfer_entry_3 = make_stock_entry_from_jc(job_card.name)
self.assertEqual(transfer_entry_3.fg_completed_qty, 0)
job_card.append(
@@ -210,17 +209,15 @@
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 +229,7 @@
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 +274,49 @@
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 ac2f61c..2aa31be 100644
--- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
+++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
@@ -21,7 +21,7 @@
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 @@
"qty": item.qty * qty,
"uom": item.uom,
"description": item.description,
- "scrap": item.scrap,
}
)
if item.bom_no:
@@ -64,5 +63,4 @@
"fieldname": "description",
"width": 150,
},
- {"label": _("Scrap"), "fieldtype": "data", "fieldname": "scrap", "width": 100},
]
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 0000000..c2267aa
--- /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/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index b463213..7522e92 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -232,7 +232,7 @@
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 cb22fb6..91f4a5e 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 @@
.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/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 890ac47..5c35ed6 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -298,19 +298,17 @@
for_update=True,
)
- for f in (
- "uom",
- "stock_uom",
- "description",
- "item_name",
- "expense_account",
- "cost_center",
- "conversion_factor",
- ):
- if f == "stock_uom" or not item.get(f):
- item.set(f, item_details.get(f))
- if f == "conversion_factor" and item.uom == item_details.get("stock_uom"):
- item.set(f, item_details.get(f))
+ reset_fields = ("stock_uom", "item_name")
+ for field in reset_fields:
+ item.set(field, item_details.get(field))
+
+ update_fields = ("uom", "description", "expense_account", "cost_center", "conversion_factor")
+
+ for field in update_fields:
+ if not item.get(field):
+ item.set(field, item_details.get(field))
+ if field == "conversion_factor" and item.uom == item_details.get("stock_uom"):
+ item.set(field, item_details.get(field))
if not item.transfer_qty and item.qty:
item.transfer_qty = flt(
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 71baf9f..6f4c910 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -2,8 +2,6 @@
# License: GNU General Public License v3. See license.txt
-import unittest
-
import frappe
from frappe.permissions import add_user_permission, remove_user_permission
from frappe.tests.utils import FrappeTestCase, change_settings
@@ -12,6 +10,7 @@
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.stock.doctype.item.test_item import (
create_item,
+ make_item,
make_item_variant,
set_item_variant_settings,
)
@@ -1443,6 +1442,21 @@
self.assertEqual(mapped_se.items[0].basic_rate, 100)
self.assertEqual(mapped_se.items[0].basic_amount, 200)
+ def test_stock_entry_item_details(self):
+ item = make_item()
+
+ se = make_stock_entry(
+ item_code=item.name, qty=1, to_warehouse="_Test Warehouse - _TC", do_not_submit=True
+ )
+
+ self.assertEqual(se.items[0].item_name, item.item_name)
+ se.items[0].item_name = "wat"
+ se.items[0].stock_uom = "Kg"
+ se.save()
+
+ self.assertEqual(se.items[0].item_name, item.item_name)
+ self.assertEqual(se.items[0].stock_uom, item.stock_uom)
+
def make_serialized_item(**args):
args = frappe._dict(args)