Merge branch 'remove-india' of github.com:resilient-tech/erpnext into remove-india
diff --git a/.mergify.yml b/.mergify.yml
index f3d0409..b7d1df4 100644
--- a/.mergify.yml
+++ b/.mergify.yml
@@ -14,9 +14,39 @@
close:
comment:
message: |
- @{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
+ @{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
+ - name: backport to develop
+ conditions:
+ - label="backport develop"
+ actions:
+ backport:
+ branches:
+ - develop
+ assignees:
+ - "{{ author }}"
+
+ - name: backport to version-14-hotfix
+ conditions:
+ - label="backport version-14-hotfix"
+ actions:
+ backport:
+ branches:
+ - version-14-hotfix
+ assignees:
+ - "{{ author }}"
+
+ - name: backport to version-14-pre-release
+ conditions:
+ - label="backport version-14-pre-release"
+ actions:
+ backport:
+ branches:
+ - version-14-pre-release
+ assignees:
+ - "{{ author }}"
+
- name: backport to version-13-hotfix
conditions:
- label="backport version-13-hotfix"
@@ -55,4 +85,4 @@
branches:
- version-12-pre-release
assignees:
- - "{{ author }}"
\ No newline at end of file
+ - "{{ author }}"
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index 4211bd0..f3351dd 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -7,6 +7,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
+from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt
from erpnext import get_company_currency
@@ -275,6 +276,10 @@
}
matching_vouchers = []
+
+ matching_vouchers.extend(get_loan_vouchers(bank_account, transaction,
+ document_types, filters))
+
for query in subquery:
matching_vouchers.extend(
frappe.db.sql(query, filters,)
@@ -311,6 +316,114 @@
return queries
+def get_loan_vouchers(bank_account, transaction, document_types, filters):
+ vouchers = []
+ amount_condition = True if "exact_match" in document_types else False
+
+ if transaction.withdrawal > 0 and "loan_disbursement" in document_types:
+ vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters))
+
+ if transaction.deposit > 0 and "loan_repayment" in document_types:
+ vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters))
+
+ return vouchers
+
+def get_ld_matching_query(bank_account, amount_condition, filters):
+ loan_disbursement = frappe.qb.DocType("Loan Disbursement")
+ matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
+ matching_party = loan_disbursement.applicant_type == filters.get("party_type") and \
+ loan_disbursement.applicant == filters.get("party")
+
+ rank = (
+ frappe.qb.terms.Case()
+ .when(matching_reference, 1)
+ .else_(0)
+ )
+
+ rank1 = (
+ frappe.qb.terms.Case()
+ .when(matching_party, 1)
+ .else_(0)
+ )
+
+ query = frappe.qb.from_(loan_disbursement).select(
+ rank + rank1 + 1,
+ ConstantColumn("Loan Disbursement").as_("doctype"),
+ loan_disbursement.name,
+ loan_disbursement.disbursed_amount,
+ loan_disbursement.reference_number,
+ loan_disbursement.reference_date,
+ loan_disbursement.applicant_type,
+ loan_disbursement.disbursement_date
+ ).where(
+ loan_disbursement.docstatus == 1
+ ).where(
+ loan_disbursement.clearance_date.isnull()
+ ).where(
+ loan_disbursement.disbursement_account == bank_account
+ )
+
+ if amount_condition:
+ query.where(
+ loan_disbursement.disbursed_amount == filters.get('amount')
+ )
+ else:
+ query.where(
+ loan_disbursement.disbursed_amount <= filters.get('amount')
+ )
+
+ vouchers = query.run(as_list=True)
+
+ return vouchers
+
+def get_lr_matching_query(bank_account, amount_condition, filters):
+ loan_repayment = frappe.qb.DocType("Loan Repayment")
+ matching_reference = loan_repayment.reference_number == filters.get("reference_number")
+ matching_party = loan_repayment.applicant_type == filters.get("party_type") and \
+ loan_repayment.applicant == filters.get("party")
+
+ rank = (
+ frappe.qb.terms.Case()
+ .when(matching_reference, 1)
+ .else_(0)
+ )
+
+ rank1 = (
+ frappe.qb.terms.Case()
+ .when(matching_party, 1)
+ .else_(0)
+ )
+
+ query = frappe.qb.from_(loan_repayment).select(
+ rank + rank1 + 1,
+ ConstantColumn("Loan Repayment").as_("doctype"),
+ loan_repayment.name,
+ loan_repayment.amount_paid,
+ loan_repayment.reference_number,
+ loan_repayment.reference_date,
+ loan_repayment.applicant_type,
+ loan_repayment.posting_date
+ ).where(
+ loan_repayment.docstatus == 1
+ ).where(
+ loan_repayment.clearance_date.isnull()
+ ).where(
+ loan_repayment.payment_account == bank_account
+ )
+
+ if amount_condition:
+ query.where(
+ loan_repayment.amount_paid == filters.get('amount')
+ )
+ else:
+ query.where(
+ loan_repayment.amount_paid <= filters.get('amount')
+ )
+
+ vouchers = query.run()
+
+ return vouchers
+
def get_pe_matching_query(amount_condition, account_from_to, transaction):
# get matching payment entries query
if transaction.deposit > 0:
@@ -348,7 +461,6 @@
# We have mapping at the bank level
# So one bank could have both types of bank accounts like asset and liability
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
- company_account = frappe.get_value("Bank Account", transaction.bank_account, "account")
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
return f"""
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 51e1d6e..a476cab 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -49,7 +49,8 @@
def clear_linked_payment_entries(self, for_cancel=False):
for payment_entry in self.payment_entries:
- if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]:
+ if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim", "Loan Repayment",
+ "Loan Disbursement"]:
self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
elif payment_entry.payment_document == "Sales Invoice":
@@ -116,11 +117,18 @@
payment_entry.payment_entry, paid_amount_field)
elif payment_entry.payment_document == "Journal Entry":
- return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, "sum(credit_in_account_currency)")
+ return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account},
+ "sum(credit_in_account_currency)")
elif payment_entry.payment_document == "Expense Claim":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed")
+ elif payment_entry.payment_document == "Loan Disbursement":
+ return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "disbursed_amount")
+
+ elif payment_entry.payment_document == "Loan Repayment":
+ return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "amount_paid")
+
else:
frappe.throw("Please reconcile {0}: {1} manually".format(payment_entry.payment_document, payment_entry.payment_entry))
diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
index 72b6893..d84b8e0 100644
--- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
@@ -109,7 +109,7 @@
frappe.get_doc({
"doctype": "Bank",
"bank_name":bank_name,
- }).insert()
+ }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
@@ -119,7 +119,7 @@
"account_name":"Checking Account",
"bank": bank_name,
"account": account_name
- }).insert()
+ }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
@@ -184,7 +184,7 @@
"supplier_group":"All Supplier Groups",
"supplier_type": "Company",
"supplier_name": "Conrad Electronic"
- }).insert()
+ }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
@@ -203,7 +203,7 @@
"supplier_group":"All Supplier Groups",
"supplier_type": "Company",
"supplier_name": "Mr G"
- }).insert()
+ }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
@@ -227,7 +227,7 @@
"supplier_group":"All Supplier Groups",
"supplier_type": "Company",
"supplier_name": "Poore Simon's"
- }).insert()
+ }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
@@ -237,7 +237,7 @@
"customer_group":"All Customer Groups",
"customer_type": "Company",
"customer_name": "Poore Simon's"
- }).insert()
+ }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
@@ -266,7 +266,7 @@
"customer_group":"All Customer Groups",
"customer_type": "Company",
"customer_name": "Fayva"
- }).insert()
+ }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
diff --git a/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json b/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json
index 22cf797..02c6875 100644
--- a/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json
+++ b/erpnext/accounts/doctype/cash_flow_mapping_template_details/cash_flow_mapping_template_details.json
@@ -1,94 +1,34 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:mapping",
- "beta": 0,
- "creation": "2018-02-08 10:18:48.513608",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2018-02-08 10:18:48.513608",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "mapping"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "mapping",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Mapping",
- "length": 0,
- "no_copy": 0,
- "options": "Cash Flow Mapping",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "mapping",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Mapping",
+ "options": "Cash Flow Mapping",
+ "reqd": 1,
+ "unique": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-02-08 10:33:39.413930",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Cash Flow Mapping Template Details",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2022-02-21 03:34:57.902332",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Cash Flow Mapping Template Details",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
index ade7f81..6e7b80e 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
@@ -166,7 +166,7 @@
frappe.scrub(row.party_type): row.party,
"is_pos": 0,
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
- "update_stock": 0,
+ "update_stock": 0, # important: https://github.com/frappe/erpnext/pull/23559
"invoice_number": row.invoice_number,
"disable_rounded_total": 1
})
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
index 6700e9b..3eaf6a2 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
@@ -1,11 +1,7 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-import unittest
-
import frappe
-from frappe.cache_manager import clear_doctype_cache
-from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
@@ -14,14 +10,17 @@
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
get_temporary_opening_account,
)
+from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
-class TestOpeningInvoiceCreationTool(unittest.TestCase):
- def setUp(self):
+class TestOpeningInvoiceCreationTool(ERPNextTestCase):
+ @classmethod
+ def setUpClass(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company()
create_dimension()
+ return super().setUpClass()
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None):
doc = frappe.get_single("Opening Invoice Creation Tool")
@@ -31,26 +30,20 @@
return doc.make_invoices()
def test_opening_sales_invoice_creation(self):
- property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check")
- try:
- invoices = self.make_invoices(company="_Test Opening Invoice Company")
+ invoices = self.make_invoices(company="_Test Opening Invoice Company")
- self.assertEqual(len(invoices), 2)
- expected_value = {
- "keys": ["customer", "outstanding_amount", "status"],
- 0: ["_Test Customer", 300, "Overdue"],
- 1: ["_Test Customer 1", 250, "Overdue"],
- }
- self.check_expected_values(invoices, expected_value)
+ self.assertEqual(len(invoices), 2)
+ expected_value = {
+ "keys": ["customer", "outstanding_amount", "status"],
+ 0: ["_Test Customer", 300, "Overdue"],
+ 1: ["_Test Customer 1", 250, "Overdue"],
+ }
+ self.check_expected_values(invoices, expected_value)
- si = frappe.get_doc("Sales Invoice", invoices[0])
+ si = frappe.get_doc("Sales Invoice", invoices[0])
- # Check if update stock is not enabled
- self.assertEqual(si.update_stock, 0)
-
- finally:
- property_setter.delete()
- clear_doctype_cache("Sales Invoice")
+ # Check if update stock is not enabled
+ self.assertEqual(si.update_stock, 0)
def check_expected_values(self, invoices, expected_value, invoice_type="Sales"):
doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice"
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 02a144d..0d8f079 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -1077,7 +1077,7 @@
if d.voucher_type in ("Purchase Invoice"):
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
- # Get all SO / PO which are not fully billed or aginst which full advance not paid
+ # Get all SO / PO which are not fully billed or against which full advance not paid
orders_to_be_billed = []
if (args.get("party_type") != "Student"):
orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"),
diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
index b590944..1d30934 100644
--- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
+++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
@@ -46,7 +46,7 @@
for tax in doc.get("taxes"):
validate_taxes_and_charges(tax)
- validate_account_head(tax, doc)
+ validate_account_head(tax.idx, tax.account_head, doc.company)
validate_cost_center(tax, doc)
validate_inclusive_tax(tax, doc)
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index c13bc23..d6f6c5b 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -307,7 +307,7 @@
.format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency)
def validate_party_accounts(doc):
-
+ from erpnext.controllers.accounts_controller import validate_account_head
companies = []
for account in doc.get("accounts"):
@@ -330,6 +330,9 @@
if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency:
frappe.throw(_("Billing currency must be equal to either default company's currency or party account currency"))
+ # validate if account is mapped for same company
+ validate_account_head(account.idx, account.account, account.company)
+
@frappe.whitelist()
def get_due_date(posting_date, party_type, party, company=None, bill_date=None):
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 6c401fb..b72d266 100644
--- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py
+++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py
@@ -4,7 +4,12 @@
import frappe
from frappe import _
-from frappe.utils import flt, getdate, nowdate
+from frappe.query_builder.custom import ConstantColumn
+from frappe.query_builder.functions import Sum
+from frappe.utils import flt, getdate
+from pypika import CustomFunction
+
+from erpnext.accounts.utils import get_balance_on
def execute(filters=None):
@@ -18,7 +23,6 @@
data = get_entries(filters)
- from erpnext.accounts.utils import get_balance_on
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
total_debit, total_credit = 0,0
@@ -118,7 +122,21 @@
]
def get_entries(filters):
- journal_entries = frappe.db.sql("""
+ journal_entries = get_journal_entries(filters)
+
+ payment_entries = get_payment_entries(filters)
+
+ loan_entries = get_loan_entries(filters)
+
+ pos_entries = []
+ if filters.include_pos_transactions:
+ pos_entries = get_pos_entries(filters)
+
+ return sorted(list(payment_entries)+list(journal_entries+list(pos_entries) + list(loan_entries)),
+ key=lambda k: getdate(k['posting_date']))
+
+def get_journal_entries(filters):
+ return frappe.db.sql("""
select "Journal Entry" as payment_document, jv.posting_date,
jv.name as payment_entry, jvd.debit_in_account_currency as debit,
jvd.credit_in_account_currency as credit, jvd.against_account,
@@ -130,7 +148,8 @@
and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s
and ifnull(jv.is_opening, 'No') = 'No'""", filters, as_dict=1)
- payment_entries = frappe.db.sql("""
+def get_payment_entries(filters):
+ return frappe.db.sql("""
select
"Payment Entry" as payment_document, name as payment_entry,
reference_no, reference_date as ref_date,
@@ -145,9 +164,8 @@
and ifnull(clearance_date, '4000-01-01') > %(report_date)s
""", filters, as_dict=1)
- pos_entries = []
- if filters.include_pos_transactions:
- pos_entries = frappe.db.sql("""
+def get_pos_entries(filters):
+ return frappe.db.sql("""
select
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
si.posting_date, si.debit_to as against_account, sip.clearance_date,
@@ -161,8 +179,42 @@
si.posting_date ASC, si.name DESC
""", filters, as_dict=1)
- return sorted(list(payment_entries)+list(journal_entries+list(pos_entries)),
- key=lambda k: k['posting_date'] or getdate(nowdate()))
+def get_loan_entries(filters):
+ loan_docs = []
+ for doctype in ["Loan Disbursement", "Loan Repayment"]:
+ loan_doc = frappe.qb.DocType(doctype)
+ ifnull = CustomFunction('IFNULL', ['value', 'default'])
+
+ if doctype == "Loan Disbursement":
+ amount_field = (loan_doc.disbursed_amount).as_("credit")
+ posting_date = (loan_doc.disbursement_date).as_("posting_date")
+ account = loan_doc.disbursement_account
+ else:
+ amount_field = (loan_doc.amount_paid).as_("debit")
+ posting_date = (loan_doc.posting_date).as_("posting_date")
+ account = loan_doc.payment_account
+
+ entries = frappe.qb.from_(loan_doc).select(
+ ConstantColumn(doctype).as_("payment_document"),
+ (loan_doc.name).as_("payment_entry"),
+ (loan_doc.reference_number).as_("reference_no"),
+ (loan_doc.reference_date).as_("ref_date"),
+ amount_field,
+ posting_date,
+ ).where(
+ loan_doc.docstatus == 1
+ ).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'))
+ ).run(as_dict=1)
+
+ loan_docs.extend(entries)
+
+ return loan_docs
+
def get_amounts_not_reflected_in_system(filters):
je_amount = frappe.db.sql("""
@@ -182,7 +234,40 @@
pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0
- return je_amount + pe_amount
+ loan_amount = get_loan_amount(filters)
+
+ return je_amount + pe_amount + loan_amount
+
+def get_loan_amount(filters):
+ total_amount = 0
+ for doctype in ["Loan Disbursement", "Loan Repayment"]:
+ loan_doc = frappe.qb.DocType(doctype)
+ ifnull = CustomFunction('IFNULL', ['value', 'default'])
+
+ if doctype == "Loan Disbursement":
+ amount_field = Sum(loan_doc.disbursed_amount)
+ posting_date = (loan_doc.disbursement_date).as_("posting_date")
+ account = loan_doc.disbursement_account
+ else:
+ amount_field = Sum(loan_doc.amount_paid)
+ posting_date = (loan_doc.posting_date).as_("posting_date")
+ account = loan_doc.payment_account
+
+ amount = frappe.qb.from_(loan_doc).select(
+ amount_field
+ ).where(
+ loan_doc.docstatus == 1
+ ).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'))
+ ).run()[0][0]
+
+ total_amount += flt(amount)
+
+ return amount
def get_balance_row(label, amount, account_currency):
if amount > 0:
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index 758e3e9..1e20f7b 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -354,9 +354,6 @@
if d.parent_account:
account = d.parent_account_name
- # if not accounts_by_name.get(account):
- # continue
-
for company in companies:
accounts_by_name[account][company] = \
accounts_by_name[account].get(company, 0.0) + d.get(company, 0.0)
@@ -367,7 +364,7 @@
accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0)
def get_account_heads(root_type, companies, filters):
- accounts = get_accounts(root_type, filters)
+ accounts = get_accounts(root_type, companies)
if not accounts:
return None, None, None
@@ -396,7 +393,7 @@
for account in accounts:
if account.parent_account:
- account["parent_account_name"] = name_to_account_map[account.parent_account]
+ account["parent_account_name"] = name_to_account_map.get(account.parent_account)
return accounts
@@ -419,12 +416,19 @@
return frappe.db.sql_list("""select name from `tabCompany`
where lft >= {0} and rgt <= {1} order by lft, rgt""".format(lft, rgt))
-def get_accounts(root_type, filters):
- return frappe.db.sql(""" select name, is_group, company,
- parent_account, lft, rgt, root_type, report_type, account_name, account_number
- from
- `tabAccount` where company = %s and root_type = %s
- """ , (filters.get('company'), root_type), as_dict=1)
+def get_accounts(root_type, companies):
+ accounts = []
+ added_accounts = []
+
+ for company in companies:
+ for account in frappe.get_all("Account", fields=["name", "is_group", "company",
+ "parent_account", "lft", "rgt", "root_type", "report_type", "account_name", "account_number"],
+ filters={"company": company, "root_type": root_type}):
+ if account.account_name not in added_accounts:
+ accounts.append(account)
+ added_accounts.append(account.account_name)
+
+ return accounts
def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters):
data = []
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js
index 2ba649d..158ff4d 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.js
+++ b/erpnext/accounts/report/gross_profit/gross_profit.js
@@ -8,20 +8,22 @@
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
- "reqd": 1,
- "default": frappe.defaults.get_user_default("Company")
+ "default": frappe.defaults.get_user_default("Company"),
+ "reqd": 1
},
{
"fieldname":"from_date",
"label": __("From Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_start_date")
+ "default": frappe.defaults.get_user_default("year_start_date"),
+ "reqd": 1
},
{
"fieldname":"to_date",
"label": __("To Date"),
"fieldtype": "Date",
- "default": frappe.defaults.get_user_default("year_end_date")
+ "default": frappe.defaults.get_user_default("year_end_date"),
+ "reqd": 1
},
{
"fieldname":"sales_invoice",
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.json b/erpnext/accounts/report/gross_profit/gross_profit.json
index 76c560a..0730ffd 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.json
+++ b/erpnext/accounts/report/gross_profit/gross_profit.json
@@ -1,5 +1,5 @@
{
- "add_total_row": 0,
+ "add_total_row": 1,
"columns": [],
"creation": "2013-02-25 17:03:34",
"disable_prepared_report": 0,
@@ -9,7 +9,7 @@
"filters": [],
"idx": 3,
"is_standard": "Yes",
- "modified": "2021-11-13 19:14:23.730198",
+ "modified": "2022-02-11 10:18:36.956558",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Gross Profit",
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index 84effc0..b03bb9b 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -70,43 +70,42 @@
data.append(row)
def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data):
- for idx, src in enumerate(gross_profit_data.grouped_data):
+ for src in gross_profit_data.grouped_data:
row = []
for col in group_wise_columns.get(scrub(filters.group_by)):
row.append(src.get(col))
row.append(filters.currency)
- if idx == len(gross_profit_data.grouped_data)-1:
- row[0] = "Total"
data.append(row)
def get_columns(group_wise_columns, filters):
columns = []
column_map = frappe._dict({
- "parent": _("Sales Invoice") + ":Link/Sales Invoice:120",
- "invoice_or_item": _("Sales Invoice") + ":Link/Sales Invoice:120",
- "posting_date": _("Posting Date") + ":Date:100",
- "posting_time": _("Posting Time") + ":Data:100",
- "item_code": _("Item Code") + ":Link/Item:100",
- "item_name": _("Item Name") + ":Data:100",
- "item_group": _("Item Group") + ":Link/Item Group:100",
- "brand": _("Brand") + ":Link/Brand:100",
- "description": _("Description") +":Data:100",
- "warehouse": _("Warehouse") + ":Link/Warehouse:100",
- "qty": _("Qty") + ":Float:80",
- "base_rate": _("Avg. Selling Rate") + ":Currency/currency:100",
- "buying_rate": _("Valuation Rate") + ":Currency/currency:100",
- "base_amount": _("Selling Amount") + ":Currency/currency:100",
- "buying_amount": _("Buying Amount") + ":Currency/currency:100",
- "gross_profit": _("Gross Profit") + ":Currency/currency:100",
- "gross_profit_percent": _("Gross Profit %") + ":Percent:100",
- "project": _("Project") + ":Link/Project:100",
- "sales_person": _("Sales person"),
- "allocated_amount": _("Allocated Amount") + ":Currency/currency:100",
- "customer": _("Customer") + ":Link/Customer:100",
- "customer_group": _("Customer Group") + ":Link/Customer Group:100",
- "territory": _("Territory") + ":Link/Territory:100"
+ "parent": {"label": _('Sales Invoice'), "fieldname": "parent_invoice", "fieldtype": "Link", "options": "Sales Invoice", "width": 120},
+ "invoice_or_item": {"label": _('Sales Invoice'), "fieldtype": "Link", "options": "Sales Invoice", "width": 120},
+ "posting_date": {"label": _('Posting Date'), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
+ "posting_time": {"label": _('Posting Time'), "fieldname": "posting_time", "fieldtype": "Data", "width": 100},
+ "item_code": {"label": _('Item Code'), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100},
+ "item_name": {"label": _('Item Name'), "fieldname": "item_name", "fieldtype": "Data", "width": 100},
+ "item_group": {"label": _('Item Group'), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100},
+ "brand": {"label": _('Brand'), "fieldtype": "Link", "options": "Brand", "width": 100},
+ "description": {"label": _('Description'), "fieldname": "description", "fieldtype": "Data", "width": 100},
+ "warehouse": {"label": _('Warehouse'), "fieldname": "warehouse", "fieldtype": "Link", "options": "warehouse", "width": 100},
+ "qty": {"label": _('Qty'), "fieldname": "qty", "fieldtype": "Float", "width": 80},
+ "base_rate": {"label": _('Avg. Selling Rate'), "fieldname": "avg._selling_rate", "fieldtype": "Currency", "options": "currency", "width": 100},
+ "buying_rate": {"label": _('Valuation Rate'), "fieldname": "valuation_rate", "fieldtype": "Currency", "options": "currency", "width": 100},
+ "base_amount": {"label": _('Selling Amount'), "fieldname": "selling_amount", "fieldtype": "Currency", "options": "currency", "width": 100},
+ "buying_amount": {"label": _('Buying Amount'), "fieldname": "buying_amount", "fieldtype": "Currency", "options": "currency", "width": 100},
+ "gross_profit": {"label": _('Gross Profit'), "fieldname": "gross_profit", "fieldtype": "Currency", "options": "currency", "width": 100},
+ "gross_profit_percent": {"label": _('Gross Profit Percent'), "fieldname": "gross_profit_%",
+ "fieldtype": "Percent", "width": 100},
+ "project": {"label": _('Project'), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100},
+ "sales_person": {"label": _('Sales Person'), "fieldname": "sales_person", "fieldtype": "Data","width": 100},
+ "allocated_amount": {"label": _('Allocated Amount'), "fieldname": "allocated_amount", "fieldtype": "Currency", "options": "currency", "width": 100},
+ "customer": {"label": _('Customer'), "fieldname": "customer", "fieldtype": "Link", "options": "Customer", "width": 100},
+ "customer_group": {"label": _('Customer Group'), "fieldname": "customer_group", "fieldtype": "Link", "options": "customer", "width": 100},
+ "territory": {"label": _('Territory'), "fieldname": "territory", "fieldtype": "Link", "options": "territory", "width": 100},
})
for col in group_wise_columns.get(scrub(filters.group_by)):
@@ -173,7 +172,7 @@
buying_amount = 0
for row in reversed(self.si_list):
- if self.skip_row(row, self.product_bundles):
+ if self.skip_row(row):
continue
row.base_amount = flt(row.base_net_amount, self.currency_precision)
@@ -223,16 +222,6 @@
self.get_average_rate_based_on_group_by()
def get_average_rate_based_on_group_by(self):
- # sum buying / selling totals for group
- self.totals = frappe._dict(
- qty=0,
- base_amount=0,
- buying_amount=0,
- gross_profit=0,
- gross_profit_percent=0,
- base_rate=0,
- buying_rate=0
- )
for key in list(self.grouped):
if self.filters.get("group_by") != "Invoice":
for i, row in enumerate(self.grouped[key]):
@@ -244,7 +233,6 @@
new_row.base_amount += flt(row.base_amount, self.currency_precision)
new_row = self.set_average_rate(new_row)
self.grouped_data.append(new_row)
- self.add_to_totals(new_row)
else:
for i, row in enumerate(self.grouped[key]):
if row.indent == 1.0:
@@ -258,17 +246,6 @@
if (flt(row.qty) or row.base_amount):
row = self.set_average_rate(row)
self.grouped_data.append(row)
- self.add_to_totals(row)
-
- self.set_average_gross_profit(self.totals)
-
- if self.filters.get("group_by") == "Invoice":
- self.totals.indent = 0.0
- self.totals.parent_invoice = ""
- self.totals.invoice_or_item = "Total"
- self.si_list.append(self.totals)
- else:
- self.grouped_data.append(self.totals)
def is_not_invoice_row(self, row):
return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get("group_by") != "Invoice"
@@ -284,11 +261,6 @@
new_row.gross_profit_percent = flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) \
if new_row.base_amount else 0
- def add_to_totals(self, new_row):
- for key in self.totals:
- if new_row.get(key):
- self.totals[key] += new_row[key]
-
def get_returned_invoice_items(self):
returned_invoices = frappe.db.sql("""
select
@@ -306,12 +278,12 @@
self.returned_invoices.setdefault(inv.return_against, frappe._dict())\
.setdefault(inv.item_code, []).append(inv)
- def skip_row(self, row, product_bundles):
+ def skip_row(self, row):
if self.filters.get("group_by") != "Invoice":
if not row.get(scrub(self.filters.get("group_by", ""))):
return True
- elif row.get("is_return") == 1:
- return True
+
+ return False
def get_buying_amount_from_product_bundle(self, row, product_bundle):
buying_amount = 0.0
@@ -369,20 +341,37 @@
return self.average_buying_rate[item_code]
def get_last_purchase_rate(self, item_code, row):
- condition = ''
- if row.project:
- condition += " AND a.project=%s" % (frappe.db.escape(row.project))
- elif row.cost_center:
- condition += " AND a.cost_center=%s" % (frappe.db.escape(row.cost_center))
- if self.filters.to_date:
- condition += " AND modified='%s'" % (self.filters.to_date)
+ purchase_invoice = frappe.qb.DocType("Purchase Invoice")
+ purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
- last_purchase_rate = frappe.db.sql("""
- select (a.base_rate / a.conversion_factor)
- from `tabPurchase Invoice Item` a
- where a.item_code = %s and a.docstatus=1
- {0}
- order by a.modified desc limit 1""".format(condition), item_code)
+ query = (frappe.qb.from_(purchase_invoice_item)
+ .inner_join(
+ purchase_invoice
+ ).on(
+ purchase_invoice.name == purchase_invoice_item.parent
+ ).select(
+ purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor
+ ).where(
+ purchase_invoice.docstatus == 1
+ ).where(
+ purchase_invoice.posting_date <= self.filters.to_date
+ ).where(
+ purchase_invoice_item.item_code == item_code
+ ))
+
+ if row.project:
+ query.where(
+ purchase_invoice_item.project == row.project
+ )
+
+ if row.cost_center:
+ query.where(
+ purchase_invoice_item.cost_center == row.cost_center
+ )
+
+ query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc)
+ query.limit(1)
+ last_purchase_rate = query.run()
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0
diff --git a/erpnext/accounts/report/tax_detail/test_tax_detail.py b/erpnext/accounts/report/tax_detail/test_tax_detail.py
index bf668ab..621de82 100644
--- a/erpnext/accounts/report/tax_detail/test_tax_detail.py
+++ b/erpnext/accounts/report/tax_detail/test_tax_detail.py
@@ -61,7 +61,7 @@
# Create GL Entries:
db_doc.submit()
else:
- db_doc.insert()
+ db_doc.insert(ignore_if_duplicate=True)
except frappe.exceptions.DuplicateEntryError:
pass
diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
index 57f7974..e6cbff5 100644
--- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
+++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
@@ -43,7 +43,7 @@
if entry.account in tds_accounts:
tds_deducted += (entry.credit - entry.debit)
- total_amount_credited += (entry.credit - entry.debit)
+ total_amount_credited += entry.credit
if tds_deducted:
row = {
diff --git a/erpnext/accounts/test/test_reports.py b/erpnext/accounts/test/test_reports.py
index 78c109a..4ed966d 100644
--- a/erpnext/accounts/test/test_reports.py
+++ b/erpnext/accounts/test/test_reports.py
@@ -39,10 +39,11 @@
def test_execute_all_accounts_reports(self):
"""Test that all script report in stock modules are executable with supported filters"""
for report, filter in REPORT_FILTER_TEST_CASES:
- execute_script_report(
- report_name=report,
- module="Accounts",
- filters=filter,
- default_filters=DEFAULT_FILTERS,
- optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
- )
+ with self.subTest(report=report):
+ execute_script_report(
+ report_name=report,
+ module="Accounts",
+ filters=filter,
+ default_filters=DEFAULT_FILTERS,
+ optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
+ )
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 39e84e3..b17b90b 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -847,7 +847,7 @@
"payment_account": bank_account.name,
"currency": bank_account.account_currency,
"payment_channel": payment_channel
- }).insert(ignore_permissions=True)
+ }).insert(ignore_permissions=True, ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
# already exists, due to a reinstall?
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 6e87426..ea473fa 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -417,11 +417,12 @@
def validate_asset_finance_books(self, row):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount")
- .format(row.idx))
+ .format(row.idx), title=_("Invalid Schedule"))
if not row.depreciation_start_date:
if not self.available_for_use_date:
- frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx))
+ frappe.throw(_("Row {0}: Depreciation Start Date is required")
+ .format(row.idx), title=_("Invalid Schedule"))
row.depreciation_start_date = get_last_day(self.available_for_use_date)
if not self.is_existing_asset:
@@ -439,8 +440,9 @@
else:
self.number_of_depreciations_booked = 0
- if cint(self.number_of_depreciations_booked) > cint(row.total_number_of_depreciations):
- frappe.throw(_("Number of Depreciations Booked cannot be greater than Total Number of Depreciations"))
+ if flt(row.total_number_of_depreciations) <= cint(self.number_of_depreciations_booked):
+ frappe.throw(_("Row {0}: Total Number of Depreciations cannot be less than or equal to Number of Depreciations Booked")
+ .format(row.idx), title=_("Invalid Schedule"))
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date):
frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date")
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index e6a0ef0..f5486dc 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -834,8 +834,9 @@
self.assertRaises(frappe.ValidationError, asset.save)
def test_number_of_depreciations(self):
- """Tests if an error is raised when number_of_depreciations_booked > total_number_of_depreciations."""
+ """Tests if an error is raised when number_of_depreciations_booked >= total_number_of_depreciations."""
+ # number_of_depreciations_booked > total_number_of_depreciations
asset = create_asset(
item_code = "Macbook Pro",
calculate_depreciation = 1,
@@ -850,6 +851,21 @@
self.assertRaises(frappe.ValidationError, asset.save)
+ # number_of_depreciations_booked = total_number_of_depreciations
+ asset_2 = create_asset(
+ item_code = "Macbook Pro",
+ calculate_depreciation = 1,
+ available_for_use_date = "2019-12-31",
+ total_number_of_depreciations = 5,
+ expected_value_after_useful_life = 10000,
+ depreciation_start_date = "2020-07-01",
+ opening_accumulated_depreciation = 10000,
+ number_of_depreciations_booked = 5,
+ do_not_save = 1
+ )
+
+ self.assertRaises(frappe.ValidationError, asset_2.save)
+
def test_depreciation_start_date_is_before_purchase_date(self):
asset = create_asset(
item_code = "Macbook Pro",
@@ -1225,7 +1241,7 @@
if not args.do_not_save:
try:
- asset.save()
+ asset.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
@@ -1266,7 +1282,7 @@
"is_grouped_asset": is_grouped_asset,
"asset_naming_series": naming_series
})
- item.insert()
+ item.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
return item
diff --git a/erpnext/assets/doctype/asset_category/test_asset_category.py b/erpnext/assets/doctype/asset_category/test_asset_category.py
index 3d19fa3..2f52248 100644
--- a/erpnext/assets/doctype/asset_category/test_asset_category.py
+++ b/erpnext/assets/doctype/asset_category/test_asset_category.py
@@ -23,7 +23,7 @@
})
try:
- asset_category.insert()
+ asset_category.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py
index 13fe9df..0fb81b2 100644
--- a/erpnext/buying/doctype/supplier/test_supplier.py
+++ b/erpnext/buying/doctype/supplier/test_supplier.py
@@ -14,151 +14,150 @@
class TestSupplier(unittest.TestCase):
- def test_get_supplier_group_details(self):
- doc = frappe.new_doc("Supplier Group")
- doc.supplier_group_name = "_Testing Supplier Group"
- doc.payment_terms = "_Test Payment Term Template 3"
- doc.accounts = []
- test_account_details = {
- "company": "_Test Company",
- "account": "Creditors - _TC",
- }
- doc.append("accounts", test_account_details)
- doc.save()
- s_doc = frappe.new_doc("Supplier")
- s_doc.supplier_name = "Testing Supplier"
- s_doc.supplier_group = "_Testing Supplier Group"
- s_doc.payment_terms = ""
- s_doc.accounts = []
- s_doc.insert()
- s_doc.get_supplier_group_details()
- self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3")
- self.assertEqual(s_doc.accounts[0].company, "_Test Company")
- self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC")
- s_doc.delete()
- doc.delete()
+ def test_get_supplier_group_details(self):
+ doc = frappe.new_doc("Supplier Group")
+ doc.supplier_group_name = "_Testing Supplier Group"
+ doc.payment_terms = "_Test Payment Term Template 3"
+ doc.accounts = []
+ test_account_details = {
+ "company": "_Test Company",
+ "account": "Creditors - _TC",
+ }
+ doc.append("accounts", test_account_details)
+ doc.save()
+ s_doc = frappe.new_doc("Supplier")
+ s_doc.supplier_name = "Testing Supplier"
+ s_doc.supplier_group = "_Testing Supplier Group"
+ s_doc.payment_terms = ""
+ s_doc.accounts = []
+ s_doc.insert()
+ s_doc.get_supplier_group_details()
+ self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3")
+ self.assertEqual(s_doc.accounts[0].company, "_Test Company")
+ self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC")
+ s_doc.delete()
+ doc.delete()
- def test_supplier_default_payment_terms(self):
- # Payment Term based on Days after invoice date
- frappe.db.set_value(
- "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 3")
+ def test_supplier_default_payment_terms(self):
+ # Payment Term based on Days after invoice date
+ frappe.db.set_value(
+ "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 3")
- due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
- self.assertEqual(due_date, "2016-02-21")
+ due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
+ self.assertEqual(due_date, "2016-02-21")
- due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
- self.assertEqual(due_date, "2017-02-21")
+ due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
+ self.assertEqual(due_date, "2017-02-21")
- # Payment Term based on last day of month
- frappe.db.set_value(
- "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 1")
+ # Payment Term based on last day of month
+ frappe.db.set_value(
+ "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 1")
- due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
- self.assertEqual(due_date, "2016-02-29")
+ due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
+ self.assertEqual(due_date, "2016-02-29")
- due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
- self.assertEqual(due_date, "2017-02-28")
+ due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
+ self.assertEqual(due_date, "2017-02-28")
- frappe.db.set_value("Supplier", "_Test Supplier With Template 1", "payment_terms", "")
+ frappe.db.set_value("Supplier", "_Test Supplier With Template 1", "payment_terms", "")
- # Set credit limit for the supplier group instead of supplier and evaluate the due date
- frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 3")
+ # Set credit limit for the supplier group instead of supplier and evaluate the due date
+ frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 3")
- due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
- self.assertEqual(due_date, "2016-02-21")
+ due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
+ self.assertEqual(due_date, "2016-02-21")
- # Payment terms for Supplier Group instead of supplier and evaluate the due date
- frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 1")
+ # Payment terms for Supplier Group instead of supplier and evaluate the due date
+ frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 1")
- # Leap year
- due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
- self.assertEqual(due_date, "2016-02-29")
- # # Non Leap year
- due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
- self.assertEqual(due_date, "2017-02-28")
+ # Leap year
+ due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
+ self.assertEqual(due_date, "2016-02-29")
+ # # Non Leap year
+ due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
+ self.assertEqual(due_date, "2017-02-28")
- # Supplier with no default Payment Terms Template
- frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "")
- frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", "")
+ # Supplier with no default Payment Terms Template
+ frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "")
+ frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", "")
- due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier")
- self.assertEqual(due_date, "2016-01-22")
- # # Non Leap year
- due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier")
- self.assertEqual(due_date, "2017-01-22")
+ due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier")
+ self.assertEqual(due_date, "2016-01-22")
+ # # Non Leap year
+ due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier")
+ self.assertEqual(due_date, "2017-01-22")
- def test_supplier_disabled(self):
- make_test_records("Item")
+ def test_supplier_disabled(self):
+ make_test_records("Item")
- frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 1)
+ frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 1)
- from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
+ from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
- po = create_purchase_order(do_not_save=True)
+ po = create_purchase_order(do_not_save=True)
- self.assertRaises(PartyDisabled, po.save)
+ self.assertRaises(PartyDisabled, po.save)
- frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 0)
+ frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 0)
- po.save()
+ po.save()
- def test_supplier_country(self):
- # Test that country field exists in Supplier DocType
- supplier = frappe.get_doc('Supplier', '_Test Supplier with Country')
- self.assertTrue('country' in supplier.as_dict())
+ def test_supplier_country(self):
+ # Test that country field exists in Supplier DocType
+ supplier = frappe.get_doc('Supplier', '_Test Supplier with Country')
+ self.assertTrue('country' in supplier.as_dict())
- # Test if test supplier field record is 'Greece'
- self.assertEqual(supplier.country, "Greece")
+ # Test if test supplier field record is 'Greece'
+ self.assertEqual(supplier.country, "Greece")
- # Test update Supplier instance country value
- supplier = frappe.get_doc('Supplier', '_Test Supplier')
- supplier.country = 'Greece'
- supplier.save()
- self.assertEqual(supplier.country, "Greece")
+ # Test update Supplier instance country value
+ supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier.country = 'Greece'
+ supplier.save()
+ self.assertEqual(supplier.country, "Greece")
- def test_party_details_tax_category(self):
- from erpnext.accounts.party import get_party_details
+ def test_party_details_tax_category(self):
+ from erpnext.accounts.party import get_party_details
- frappe.delete_doc_if_exists("Address", "_Test Address With Tax Category-Billing")
+ frappe.delete_doc_if_exists("Address", "_Test Address With Tax Category-Billing")
- # Tax Category without Address
- details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier")
- self.assertEqual(details.tax_category, "_Test Tax Category 1")
+ # Tax Category without Address
+ details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier")
+ self.assertEqual(details.tax_category, "_Test Tax Category 1")
- address = frappe.get_doc(dict(
- doctype='Address',
- address_title='_Test Address With Tax Category',
- tax_category='_Test Tax Category 2',
- address_type='Billing',
- address_line1='Station Road',
- city='_Test City',
- country='India',
- links=[dict(
- link_doctype='Supplier',
- link_name='_Test Supplier With Tax Category'
- )]
- )).insert()
+ address = frappe.get_doc(dict(
+ doctype='Address',
+ address_title='_Test Address With Tax Category',
+ tax_category='_Test Tax Category 2',
+ address_type='Billing',
+ address_line1='Station Road',
+ city='_Test City',
+ country='India',
+ links=[dict(
+ link_doctype='Supplier',
+ link_name='_Test Supplier With Tax Category'
+ )]
+ )).insert()
- # Tax Category with Address
- details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier")
- self.assertEqual(details.tax_category, "_Test Tax Category 2")
+ # Tax Category with Address
+ details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier")
+ self.assertEqual(details.tax_category, "_Test Tax Category 2")
- # Rollback
- address.delete()
+ # Rollback
+ address.delete()
def create_supplier(**args):
- args = frappe._dict(args)
+ args = frappe._dict(args)
- try:
- doc = frappe.get_doc({
- "doctype": "Supplier",
- "supplier_name": args.supplier_name,
- "supplier_group": args.supplier_group or "Services",
- "supplier_type": args.supplier_type or "Company",
- "tax_withholding_category": args.tax_withholding_category
- }).insert()
+ if frappe.db.exists("Supplier", args.supplier_name):
+ return frappe.get_doc("Supplier", args.supplier_name)
- return doc
+ doc = frappe.get_doc({
+ "doctype": "Supplier",
+ "supplier_name": args.supplier_name,
+ "supplier_group": args.supplier_group or "Services",
+ "supplier_type": args.supplier_type or "Company",
+ "tax_withholding_category": args.tax_withholding_category
+ }).insert()
- except frappe.DuplicateEntryError:
- return frappe.get_doc("Supplier", args.supplier_name)
+ return doc
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index d05787f..a94af10 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1566,13 +1566,12 @@
tax.rate = None
-def validate_account_head(tax, doc):
- company = frappe.get_cached_value('Account',
- tax.account_head, 'company')
+def validate_account_head(idx, account, company):
+ account_company = frappe.get_cached_value('Account', account, 'company')
- if company != doc.company:
+ if account_company != company:
frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}')
- .format(tax.idx, frappe.bold(tax.account_head), frappe.bold(doc.company)), title=_('Invalid Account'))
+ .format(idx, frappe.bold(account), frappe.bold(company)), title=_('Invalid Account'))
def validate_cost_center(tax, doc):
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index a181af7..b740476 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -249,6 +249,7 @@
"posting_time": self.get('posting_time'),
"qty": -1 * flt(d.get('stock_qty')),
"serial_no": d.get('serial_no'),
+ "batch_no": d.get("batch_no"),
"company": self.company,
"voucher_type": self.doctype,
"voucher_no": self.name,
@@ -278,7 +279,8 @@
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * d.consumed_qty,
- "serial_no": d.serial_no
+ "serial_no": d.serial_no,
+ "batch_no": d.batch_no,
})
if rate > 0:
diff --git a/erpnext/controllers/employee_boarding_controller.py b/erpnext/controllers/employee_boarding_controller.py
index ae2c737..dd02ce1 100644
--- a/erpnext/controllers/employee_boarding_controller.py
+++ b/erpnext/controllers/employee_boarding_controller.py
@@ -104,11 +104,11 @@
def get_task_dates(self, activity, holiday_list):
start_date = end_date = None
- if activity.begin_on:
+ if activity.begin_on is not None:
start_date = add_days(self.boarding_begins_on, activity.begin_on)
start_date = self.update_if_holiday(start_date, holiday_list)
- if activity.duration:
+ if activity.duration is not None:
end_date = add_days(self.boarding_begins_on, activity.begin_on + activity.duration)
end_date = self.update_if_holiday(end_date, holiday_list)
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index df3c5f1..8c3aab4 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -420,6 +420,7 @@
"posting_time": sle.get('posting_time'),
"qty": sle.actual_qty,
"serial_no": sle.get('serial_no'),
+ "batch_no": sle.get("batch_no"),
"company": sle.company,
"voucher_type": sle.voucher_type,
"voucher_no": sle.voucher_no
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 31b2209..e918cde 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -394,6 +394,7 @@
"posting_time": self.get('posting_time') or nowtime(),
"qty": qty if cint(self.get("is_return")) else (-1 * qty),
"serial_no": d.get('serial_no'),
+ "batch_no": d.get("batch_no"),
"company": self.company,
"voucher_type": self.doctype,
"voucher_no": self.name,
diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
index 8519e68..6be8c94 100644
--- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
+++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
@@ -175,7 +175,7 @@
def create_tax_rule(self):
tax_rule = frappe.get_test_records("Tax Rule")[0]
try:
- frappe.get_doc(tax_rule).insert()
+ frappe.get_doc(tax_rule).insert(ignore_if_duplicate=True)
except (frappe.DuplicateEntryError, ConflictingTaxRule):
pass
diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
index 54ed6f7..26bd19f 100644
--- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
+++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
@@ -82,7 +82,7 @@
"is_private": True
})
try:
- f.insert()
+ f.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
setattr(self, key, f.file_url)
diff --git a/erpnext/erpnext_integrations/taxjar_integration.py b/erpnext/erpnext_integrations/taxjar_integration.py
index a4e2157..14c86d5 100644
--- a/erpnext/erpnext_integrations/taxjar_integration.py
+++ b/erpnext/erpnext_integrations/taxjar_integration.py
@@ -8,10 +8,6 @@
from erpnext import get_default_company, get_region
-TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
-SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head")
-TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
-TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
SUPPORTED_COUNTRY_CODES = ["AT", "AU", "BE", "BG", "CA", "CY", "CZ", "DE", "DK", "EE", "ES", "FI",
"FR", "GB", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT", "NL", "PL", "PT", "RO",
"SE", "SI", "SK", "US"]
@@ -35,12 +31,14 @@
if api_key and api_url:
client = taxjar.Client(api_key=api_key, api_url=api_url)
client.set_api_config('headers', {
- 'x-api-version': '2020-08-07'
+ 'x-api-version': '2022-01-24'
})
return client
def create_transaction(doc, method):
+ TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
+
"""Create an order transaction in TaxJar"""
if not TAXJAR_CREATE_TRANSACTIONS:
@@ -51,6 +49,7 @@
if not client:
return
+ TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
sales_tax = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == TAX_ACCOUNT_HEAD])
if not sales_tax:
@@ -79,6 +78,7 @@
def delete_transaction(doc, method):
"""Delete an existing TaxJar order transaction"""
+ TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
if not TAXJAR_CREATE_TRANSACTIONS:
return
@@ -92,6 +92,8 @@
def get_tax_data(doc):
+ SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head")
+
from_address = get_company_address_details(doc)
from_shipping_state = from_address.get("state")
from_country_code = frappe.db.get_value("Country", from_address.country, "code")
@@ -113,20 +115,20 @@
to_shipping_state = get_state_code(to_address, 'Shipping')
tax_dict = {
- 'from_country': from_country_code,
- 'from_zip': from_address.pincode,
- 'from_state': from_shipping_state,
- 'from_city': from_address.city,
- 'from_street': from_address.address_line1,
- 'to_country': to_country_code,
- 'to_zip': to_address.pincode,
- 'to_city': to_address.city,
- 'to_street': to_address.address_line1,
- 'to_state': to_shipping_state,
- 'shipping': shipping,
- 'amount': doc.net_total,
- 'plugin': 'erpnext',
- 'line_items': line_items
+ "from_country": from_country_code,
+ "from_zip": from_address.pincode,
+ "from_state": from_shipping_state,
+ "from_city": from_address.city,
+ "from_street": from_address.address_line1,
+ "to_country": to_country_code,
+ "to_zip": to_address.pincode,
+ "to_city": to_address.city,
+ "to_street": to_address.address_line1,
+ "to_state": to_shipping_state,
+ "shipping": shipping,
+ "amount": doc.net_total,
+ "plugin": "erpnext",
+ "line_items": line_items
}
return tax_dict
@@ -156,6 +158,9 @@
return tax_dict
def set_sales_tax(doc, method):
+ TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
+ TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
+
if not TAXJAR_CALCULATE_TAX:
return
@@ -206,6 +211,7 @@
doc.run_method("calculate_taxes_and_totals")
def check_for_nexus(doc, tax_dict):
+ TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
if not frappe.db.get_value('TaxJar Nexus', {'region_code': tax_dict["to_state"]}):
for item in doc.get("items"):
item.tax_collectable = flt(0)
@@ -218,6 +224,8 @@
def check_sales_tax_exemption(doc):
# if the party is exempt from sales tax, then set all tax account heads to zero
+ TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
+
sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \
or frappe.db.has_column("Customer", "exempt_from_sales_tax") \
and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax")
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index a2df26c..6e52eb9 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -142,7 +142,7 @@
"file_url": self.image,
"attached_to_doctype": "User",
"attached_to_name": self.user_id
- }).insert()
+ }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
# already exists
pass
diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
index 2d129c8..0fb821d 100644
--- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
+++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
@@ -4,7 +4,7 @@
import unittest
import frappe
-from frappe.utils import getdate
+from frappe.utils import add_days, getdate
from erpnext.hr.doctype.employee_onboarding.employee_onboarding import (
IncompleteTaskError,
@@ -35,6 +35,15 @@
# boarding status
self.assertEqual(onboarding.boarding_status, 'Pending')
+ # start and end dates
+ start_date, end_date = frappe.db.get_value('Task', onboarding.activities[0].task, ['exp_start_date', 'exp_end_date'])
+ self.assertEqual(getdate(start_date), getdate(onboarding.boarding_begins_on))
+ self.assertEqual(getdate(end_date), add_days(start_date, onboarding.activities[0].duration))
+
+ start_date, end_date = frappe.db.get_value('Task', onboarding.activities[1].task, ['exp_start_date', 'exp_end_date'])
+ self.assertEqual(getdate(start_date), add_days(onboarding.boarding_begins_on, onboarding.activities[0].duration))
+ self.assertEqual(getdate(end_date), add_days(start_date, onboarding.activities[1].duration))
+
# complete the task
project = frappe.get_doc('Project', onboarding.project)
for task in frappe.get_all('Task', dict(project=project.name)):
@@ -57,10 +66,7 @@
self.assertEqual(employee.employee_name, 'Test Researcher')
def tearDown(self):
- for entry in frappe.get_all('Employee Onboarding'):
- doc = frappe.get_doc('Employee Onboarding', entry.name)
- doc.cancel()
- doc.delete()
+ frappe.db.rollback()
def get_job_applicant():
@@ -87,23 +93,31 @@
def create_employee_onboarding():
applicant = get_job_applicant()
job_offer = get_job_offer(applicant.name)
- holiday_list = make_holiday_list()
+
+ holiday_list = make_holiday_list('_Test Employee Boarding')
+ holiday_list = frappe.get_doc('Holiday List', holiday_list)
+ holiday_list.holidays = []
+ holiday_list.save()
onboarding = frappe.new_doc('Employee Onboarding')
onboarding.job_applicant = applicant.name
onboarding.job_offer = job_offer.name
onboarding.date_of_joining = onboarding.boarding_begins_on = getdate()
onboarding.company = '_Test Company'
- onboarding.holiday_list = holiday_list
+ onboarding.holiday_list = holiday_list.name
onboarding.designation = 'Researcher'
onboarding.append('activities', {
'activity_name': 'Assign ID Card',
'role': 'HR User',
- 'required_for_employee_creation': 1
+ 'required_for_employee_creation': 1,
+ 'begin_on': 0,
+ 'duration': 1
})
onboarding.append('activities', {
'activity_name': 'Assign a laptop',
- 'role': 'HR User'
+ 'role': 'HR User',
+ 'begin_on': 1,
+ 'duration': 1
})
onboarding.status = 'Pending'
onboarding.insert()
diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py
index 30e19f1..59fb2fd 100644
--- a/erpnext/hr/doctype/exit_interview/exit_interview.py
+++ b/erpnext/hr/doctype/exit_interview/exit_interview.py
@@ -128,4 +128,4 @@
message += _('{0} due to missing email information for employee(s): {1}').format(
frappe.bold('Sending Failed'), ', '.join(email_failure))
- frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True)
\ No newline at end of file
+ frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True)
diff --git a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py
index acd50f2..abb2887 100644
--- a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py
+++ b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py
@@ -82,7 +82,7 @@
"vehicle_value": flt(500000)
})
try:
- vehicle.insert()
+ vehicle.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
return license_plate
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
index 7811d56..50926d7 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
@@ -14,11 +14,15 @@
"applicant",
"section_break_7",
"disbursement_date",
+ "clearance_date",
"column_break_8",
"disbursed_amount",
"accounting_dimensions_section",
"cost_center",
- "customer_details_section",
+ "accounting_details",
+ "disbursement_account",
+ "column_break_16",
+ "loan_account",
"bank_account",
"disbursement_references_section",
"reference_date",
@@ -107,11 +111,6 @@
"label": "Disbursement Details"
},
{
- "fieldname": "customer_details_section",
- "fieldtype": "Section Break",
- "label": "Customer Details"
- },
- {
"fetch_from": "against_loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
@@ -149,15 +148,48 @@
"fieldname": "reference_number",
"fieldtype": "Data",
"label": "Reference Number"
+ },
+ {
+ "fieldname": "clearance_date",
+ "fieldtype": "Date",
+ "label": "Clearance Date",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "accounting_details",
+ "fieldtype": "Section Break",
+ "label": "Accounting Details"
+ },
+ {
+ "fetch_from": "against_loan.disbursement_account",
+ "fieldname": "disbursement_account",
+ "fieldtype": "Link",
+ "label": "Disbursement Account",
+ "options": "Account",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_16",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "against_loan.loan_account",
+ "fieldname": "loan_account",
+ "fieldtype": "Link",
+ "label": "Loan Account",
+ "options": "Account",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-04-19 18:09:32.175355",
+ "modified": "2022-02-17 18:23:44.157598",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Disbursement",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -194,5 +226,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
index df3aadf..54a03b9 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
@@ -42,9 +42,6 @@
if not self.posting_date:
self.posting_date = self.disbursement_date or nowdate()
- if not self.bank_account and self.applicant_type == "Customer":
- self.bank_account = frappe.db.get_value("Customer", self.applicant, "default_bank_account")
-
def validate_disbursal_amount(self):
possible_disbursal_amount = get_disbursal_amount(self.against_loan)
@@ -117,12 +114,11 @@
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
- loan_details = frappe.get_doc("Loan", self.against_loan)
gle_map.append(
self.get_gl_dict({
- "account": loan_details.loan_account,
- "against": loan_details.disbursement_account,
+ "account": self.loan_account,
+ "against": self.disbursement_account,
"debit": self.disbursed_amount,
"debit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan",
@@ -137,8 +133,8 @@
gle_map.append(
self.get_gl_dict({
- "account": loan_details.disbursement_account,
- "against": loan_details.loan_account,
+ "account": self.disbursement_account,
+ "against": self.loan_account,
"credit": self.disbursed_amount,
"credit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan",
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
index 0de073f..1c800a0 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
@@ -74,39 +74,6 @@
})
)
- if self.payable_principal_amount:
- gle_map.append(
- self.get_gl_dict({
- "account": self.loan_account,
- "party_type": self.applicant_type,
- "party": self.applicant,
- "against": self.interest_income_account,
- "debit": self.payable_principal_amount,
- "debit_in_account_currency": self.interest_amount,
- "against_voucher_type": "Loan",
- "against_voucher": self.loan,
- "remarks": _("Interest accrued from {0} to {1} against loan: {2}").format(
- self.last_accrual_date, self.posting_date, self.loan),
- "cost_center": erpnext.get_default_cost_center(self.company),
- "posting_date": self.posting_date
- })
- )
-
- gle_map.append(
- self.get_gl_dict({
- "account": self.interest_income_account,
- "against": self.loan_account,
- "credit": self.payable_principal_amount,
- "credit_in_account_currency": self.interest_amount,
- "against_voucher_type": "Loan",
- "against_voucher": self.loan,
- "remarks": ("Interest accrued from {0} to {1} against loan: {2}").format(
- self.last_accrual_date, self.posting_date, self.loan),
- "cost_center": erpnext.get_default_cost_center(self.company),
- "posting_date": self.posting_date
- })
- )
-
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj)
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
index 93ef217..480e010 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
@@ -1,7 +1,7 @@
{
"actions": [],
"autoname": "LM-REP-.####",
- "creation": "2019-09-03 14:44:39.977266",
+ "creation": "2022-01-25 10:30:02.767941",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
@@ -13,6 +13,7 @@
"column_break_3",
"company",
"posting_date",
+ "clearance_date",
"rate_of_interest",
"payroll_payable_account",
"is_term_loan",
@@ -37,7 +38,12 @@
"total_penalty_paid",
"total_interest_paid",
"repayment_details",
- "amended_from"
+ "amended_from",
+ "accounting_details_section",
+ "payment_account",
+ "penalty_income_account",
+ "column_break_36",
+ "loan_account"
],
"fields": [
{
@@ -260,12 +266,52 @@
"fieldname": "repay_from_salary",
"fieldtype": "Check",
"label": "Repay From Salary"
+ },
+ {
+ "fieldname": "clearance_date",
+ "fieldtype": "Date",
+ "label": "Clearance Date",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "accounting_details_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Details"
+ },
+ {
+ "fetch_from": "against_loan.payment_account",
+ "fieldname": "payment_account",
+ "fieldtype": "Link",
+ "label": "Repayment Account",
+ "options": "Account",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_36",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "against_loan.loan_account",
+ "fieldname": "loan_account",
+ "fieldtype": "Link",
+ "label": "Loan Account",
+ "options": "Account",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "against_loan.penalty_income_account",
+ "fieldname": "penalty_income_account",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Penalty Income Account",
+ "options": "Account"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-01-06 01:51:06.707782",
+ "modified": "2022-02-18 19:10:07.742298",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Repayment",
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index f3ed611..67c2b1e 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -310,7 +310,6 @@
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
- loan_details = frappe.get_doc("Loan", self.against_loan)
if self.shortfall_amount and self.amount_paid > self.shortfall_amount:
remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(self.shortfall_amount,
@@ -323,13 +322,13 @@
if self.repay_from_salary:
payment_account = self.payroll_payable_account
else:
- payment_account = loan_details.payment_account
+ payment_account = self.payment_account
if self.total_penalty_paid:
gle_map.append(
self.get_gl_dict({
- "account": loan_details.loan_account,
- "against": loan_details.payment_account,
+ "account": self.loan_account,
+ "against": payment_account,
"debit": self.total_penalty_paid,
"debit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
@@ -344,8 +343,8 @@
gle_map.append(
self.get_gl_dict({
- "account": loan_details.penalty_income_account,
- "against": loan_details.loan_account,
+ "account": self.penalty_income_account,
+ "against": self.loan_account,
"credit": self.total_penalty_paid,
"credit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
@@ -359,8 +358,7 @@
gle_map.append(
self.get_gl_dict({
"account": payment_account,
- "against": loan_details.loan_account + ", " + loan_details.interest_income_account
- + ", " + loan_details.penalty_income_account,
+ "against": self.loan_account + ", " + self.penalty_income_account,
"debit": self.amount_paid,
"debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
@@ -368,16 +366,16 @@
"remarks": remarks,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date),
- "party_type": loan_details.applicant_type if self.repay_from_salary else '',
- "party": loan_details.applicant if self.repay_from_salary else ''
+ "party_type": self.applicant_type if self.repay_from_salary else '',
+ "party": self.applicant if self.repay_from_salary else ''
})
)
gle_map.append(
self.get_gl_dict({
- "account": loan_details.loan_account,
- "party_type": loan_details.applicant_type,
- "party": loan_details.applicant,
+ "account": self.loan_account,
+ "party_type": self.applicant_type,
+ "party": self.applicant,
"against": payment_account,
"credit": self.amount_paid,
"credit_in_account_currency": self.amount_paid,
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 8d00019..9f4ace2 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -62,7 +62,7 @@
if self.get('time_logs'):
for d in self.get('time_logs'):
- if get_datetime(d.from_time) > get_datetime(d.to_time):
+ if d.to_time and get_datetime(d.from_time) > get_datetime(d.to_time):
frappe.throw(_("Row {0}: From time must be less than to time").format(d.idx))
data = self.get_overlap_for(d)
diff --git a/erpnext/manufacturing/report/test_reports.py b/erpnext/manufacturing/report/test_reports.py
index 9f51ded..e436fdc 100644
--- a/erpnext/manufacturing/report/test_reports.py
+++ b/erpnext/manufacturing/report/test_reports.py
@@ -55,10 +55,11 @@
def test_execute_all_manufacturing_reports(self):
"""Test that all script report in manufacturing modules are executable with supported filters"""
for report, filter in REPORT_FILTER_TEST_CASES:
- execute_script_report(
- report_name=report,
- module="Manufacturing",
- filters=filter,
- default_filters=DEFAULT_FILTERS,
- optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
- )
+ with self.subTest(report=report):
+ execute_script_report(
+ report_name=report,
+ module="Manufacturing",
+ filters=filter,
+ default_filters=DEFAULT_FILTERS,
+ optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
+ )
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index a93ceca..1908faa 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -10,9 +10,6 @@
erpnext.patches.v5_7.update_item_description_based_on_item_master
erpnext.patches.v4_2.repost_reserved_qty #2021-03-31
execute:frappe.reload_doc("Payroll", "doctype", "salary_slip")
-erpnext.patches.v8_1.setup_gst_india #2017-06-27
-erpnext.patches.v8_1.removed_roles_from_gst_report_non_indian_account #16-08-2018
-erpnext.patches.v8_7.sync_india_custom_fields
erpnext.patches.v10_0.fichier_des_ecritures_comptables_for_france
erpnext.patches.v10_0.rename_price_to_rate_in_pricing_rule
erpnext.patches.v10_0.set_currency_in_pricing_rule
@@ -46,7 +43,6 @@
erpnext.patches.v11_0.move_item_defaults_to_child_table_for_multicompany #02-07-2018 #19-06-2019
erpnext.patches.v11_0.rename_overproduction_percent_field
erpnext.patches.v11_0.update_backflush_subcontract_rm_based_on_bom
-erpnext.patches.v11_0.inter_state_field_for_gst
erpnext.patches.v11_0.rename_members_with_naming_series #04-06-2018
erpnext.patches.v11_0.set_update_field_and_value_in_workflow_state
erpnext.patches.v11_0.update_total_qty_field
@@ -68,20 +64,15 @@
execute:frappe.delete_doc_if_exists("Page", "purchase-analytics")
execute:frappe.delete_doc_if_exists("Page", "stock-analytics")
execute:frappe.delete_doc_if_exists("Page", "production-analytics")
-erpnext.patches.v11_0.ewaybill_fields_gst_india #2018-11-13 #2019-01-09 #2019-04-01 #2019-04-26 #2019-05-03
erpnext.patches.v11_0.drop_column_max_days_allowed
erpnext.patches.v10_0.item_barcode_childtable_migrate # 16-02-2019
erpnext.patches.v11_0.update_delivery_trip_status
-erpnext.patches.v11_0.set_missing_gst_hsn_code
erpnext.patches.v11_0.rename_bom_wo_fields
erpnext.patches.v12_0.set_default_homepage_type
erpnext.patches.v11_0.rename_additional_salary_component_additional_salary
erpnext.patches.v11_0.renamed_from_to_fields_in_project
-erpnext.patches.v11_0.add_permissions_in_gst_settings #2020-04-04
erpnext.patches.v11_1.setup_guardian_role
execute:frappe.delete_doc('DocType', 'Notification Control')
-erpnext.patches.v12_0.set_gst_category
-erpnext.patches.v12_0.update_gst_category
erpnext.patches.v11_0.remove_barcodes_field_from_copy_fields_to_variants
erpnext.patches.v12_0.set_task_status
erpnext.patches.v11_0.make_italian_localization_fields # 26-03-2019
@@ -118,7 +109,6 @@
erpnext.patches.v11_1.update_default_supplier_in_item_defaults
erpnext.patches.v12_0.update_due_date_in_gle
erpnext.patches.v12_0.add_default_buying_selling_terms_in_company
-erpnext.patches.v12_0.update_ewaybill_field_position
erpnext.patches.v12_0.create_accounting_dimensions_in_missing_doctypes #2020-05-11
erpnext.patches.v11_1.set_status_for_material_request_type_manufacture
erpnext.patches.v12_0.move_plaid_settings_to_doctype
@@ -141,14 +131,12 @@
erpnext.patches.v12_0.set_automatically_process_deferred_accounting_in_accounts_settings
erpnext.patches.v12_0.set_payment_entry_status
erpnext.patches.v12_0.update_owner_fields_in_acc_dimension_custom_fields
-erpnext.patches.v12_0.add_export_type_field_in_party_master
erpnext.patches.v12_0.remove_denied_leaves_from_leave_ledger
erpnext.patches.v12_0.update_price_or_product_discount
erpnext.patches.v12_0.set_production_capacity_in_workstation
erpnext.patches.v12_0.set_employee_preferred_emails
erpnext.patches.v12_0.set_against_blanket_order_in_sales_and_purchase_order
erpnext.patches.v12_0.set_cost_center_in_child_table_of_expense_claim
-erpnext.patches.v12_0.add_eway_bill_in_delivery_note
erpnext.patches.v12_0.set_lead_title_field
erpnext.patches.v12_0.set_permission_einvoicing
erpnext.patches.v12_0.set_job_offer_applicant_email
@@ -196,7 +184,6 @@
erpnext.patches.v13_0.update_subscription
erpnext.patches.v12_0.unhide_cost_center_field
erpnext.patches.v13_0.update_sla_enhancements
-erpnext.patches.v12_0.update_address_template_for_india
erpnext.patches.v13_0.update_deferred_settings
erpnext.patches.v12_0.set_multi_uom_in_rfq
erpnext.patches.v13_0.delete_old_sales_reports
@@ -225,7 +212,6 @@
erpnext.patches.v13_0.set_app_name
erpnext.patches.v13_0.print_uom_after_quantity_patch
erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account
-erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02
erpnext.patches.v13_0.updates_for_multi_currency_payroll
erpnext.patches.v13_0.update_reason_for_resignation_in_employee
execute:frappe.delete_doc("Report", "Quoted Item Comparison")
@@ -245,22 +231,18 @@
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes
erpnext.patches.v13_0.update_vehicle_no_reqd_condition
-erpnext.patches.v12_0.add_einvoice_status_field #2021-03-17
-erpnext.patches.v12_0.add_einvoice_summary_report_permissions
erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation
erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings
erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae
erpnext.patches.v13_0.setup_uae_vat_fields
execute:frappe.db.set_value('System Settings', None, 'app_name', 'ERPNext')
erpnext.patches.v12_0.create_taxable_value_field
-erpnext.patches.v12_0.add_gst_category_in_delivery_note
erpnext.patches.v12_0.purchase_receipt_status
erpnext.patches.v13_0.fix_non_unique_represents_company
erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing
erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021 #17-01-2022
erpnext.patches.v13_0.update_shipment_status
erpnext.patches.v13_0.remove_attribute_field_from_item_variant_setting
-erpnext.patches.v12_0.add_ewaybill_validity_field
erpnext.patches.v13_0.germany_make_custom_fields
erpnext.patches.v13_0.germany_fill_debtor_creditor_number
erpnext.patches.v13_0.set_pos_closing_as_failed
@@ -276,9 +258,7 @@
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
erpnext.patches.v13_0.update_subscription_status_in_memberships
erpnext.patches.v13_0.update_amt_in_work_order_required_items
-erpnext.patches.v12_0.show_einvoice_irn_cancelled_field
erpnext.patches.v13_0.delete_orphaned_tables
-erpnext.patches.v13_0.update_export_type_for_gst #2021-08-16
erpnext.patches.v13_0.update_tds_check_field #3
erpnext.patches.v13_0.add_custom_field_for_south_africa #2
erpnext.patches.v13_0.update_recipient_email_digest
@@ -291,7 +271,6 @@
execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Category")
erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021
erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
-erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021
erpnext.patches.v13_0.fix_invoice_statuses
erpnext.patches.v13_0.create_website_items #30-09-2021
erpnext.patches.v13_0.populate_e_commerce_settings
@@ -299,7 +278,6 @@
erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item
erpnext.patches.v13_0.update_dates_in_tax_withholding_category
erpnext.patches.v14_0.update_opportunity_currency_fields
-erpnext.patches.v13_0.gst_fields_for_pos_invoice
erpnext.patches.v13_0.create_accounting_dimensions_in_pos_doctypes
erpnext.patches.v13_0.trim_sales_invoice_custom_field_length
erpnext.patches.v13_0.create_custom_field_for_finance_book
@@ -316,7 +294,6 @@
erpnext.patches.v13_0.item_naming_series_not_mandatory
erpnext.patches.v14_0.delete_healthcare_doctypes
erpnext.patches.v13_0.update_category_in_ltds_certificate
-erpnext.patches.v13_0.create_pan_field_for_india #2
erpnext.patches.v13_0.fetch_thumbnail_in_website_items
erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit
erpnext.patches.v13_0.create_ksa_vat_custom_fields # 07-01-2022
@@ -324,7 +301,6 @@
erpnext.patches.v13_0.rename_ksa_qr_field
erpnext.patches.v13_0.wipe_serial_no_field_for_0_qty
erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
-erpnext.patches.v13_0.update_tax_category_for_rcm
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
erpnext.patches.v14_0.set_payroll_cost_centers
erpnext.patches.v13_0.agriculture_deprecation_warning
@@ -342,9 +318,7 @@
erpnext.patches.v14_0.delete_agriculture_doctypes
erpnext.patches.v14_0.rearrange_company_fields
erpnext.patches.v14_0.update_leave_notification_template
-erpnext.patches.v14_0.restore_einvoice_fields
erpnext.patches.v13_0.update_sane_transfer_against
-erpnext.patches.v12_0.add_company_link_to_einvoice_settings
erpnext.patches.v14_0.migrate_cost_center_allocations
erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template
erpnext.patches.v13_0.shopping_cart_to_ecommerce
@@ -353,3 +327,5 @@
erpnext.patches.v13_0.update_exchange_rate_settings
erpnext.patches.v14_0.delete_amazon_mws_doctype
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
diff --git a/erpnext/patches/v11_0/add_permissions_in_gst_settings.py b/erpnext/patches/v11_0/add_permissions_in_gst_settings.py
deleted file mode 100644
index 9df1b58..0000000
--- a/erpnext/patches/v11_0/add_permissions_in_gst_settings.py
+++ /dev/null
@@ -1,13 +0,0 @@
-import frappe
-
-from erpnext.regional.india.setup import add_permissions
-
-
-def execute():
- company = frappe.get_all('Company', filters = {'country': 'India'})
- if not company:
- return
-
- frappe.reload_doc("regional", "doctype", "lower_deduction_certificate")
- frappe.reload_doc("regional", "doctype", "gstr_3b_report")
- add_permissions()
diff --git a/erpnext/patches/v11_0/ewaybill_fields_gst_india.py b/erpnext/patches/v11_0/ewaybill_fields_gst_india.py
deleted file mode 100644
index 5974e27..0000000
--- a/erpnext/patches/v11_0/ewaybill_fields_gst_india.py
+++ /dev/null
@@ -1,11 +0,0 @@
-import frappe
-
-from erpnext.regional.india.setup import make_custom_fields
-
-
-def execute():
- company = frappe.get_all('Company', filters = {'country': 'India'})
- if not company:
- return
-
- make_custom_fields()
diff --git a/erpnext/patches/v11_0/inter_state_field_for_gst.py b/erpnext/patches/v11_0/inter_state_field_for_gst.py
deleted file mode 100644
index a1f1594..0000000
--- a/erpnext/patches/v11_0/inter_state_field_for_gst.py
+++ /dev/null
@@ -1,72 +0,0 @@
-import frappe
-
-from erpnext.regional.india.setup import make_custom_fields
-
-
-def execute():
- company = frappe.get_all('Company', filters = {'country': 'India'})
- if not company:
- return
- frappe.reload_doc("Payroll", "doctype", "Employee Tax Exemption Declaration")
- frappe.reload_doc("Payroll", "doctype", "Employee Tax Exemption Proof Submission")
- frappe.reload_doc("hr", "doctype", "Employee Grade")
- frappe.reload_doc("hr", "doctype", "Leave Policy")
-
- frappe.reload_doc("accounts", "doctype", "Bank Account")
- frappe.reload_doc("accounts", "doctype", "Tax Withholding Category")
- frappe.reload_doc("accounts", "doctype", "Allowed To Transact With")
- frappe.reload_doc("accounts", "doctype", "Finance Book")
- frappe.reload_doc("accounts", "doctype", "Loyalty Program")
-
- frappe.reload_doc("stock", "doctype", "Item Barcode")
-
- make_custom_fields()
-
- frappe.reload_doc("accounts", "doctype", "sales_taxes_and_charges")
- frappe.reload_doc("accounts", "doctype", "purchase_taxes_and_charges")
- frappe.reload_doc("accounts", "doctype", "sales_taxes_and_charges_template")
- frappe.reload_doc("accounts", "doctype", "purchase_taxes_and_charges_template")
-
- # set is_inter_state in Taxes And Charges Templates
- if frappe.db.has_column("Sales Taxes and Charges Template", "is_inter_state") and\
- frappe.db.has_column("Purchase Taxes and Charges Template", "is_inter_state"):
-
- igst_accounts = set(frappe.db.sql_list('''SELECT igst_account from `tabGST Account` WHERE parent = "GST Settings"'''))
- cgst_accounts = set(frappe.db.sql_list('''SELECT cgst_account FROM `tabGST Account` WHERE parenttype = "GST Settings"'''))
-
- when_then_sales = get_formatted_data("Sales Taxes and Charges", igst_accounts, cgst_accounts)
- when_then_purchase = get_formatted_data("Purchase Taxes and Charges", igst_accounts, cgst_accounts)
-
- if when_then_sales:
- frappe.db.sql('''update `tabSales Taxes and Charges Template`
- set is_inter_state = Case {when_then} Else 0 End
- '''.format(when_then=" ".join(when_then_sales)))
-
- if when_then_purchase:
- frappe.db.sql('''update `tabPurchase Taxes and Charges Template`
- set is_inter_state = Case {when_then} Else 0 End
- '''.format(when_then=" ".join(when_then_purchase)))
-
-def get_formatted_data(doctype, igst_accounts, cgst_accounts):
- # fetch all the rows data from child table
- all_details = frappe.db.sql('''
- select parent, account_head from `tab{doctype}`
- where parenttype="{doctype} Template"'''.format(doctype=doctype), as_dict=True)
-
- # group the data in the form "parent: [list of accounts]""
- group_detail = {}
- for i in all_details:
- if not i['parent'] in group_detail: group_detail[i['parent']] = []
- for j in all_details:
- if i['parent']==j['parent']:
- group_detail[i['parent']].append(j['account_head'])
-
- # form when_then condition based on - if list of accounts for a document
- # matches any account in igst_accounts list and not matches any in cgst_accounts list
- when_then = []
- for i in group_detail:
- temp = set(group_detail[i])
- if not temp.isdisjoint(igst_accounts) and temp.isdisjoint(cgst_accounts):
- when_then.append('''When name='{name}' Then 1'''.format(name=i))
-
- return when_then
diff --git a/erpnext/patches/v11_0/set_missing_gst_hsn_code.py b/erpnext/patches/v11_0/set_missing_gst_hsn_code.py
deleted file mode 100644
index ec75d45..0000000
--- a/erpnext/patches/v11_0/set_missing_gst_hsn_code.py
+++ /dev/null
@@ -1,45 +0,0 @@
-import frappe
-
-from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_html
-
-
-def execute():
- company = frappe.db.sql_list("select name from tabCompany where country = 'India'")
- if not company:
- return
-
- doctypes = ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice",
- "Supplier Quotation", "Purchase Order", "Purchase Receipt", "Purchase Invoice"]
-
- for dt in doctypes:
- date_field = "posting_date"
- if dt in ["Quotation", "Sales Order", "Supplier Quotation", "Purchase Order"]:
- date_field = "transaction_date"
-
- transactions = frappe.db.sql("""
- select dt.name, dt_item.name as child_name
- from `tab{dt}` dt, `tab{dt} Item` dt_item
- where dt.name = dt_item.parent
- and dt.`{date_field}` > '2018-06-01'
- and dt.docstatus = 1
- and ifnull(dt_item.gst_hsn_code, '') = ''
- and ifnull(dt_item.item_code, '') != ''
- and dt.company in ({company})
- """.format(dt=dt, date_field=date_field, company=", ".join(['%s']*len(company))), tuple(company), as_dict=1)
-
- if not transactions:
- continue
-
- transaction_rows_name = [d.child_name for d in transactions]
-
- frappe.db.sql("""
- update `tab{dt} Item` dt_item
- set dt_item.gst_hsn_code = (select gst_hsn_code from tabItem where name=dt_item.item_code)
- where dt_item.name in ({rows_name})
- """.format(dt=dt, rows_name=", ".join(['%s']*len(transaction_rows_name))), tuple(transaction_rows_name))
-
- parent = set([d.name for d in transactions])
- for t in list(parent):
- trans_doc = frappe.get_doc(dt, t)
- hsnwise_tax = get_itemised_tax_breakup_html(trans_doc)
- frappe.db.set_value(dt, t, "other_charges_calculation", hsnwise_tax, update_modified=False)
diff --git a/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py
deleted file mode 100644
index e498b67..0000000
--- a/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from __future__ import unicode_literals
-
-import frappe
-
-
-def execute():
- company = frappe.get_all('Company', filters = {'country': 'India'})
- if not company or not frappe.db.count('E Invoice User'):
- return
-
- frappe.reload_doc("regional", "doctype", "e_invoice_user")
- for creds in frappe.db.get_all('E Invoice User', fields=['name', 'gstin']):
- company_name = frappe.db.sql("""
- select dl.link_name from `tabAddress` a, `tabDynamic Link` dl
- where a.gstin = %s and dl.parent = a.name and dl.link_doctype = 'Company'
- """, (creds.get('gstin')))
- if company_name and len(company_name) > 0:
- frappe.db.set_value('E Invoice User', creds.get('name'), 'company', company_name[0][0])
diff --git a/erpnext/patches/v12_0/add_einvoice_status_field.py b/erpnext/patches/v12_0/add_einvoice_status_field.py
deleted file mode 100644
index aeff9ca..0000000
--- a/erpnext/patches/v12_0/add_einvoice_status_field.py
+++ /dev/null
@@ -1,72 +0,0 @@
-from __future__ import unicode_literals
-
-import json
-
-import frappe
-from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
-
-
-def execute():
- company = frappe.get_all('Company', filters = {'country': 'India'})
- if not company:
- return
-
- # move hidden einvoice fields to a different section
- custom_fields = {
- 'Sales Invoice': [
- dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type',
- print_hide=1, hidden=1),
-
- dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section',
- no_copy=1, print_hide=1),
-
- dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
-
- dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date',
- no_copy=1, print_hide=1),
-
- dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date',
- no_copy=1, print_hide=1, read_only=1),
-
- dict(fieldname='signed_qr_code', label='Signed QRCode', fieldtype='Code', options='JSON', hidden=1, insert_after='signed_einvoice',
- no_copy=1, print_hide=1, read_only=1),
-
- dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, insert_after='signed_qr_code',
- no_copy=1, print_hide=1, read_only=1),
-
- dict(fieldname='einvoice_status', label='E-Invoice Status', fieldtype='Select', insert_after='qrcode_image',
- options='\nPending\nGenerated\nCancelled\nFailed', default=None, hidden=1, no_copy=1, print_hide=1, read_only=1),
-
- dict(fieldname='failure_description', label='E-Invoice Failure Description', fieldtype='Code', options='JSON',
- hidden=1, insert_after='einvoice_status', no_copy=1, print_hide=1, read_only=1)
- ]
- }
- create_custom_fields(custom_fields, update=True)
-
- if frappe.db.exists('E Invoice Settings') and frappe.db.get_single_value('E Invoice Settings', 'enable'):
- frappe.db.sql('''
- UPDATE `tabSales Invoice` SET einvoice_status = 'Pending'
- WHERE
- posting_date >= '2021-04-01'
- AND ifnull(irn, '') = ''
- AND ifnull(`billing_address_gstin`, '') != ifnull(`company_gstin`, '')
- AND ifnull(gst_category, '') in ('Registered Regular', 'SEZ', 'Overseas', 'Deemed Export')
- ''')
-
- # set appropriate statuses
- frappe.db.sql('''UPDATE `tabSales Invoice` SET einvoice_status = 'Generated'
- WHERE ifnull(irn, '') != '' AND ifnull(irn_cancelled, 0) = 0''')
-
- frappe.db.sql('''UPDATE `tabSales Invoice` SET einvoice_status = 'Cancelled'
- WHERE ifnull(irn_cancelled, 0) = 1''')
-
- # set correct acknowledgement in e-invoices
- einvoices = frappe.get_all('Sales Invoice', {'irn': ['is', 'set']}, ['name', 'signed_einvoice'])
-
- if einvoices:
- for inv in einvoices:
- signed_einvoice = inv.get('signed_einvoice')
- if signed_einvoice:
- signed_einvoice = json.loads(signed_einvoice)
- frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_no', signed_einvoice.get('AckNo'), update_modified=False)
- frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_date', signed_einvoice.get('AckDt'), update_modified=False)
diff --git a/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py b/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py
deleted file mode 100644
index e837786..0000000
--- a/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from __future__ import unicode_literals
-
-import frappe
-
-
-def execute():
- company = frappe.get_all('Company', filters = {'country': 'India'})
- if not company:
- return
-
- if frappe.db.exists('Report', 'E-Invoice Summary') and \
- not frappe.db.get_value('Custom Role', dict(report='E-Invoice Summary')):
- frappe.get_doc(dict(
- doctype='Custom Role',
- report='E-Invoice Summary',
- roles= [
- dict(role='Accounts User'),
- dict(role='Accounts Manager')
- ]
- )).insert()
diff --git a/erpnext/patches/v12_0/add_eway_bill_in_delivery_note.py b/erpnext/patches/v12_0/add_eway_bill_in_delivery_note.py
deleted file mode 100644
index 973da89..0000000
--- a/erpnext/patches/v12_0/add_eway_bill_in_delivery_note.py
+++ /dev/null
@@ -1,20 +0,0 @@
-import frappe
-from frappe.custom.doctype.custom_field.custom_field import create_custom_field
-
-
-def execute():
- company = frappe.get_all('Company', filters = {'country': 'India'})
-
- if not company:
- return
-
- create_custom_field('Delivery Note', {
- 'fieldname': 'ewaybill',
- 'label': 'E-Way Bill No.',
- 'fieldtype': 'Data',
- 'depends_on': 'eval:(doc.docstatus === 1)',
- 'allow_on_submit': 1,
- 'insert_after': 'customer_name_in_arabic',
- 'translatable': 0,
- 'owner': 'Administrator'
- })
diff --git a/erpnext/patches/v12_0/add_ewaybill_validity_field.py b/erpnext/patches/v12_0/add_ewaybill_validity_field.py
deleted file mode 100644
index 247140d..0000000
--- a/erpnext/patches/v12_0/add_ewaybill_validity_field.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from __future__ import unicode_literals
-
-import frappe
-from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
-
-
-def execute():
- company = frappe.get_all('Company', filters = {'country': 'India'})
- if not company:
- return
-
- custom_fields = {
- 'Sales Invoice': [
- dict(fieldname='eway_bill_validity', label='E-Way Bill Validity', fieldtype='Data', no_copy=1, print_hide=1,
- depends_on='ewaybill', read_only=1, allow_on_submit=1, insert_after='ewaybill')
- ]
- }
- create_custom_fields(custom_fields, update=True)
diff --git a/erpnext/patches/v12_0/add_export_type_field_in_party_master.py b/erpnext/patches/v12_0/add_export_type_field_in_party_master.py
deleted file mode 100644
index dc9e884..0000000
--- a/erpnext/patches/v12_0/add_export_type_field_in_party_master.py
+++ /dev/null
@@ -1,41 +0,0 @@
-import frappe
-
-from erpnext.regional.india.setup import make_custom_fields
-
-
-def execute():
-
- company = frappe.get_all('Company', filters = {'country': 'India'})
- if not company:
- return
-
- make_custom_fields()
-
- frappe.reload_doctype('Tax Category')
- frappe.reload_doctype('Sales Taxes and Charges Template')
- frappe.reload_doctype('Purchase Taxes and Charges Template')
-
- # Create tax category with inter state field checked
- tax_category = frappe.db.get_value('Tax Category', {'name': 'OUT OF STATE'}, 'name')
-
- if not tax_category:
- inter_state_category = frappe.get_doc({
- 'doctype': 'Tax Category',
- 'title': 'OUT OF STATE',
- 'is_inter_state': 1
- }).insert()
-
- tax_category = inter_state_category.name
-
- for doctype in ('Sales Taxes and Charges Template', 'Purchase Taxes and Charges Template'):
- if not frappe.get_meta(doctype).has_field('is_inter_state'): continue
-
- template = frappe.db.get_value(doctype, {'is_inter_state': 1, 'disabled': 0}, ['name'])
- if template:
- frappe.db.set_value(doctype, template, 'tax_category', tax_category)
-
- frappe.db.sql("""
- DELETE FROM `tabCustom Field`
- WHERE fieldname = 'is_inter_state'
- AND dt IN ('Sales Taxes and Charges Template', 'Purchase Taxes and Charges Template')
- """)
diff --git a/erpnext/patches/v12_0/add_gst_category_in_delivery_note.py b/erpnext/patches/v12_0/add_gst_category_in_delivery_note.py
deleted file mode 100644
index 6316bb3..0000000
--- a/erpnext/patches/v12_0/add_gst_category_in_delivery_note.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import frappe
-from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
-
-
-def execute():
- company = frappe.get_all('Company', filters = {'country': 'India'})
- if not company:
- return
-
- custom_fields = {
- 'Delivery Note': [
- dict(fieldname='gst_category', label='GST Category',
- fieldtype='Select', insert_after='gst_vehicle_type', print_hide=1,
- options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders',
- fetch_from='customer.gst_category', fetch_if_empty=1),
- ]
- }
-
- create_custom_fields(custom_fields, update=True)
diff --git a/erpnext/patches/v12_0/set_gst_category.py b/erpnext/patches/v12_0/set_gst_category.py
deleted file mode 100644
index 094e2a3..0000000
--- a/erpnext/patches/v12_0/set_gst_category.py
+++ /dev/null
@@ -1,52 +0,0 @@
-import frappe
-
-from erpnext.regional.india.setup import make_custom_fields
-
-
-def execute():
-
- company = frappe.get_all('Company', filters = {'country': 'India'})
- if not company:
- return
-
- frappe.reload_doc('accounts', 'doctype', 'Tax Category')
-
- make_custom_fields()
-
- for doctype in ['Sales Invoice', 'Purchase Invoice']:
- has_column = frappe.db.has_column(doctype,'invoice_type')
-
- if has_column:
- update_map = {
- 'Regular': 'Registered Regular',
- 'Export': 'Overseas',
- 'SEZ': 'SEZ',
- 'Deemed Export': 'Deemed Export',
- }
-
- for old, new in update_map.items():
- frappe.db.sql("UPDATE `tab{doctype}` SET gst_category = %s where invoice_type = %s".format(doctype=doctype), (new, old)) #nosec
-
- frappe.delete_doc('Custom Field', 'Sales Invoice-invoice_type')
- frappe.delete_doc('Custom Field', 'Purchase Invoice-invoice_type')
-
- itc_update_map = {
- "ineligible": "Ineligible",
- "input service": "Input Service Distributor",
- "capital goods": "Import Of Capital Goods",
- "input": "All Other ITC"
- }
-
- has_gst_fields = frappe.db.has_column('Purchase Invoice','eligibility_for_itc')
-
- if has_gst_fields:
- for old, new in itc_update_map.items():
- frappe.db.sql("UPDATE `tabPurchase Invoice` SET eligibility_for_itc = %s where eligibility_for_itc = %s ", (new, old))
-
- for doctype in ["Customer", "Supplier"]:
-
- frappe.db.sql(""" UPDATE `tab{doctype}` t1, `tabAddress` t2, `tabDynamic Link` t3 SET t1.gst_category = "Registered Regular"
- where t3.link_name = t1.name and t3.parent = t2.name and t2.gstin IS NOT NULL and t2.gstin != '' """.format(doctype=doctype)) #nosec
-
- frappe.db.sql(""" UPDATE `tab{doctype}` t1, `tabAddress` t2, `tabDynamic Link` t3 SET t1.gst_category = "Overseas"
- where t3.link_name = t1.name and t3.parent = t2.name and t2.country != 'India' """.format(doctype=doctype)) #nosec
diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py
deleted file mode 100644
index c17666a..0000000
--- a/erpnext/patches/v12_0/setup_einvoice_fields.py
+++ /dev/null
@@ -1,59 +0,0 @@
-from __future__ import unicode_literals
-
-import frappe
-from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
-
-from erpnext.regional.india.setup import add_permissions, add_print_formats
-
-
-def execute():
- company = frappe.get_all('Company', filters = {'country': 'India'})
- if not company:
- return
-
- frappe.reload_doc("custom", "doctype", "custom_field")
- frappe.reload_doc("regional", "doctype", "e_invoice_settings")
- custom_fields = {
- 'Sales Invoice': [
- dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1,
- depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'),
-
- dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1),
-
- dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
-
- dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
- depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
-
- dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
- depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
-
- dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
-
- dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
-
- dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1)
- ]
- }
- create_custom_fields(custom_fields, update=True)
- add_permissions()
- add_print_formats()
-
- einvoice_cond = 'in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category)'
- t = {
- 'mode_of_transport': [{'default': None}],
- 'distance': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.transporter'}],
- 'gst_vehicle_type': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}],
- 'lr_date': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}],
- 'lr_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}],
- 'vehicle_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}],
- 'ewaybill': [
- {'read_only_depends_on': 'eval:doc.irn && doc.ewaybill'},
- {'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)'}
- ]
- }
-
- for field, conditions in t.items():
- for c in conditions:
- [(prop, value)] = c.items()
- frappe.db.set_value('Custom Field', { 'fieldname': field }, prop, value)
diff --git a/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py b/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py
deleted file mode 100644
index 3f90a03..0000000
--- a/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from __future__ import unicode_literals
-
-import frappe
-
-
-def execute():
- company = frappe.get_all('Company', filters = {'country': 'India'})
- if not company:
- return
-
- irn_cancelled_field = frappe.db.exists('Custom Field', {'dt': 'Sales Invoice', 'fieldname': 'irn_cancelled'})
- if irn_cancelled_field:
- frappe.db.set_value('Custom Field', irn_cancelled_field, 'depends_on', 'eval: doc.irn')
- frappe.db.set_value('Custom Field', irn_cancelled_field, 'read_only', 0)
diff --git a/erpnext/patches/v12_0/update_address_template_for_india.py b/erpnext/patches/v12_0/update_address_template_for_india.py
deleted file mode 100644
index 64a2e41..0000000
--- a/erpnext/patches/v12_0/update_address_template_for_india.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright (c) 2020, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-
-
-import frappe
-
-from erpnext.regional.address_template.setup import set_up_address_templates
-
-
-def execute():
- if frappe.db.get_value('Company', {'country': 'India'}, 'name'):
- address_template = frappe.db.get_value('Address Template', 'India', 'template')
- if not address_template or "gstin" not in address_template:
- set_up_address_templates(default_country='India')
diff --git a/erpnext/patches/v12_0/update_ewaybill_field_position.py b/erpnext/patches/v12_0/update_ewaybill_field_position.py
deleted file mode 100644
index 132fd90..0000000
--- a/erpnext/patches/v12_0/update_ewaybill_field_position.py
+++ /dev/null
@@ -1,27 +0,0 @@
-import frappe
-
-
-def execute():
- company = frappe.get_all('Company', filters = {'country': 'India'})
-
- if not company:
- return
-
- field = frappe.db.get_value("Custom Field", {"dt": "Sales Invoice", "fieldname": "ewaybill"})
-
- if field:
- ewaybill_field = frappe.get_doc("Custom Field", field)
-
- ewaybill_field.flags.ignore_validate = True
-
- ewaybill_field.update({
- 'fieldname': 'ewaybill',
- 'label': 'e-Way Bill No.',
- 'fieldtype': 'Data',
- 'depends_on': 'eval:(doc.docstatus === 1)',
- 'allow_on_submit': 1,
- 'insert_after': 'tax_id',
- 'translatable': 0
- })
-
- ewaybill_field.save()
diff --git a/erpnext/patches/v12_0/update_gst_category.py b/erpnext/patches/v12_0/update_gst_category.py
deleted file mode 100644
index 8b15370..0000000
--- a/erpnext/patches/v12_0/update_gst_category.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import frappe
-
-
-def execute():
-
- company = frappe.get_all('Company', filters = {'country': 'India'})
- if not company:
- return
-
- frappe.db.sql(""" UPDATE `tabSales Invoice` set gst_category = 'Unregistered'
- where gst_category = 'Registered Regular'
- and ifnull(customer_gstin, '')=''
- and ifnull(billing_address_gstin,'')=''
- """)
-
- frappe.db.sql(""" UPDATE `tabPurchase Invoice` set gst_category = 'Unregistered'
- where gst_category = 'Registered Regular'
- and ifnull(supplier_gstin, '')=''
- """)
diff --git a/erpnext/patches/v13_0/create_gst_payment_entry_fields.py b/erpnext/patches/v13_0/create_gst_payment_entry_fields.py
deleted file mode 100644
index 4166945..0000000
--- a/erpnext/patches/v13_0/create_gst_payment_entry_fields.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# Copyright (c) 2021, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-
-import frappe
-from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
-
-
-def execute():
- frappe.reload_doc('accounts', 'doctype', 'advance_taxes_and_charges')
- frappe.reload_doc('accounts', 'doctype', 'payment_entry')
-
- if frappe.db.exists('Company', {'country': 'India'}):
- custom_fields = {
- 'Payment Entry': [
- dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', insert_after='deductions',
- print_hide=1, collapsible=1),
- dict(fieldname='company_address', label='Company Address', fieldtype='Link', insert_after='gst_section',
- print_hide=1, options='Address'),
- dict(fieldname='company_gstin', label='Company GSTIN',
- fieldtype='Data', insert_after='company_address',
- fetch_from='company_address.gstin', print_hide=1, read_only=1),
- dict(fieldname='place_of_supply', label='Place of Supply',
- fieldtype='Data', insert_after='company_gstin',
- print_hide=1, read_only=1),
- dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='place_of_supply',
- print_hide=1, options='Address', depends_on = 'eval:doc.party_type == "Customer"'),
- dict(fieldname='customer_gstin', label='Customer GSTIN',
- fieldtype='Data', insert_after='customer_address',
- fetch_from='customer_address.gstin', print_hide=1, read_only=1)
- ]
- }
-
- create_custom_fields(custom_fields, update=True)
- else:
- fields = ['gst_section', 'company_address', 'company_gstin', 'place_of_supply', 'customer_address', 'customer_gstin']
- for field in fields:
- frappe.delete_doc_if_exists("Custom Field", f"Payment Entry-{field}")
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/create_pan_field_for_india.py b/erpnext/patches/v13_0/create_pan_field_for_india.py
deleted file mode 100644
index 6df6e1e..0000000
--- a/erpnext/patches/v13_0/create_pan_field_for_india.py
+++ /dev/null
@@ -1,29 +0,0 @@
-import frappe
-from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
-
-
-def execute():
- frappe.reload_doc('buying', 'doctype', 'supplier', force=True)
- frappe.reload_doc('selling', 'doctype', 'customer', force=True)
- frappe.reload_doc('core', 'doctype', 'doctype', force=True)
-
- custom_fields = {
- 'Supplier': [
- {
- 'fieldname': 'pan',
- 'label': 'PAN',
- 'fieldtype': 'Data',
- 'insert_after': 'supplier_type'
- }
- ],
- 'Customer': [
- {
- 'fieldname': 'pan',
- 'label': 'PAN',
- 'fieldtype': 'Data',
- 'insert_after': 'customer_type'
- }
- ]
- }
-
- create_custom_fields(custom_fields, update=True)
diff --git a/erpnext/patches/v13_0/gst_fields_for_pos_invoice.py b/erpnext/patches/v13_0/gst_fields_for_pos_invoice.py
deleted file mode 100644
index 76f8b27..0000000
--- a/erpnext/patches/v13_0/gst_fields_for_pos_invoice.py
+++ /dev/null
@@ -1,42 +0,0 @@
-import frappe
-from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
-
-
-def execute():
- company = frappe.get_all('Company', filters = {'country': 'India'}, fields=['name'])
- if not company:
- return
-
- hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
- fieldtype='Data', fetch_from='item_code.gst_hsn_code', insert_after='description',
- allow_on_submit=1, print_hide=1, fetch_if_empty=1)
- nil_rated_exempt = dict(fieldname='is_nil_exempt', label='Is Nil Rated or Exempted',
- fieldtype='Check', fetch_from='item_code.is_nil_exempt', insert_after='gst_hsn_code',
- print_hide=1)
- is_non_gst = dict(fieldname='is_non_gst', label='Is Non GST',
- fieldtype='Check', fetch_from='item_code.is_non_gst', insert_after='is_nil_exempt',
- print_hide=1)
- taxable_value = dict(fieldname='taxable_value', label='Taxable Value',
- fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency",
- print_hide=1)
- sales_invoice_gst_fields = [
- dict(fieldname='billing_address_gstin', label='Billing Address GSTIN',
- fieldtype='Data', insert_after='customer_address', read_only=1,
- fetch_from='customer_address.gstin', print_hide=1),
- dict(fieldname='customer_gstin', label='Customer GSTIN',
- fieldtype='Data', insert_after='shipping_address_name',
- fetch_from='shipping_address_name.gstin', print_hide=1),
- dict(fieldname='place_of_supply', label='Place of Supply',
- fieldtype='Data', insert_after='customer_gstin',
- print_hide=1, read_only=1),
- dict(fieldname='company_gstin', label='Company GSTIN',
- fieldtype='Data', insert_after='company_address',
- fetch_from='company_address.gstin', print_hide=1, read_only=1),
- ]
-
- custom_fields = {
- 'POS Invoice': sales_invoice_gst_fields,
- 'POS Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value],
- }
-
- create_custom_fields(custom_fields, update=True)
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_accounts_in_loan_docs.py b/erpnext/patches/v13_0/update_accounts_in_loan_docs.py
new file mode 100644
index 0000000..440f912
--- /dev/null
+++ b/erpnext/patches/v13_0/update_accounts_in_loan_docs.py
@@ -0,0 +1,37 @@
+import frappe
+
+
+def execute():
+ ld = frappe.qb.DocType("Loan Disbursement").as_("ld")
+ lr = frappe.qb.DocType("Loan Repayment").as_("lr")
+ loan = frappe.qb.DocType("Loan")
+
+ frappe.qb.update(
+ ld
+ ).inner_join(
+ loan
+ ).on(
+ loan.name == ld.against_loan
+ ).set(
+ ld.disbursement_account, loan.disbursement_account
+ ).set(
+ ld.loan_account, loan.loan_account
+ ).where(
+ ld.docstatus < 2
+ ).run()
+
+ frappe.qb.update(
+ lr
+ ).inner_join(
+ loan
+ ).on(
+ loan.name == lr.against_loan
+ ).set(
+ lr.payment_account, loan.payment_account
+ ).set(
+ lr.loan_account, loan.loan_account
+ ).set(
+ lr.penalty_income_account, loan.penalty_income_account
+ ).where(
+ lr.docstatus < 2
+ ).run()
diff --git a/erpnext/patches/v13_0/update_export_type_for_gst.py b/erpnext/patches/v13_0/update_export_type_for_gst.py
deleted file mode 100644
index de57861..0000000
--- a/erpnext/patches/v13_0/update_export_type_for_gst.py
+++ /dev/null
@@ -1,33 +0,0 @@
-import frappe
-
-
-def execute():
- company = frappe.get_all('Company', filters = {'country': 'India'})
- if not company:
- return
-
- # Update custom fields
- fieldname = frappe.db.get_value('Custom Field', {'dt': 'Customer', 'fieldname': 'export_type'})
- if fieldname:
- frappe.db.set_value('Custom Field', fieldname,
- {
- 'default': '',
- 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)'
- })
-
- fieldname = frappe.db.get_value('Custom Field', {'dt': 'Supplier', 'fieldname': 'export_type'})
- if fieldname:
- frappe.db.set_value('Custom Field', fieldname,
- {
- 'default': '',
- 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas"], doc.gst_category)'
- })
-
- # Update Customer/Supplier Masters
- frappe.db.sql("""
- UPDATE `tabCustomer` set export_type = '' WHERE gst_category NOT IN ('SEZ', 'Overseas', 'Deemed Export')
- """)
-
- frappe.db.sql("""
- UPDATE `tabSupplier` set export_type = '' WHERE gst_category NOT IN ('SEZ', 'Overseas')
- """)
diff --git a/erpnext/patches/v13_0/update_tax_category_for_rcm.py b/erpnext/patches/v13_0/update_tax_category_for_rcm.py
deleted file mode 100644
index 7af2366..0000000
--- a/erpnext/patches/v13_0/update_tax_category_for_rcm.py
+++ /dev/null
@@ -1,31 +0,0 @@
-import frappe
-from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
-
-from erpnext.regional.india import states
-
-
-def execute():
- company = frappe.get_all('Company', filters = {'country': 'India'})
- if not company:
- return
-
- create_custom_fields({
- 'Tax Category': [
- dict(fieldname='is_inter_state', label='Is Inter State',
- fieldtype='Check', insert_after='disabled', print_hide=1),
- dict(fieldname='is_reverse_charge', label='Is Reverse Charge', fieldtype='Check',
- insert_after='is_inter_state', print_hide=1),
- dict(fieldname='tax_category_column_break', fieldtype='Column Break',
- insert_after='is_reverse_charge'),
- dict(fieldname='gst_state', label='Source State', fieldtype='Select',
- options='\n'.join(states), insert_after='company')
- ]
- }, update=True)
-
- tax_category = frappe.qb.DocType("Tax Category")
-
- frappe.qb.update(tax_category).set(
- tax_category.is_reverse_charge, 1
- ).where(
- tax_category.name.isin(['Reverse Charge Out-State', 'Reverse Charge In-State'])
- ).run()
\ No newline at end of file
diff --git a/erpnext/patches/v14_0/restore_einvoice_fields.py b/erpnext/patches/v14_0/restore_einvoice_fields.py
deleted file mode 100644
index c4431fb..0000000
--- a/erpnext/patches/v14_0/restore_einvoice_fields.py
+++ /dev/null
@@ -1,24 +0,0 @@
-import frappe
-from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
-
-from erpnext.regional.india.setup import add_permissions, add_print_formats
-
-
-def execute():
- # restores back the 2 custom fields that was deleted while removing e-invoicing from v14
- company = frappe.get_all('Company', filters = {'country': 'India'})
- if not company:
- return
-
- custom_fields = {
- 'Sales Invoice': [
- dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
- depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
-
- dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
- depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
- ]
- }
- create_custom_fields(custom_fields, update=True)
- add_permissions()
- add_print_formats()
diff --git a/erpnext/patches/v14_0/update_batch_valuation_flag.py b/erpnext/patches/v14_0/update_batch_valuation_flag.py
new file mode 100644
index 0000000..55c8c48
--- /dev/null
+++ b/erpnext/patches/v14_0/update_batch_valuation_flag.py
@@ -0,0 +1,11 @@
+import frappe
+
+
+def execute():
+ """
+ - Don't use batchwise valuation for existing batches.
+ - Only batches created after this patch shoule use it.
+ """
+
+ batch = frappe.qb.DocType("Batch")
+ frappe.qb.update(batch).set(batch.use_batchwise_valuation, 0).run()
diff --git a/erpnext/patches/v8_1/removed_roles_from_gst_report_non_indian_account.py b/erpnext/patches/v8_1/removed_roles_from_gst_report_non_indian_account.py
deleted file mode 100644
index ed1dffe..0000000
--- a/erpnext/patches/v8_1/removed_roles_from_gst_report_non_indian_account.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) 2017, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-
-
-import frappe
-
-
-def execute():
- frappe.reload_doc('core', 'doctype', 'has_role')
- company = frappe.get_all('Company', filters = {'country': 'India'})
-
- if not company:
- frappe.db.sql("""
- delete from
- `tabHas Role`
- where
- parenttype = 'Report' and parent in('GST Sales Register',
- 'GST Purchase Register', 'GST Itemised Sales Register',
- 'GST Itemised Purchase Register', 'Eway Bill')""")
diff --git a/erpnext/patches/v8_1/setup_gst_india.py b/erpnext/patches/v8_1/setup_gst_india.py
deleted file mode 100644
index ff9e6a4..0000000
--- a/erpnext/patches/v8_1/setup_gst_india.py
+++ /dev/null
@@ -1,53 +0,0 @@
-import frappe
-from frappe.email import sendmail_to_system_managers
-
-
-def execute():
- frappe.reload_doc('stock', 'doctype', 'item')
- frappe.reload_doc("stock", "doctype", "customs_tariff_number")
- frappe.reload_doc("accounts", "doctype", "payment_terms_template")
- frappe.reload_doc("accounts", "doctype", "payment_schedule")
-
- company = frappe.get_all('Company', filters = {'country': 'India'})
- if not company:
- return
-
- frappe.reload_doc('regional', 'doctype', 'gst_settings')
- frappe.reload_doc('regional', 'doctype', 'gst_hsn_code')
-
- for report_name in ('GST Sales Register', 'GST Purchase Register',
- 'GST Itemised Sales Register', 'GST Itemised Purchase Register'):
-
- frappe.reload_doc('regional', 'report', frappe.scrub(report_name))
-
- from erpnext.regional.india.setup import setup
- delete_custom_field_tax_id_if_exists()
- setup(patch=True)
- send_gst_update_email()
-
-def delete_custom_field_tax_id_if_exists():
- for field in frappe.db.sql_list("""select name from `tabCustom Field` where fieldname='tax_id'
- and dt in ('Sales Order', 'Sales Invoice', 'Delivery Note')"""):
- frappe.delete_doc("Custom Field", field, ignore_permissions=True)
- frappe.db.commit()
-
-def send_gst_update_email():
- message = """Hello,
-
-<p>ERPNext is now GST Ready!</p>
-
-<p>To start making GST Invoices from 1st of July, you just need to create new Tax Accounts,
-Templates and update your Customer's and Supplier's GST Numbers.</p>
-
-<p>Please refer {gst_document_link} to know more about how to setup and implement GST in ERPNext.</p>
-
-<p>Please contact us at support@erpnext.com, if you have any questions.</p>
-
-<p>Thanks,</p>
-ERPNext Team.
- """.format(gst_document_link="<a href='http://frappe.github.io/erpnext/user/manual/en/regional/india/'> ERPNext GST Document </a>")
-
- try:
- sendmail_to_system_managers("[Important] ERPNext GST updates", message)
- except Exception as e:
- pass
diff --git a/erpnext/patches/v8_7/sync_india_custom_fields.py b/erpnext/patches/v8_7/sync_india_custom_fields.py
deleted file mode 100644
index 808c833..0000000
--- a/erpnext/patches/v8_7/sync_india_custom_fields.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import frappe
-
-from erpnext.regional.india.setup import make_custom_fields
-
-
-def execute():
- company = frappe.get_all('Company', filters = {'country': 'India'})
- if not company:
- return
-
- frappe.reload_doc('Payroll', 'doctype', 'payroll_period')
- frappe.reload_doc('Payroll', 'doctype', 'employee_tax_exemption_declaration')
- frappe.reload_doc('Payroll', 'doctype', 'employee_tax_exemption_proof_submission')
- frappe.reload_doc('Payroll', 'doctype', 'employee_tax_exemption_declaration_category')
- frappe.reload_doc('Payroll', 'doctype', 'employee_tax_exemption_proof_submission_detail')
-
- frappe.reload_doc('accounts', 'doctype', 'tax_category')
-
- for doctype in ["Sales Invoice", "Delivery Note", "Purchase Invoice"]:
- frappe.db.sql("""delete from `tabCustom Field` where dt = %s
- and fieldname in ('port_code', 'shipping_bill_number', 'shipping_bill_date')""", doctype)
-
- make_custom_fields()
-
- frappe.db.sql("""
- update `tabCustom Field`
- set reqd = 0, `default` = ''
- where fieldname = 'reason_for_issuing_document'
- """)
-
- frappe.db.sql("""
- update tabAddress
- set gst_state_number=concat("0", gst_state_number)
- where ifnull(gst_state_number, '') != '' and gst_state_number<10
- """)
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index f727ff4..d2a3998 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -1268,7 +1268,7 @@
for i, earning in enumerate(self.earnings):
if earning.salary_component == salary_component:
self.earnings[i].amount = wages_amount
- self.gross_pay += self.earnings[i].amount
+ self.gross_pay += flt(self.earnings[i].amount, earning.precision("amount"))
self.net_pay = flt(self.gross_pay) - flt(self.total_deduction)
def compute_year_to_date(self):
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index daa0f89..6a5debf 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -1019,13 +1019,13 @@
frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None)
frappe.db.set_value('HR Settings', None, 'leave_approval_notification_template', None)
-def make_holiday_list():
+def make_holiday_list(holiday_list_name=None):
fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
- holiday_list = frappe.db.exists("Holiday List", "Salary Slip Test Holiday List")
+ holiday_list = frappe.db.exists("Holiday List", holiday_list_name or "Salary Slip Test Holiday List")
if not holiday_list:
holiday_list = frappe.get_doc({
"doctype": "Holiday List",
- "holiday_list_name": "Salary Slip Test Holiday List",
+ "holiday_list_name": holiday_list_name or "Salary Slip Test Holiday List",
"from_date": fiscal_year[1],
"to_date": fiscal_year[2],
"weekly_off": "Sunday"
diff --git a/erpnext/portal/doctype/homepage_section/test_homepage_section.py b/erpnext/portal/doctype/homepage_section/test_homepage_section.py
index b30d983..c3be146 100644
--- a/erpnext/portal/doctype/homepage_section/test_homepage_section.py
+++ b/erpnext/portal/doctype/homepage_section/test_homepage_section.py
@@ -21,7 +21,7 @@
{'title': 'Card 2', 'subtitle': 'Subtitle 2', 'content': 'This is test card 2', 'image': 'test.jpg'},
],
'no_of_columns': 3
- }).insert()
+ }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
index ca73393..214a1be 100644
--- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
+++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
@@ -182,6 +182,12 @@
onchange: () => this.update_options(),
},
{
+ fieldtype: "Check",
+ label: "Loan Repayment",
+ fieldname: "loan_repayment",
+ onchange: () => this.update_options(),
+ },
+ {
fieldname: "column_break_5",
fieldtype: "Column Break",
},
@@ -191,7 +197,6 @@
fieldname: "sales_invoice",
onchange: () => this.update_options(),
},
-
{
fieldtype: "Check",
label: "Purchase Invoice",
@@ -199,6 +204,12 @@
onchange: () => this.update_options(),
},
{
+ fieldtype: "Check",
+ label: "Show Only Exact Amount",
+ fieldname: "exact_match",
+ onchange: () => this.update_options(),
+ },
+ {
fieldname: "column_break_5",
fieldtype: "Column Break",
},
@@ -210,8 +221,8 @@
},
{
fieldtype: "Check",
- label: "Show Only Exact Amount",
- fieldname: "exact_match",
+ label: "Loan Disbursement",
+ fieldname: "loan_disbursement",
onchange: () => this.update_options(),
},
{
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 136e1ed..ae8c0c8 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -525,6 +525,7 @@
item.weight_per_unit = 0;
item.weight_uom = '';
+ item.conversion_factor = 0;
if(['Sales Invoice'].includes(this.frm.doc.doctype)) {
update_stock = cint(me.frm.doc.update_stock);
@@ -719,6 +720,7 @@
'posting_time': posting_time,
'qty': item.qty * item.conversion_factor,
'serial_no': item.serial_no,
+ 'batch_no': item.batch_no,
'voucher_type': voucher_type,
'company': company,
'allow_zero_valuation_rate': item.allow_zero_valuation_rate
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
index 4b645b9..666043b 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -338,14 +338,14 @@
.btn-add-to-wishlist {
svg use {
- stroke: #F47A7A;
+ --icon-stroke: #F47A7A;
}
}
.btn-view-in-wishlist {
svg use {
fill: #F47A7A;
- stroke: none;
+ --icon-stroke: none;
}
}
@@ -1022,7 +1022,7 @@
.not-wished {
cursor: pointer;
- stroke: #F47A7A !important;
+ --icon-stroke: #F47A7A !important;
&:hover {
fill: #F47A7A;
@@ -1030,7 +1030,7 @@
}
.wished {
- stroke: none;
+ --icon-stroke: none;
fill: #F47A7A !important;
}
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index 95f6c4e..080d517 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -83,8 +83,8 @@
"planned_qty",
"column_break_69",
"work_order_qty",
- "delivered_qty",
"produced_qty",
+ "delivered_qty",
"returned_qty",
"shopping_cart_section",
"additional_notes",
@@ -701,10 +701,8 @@
"width": "50px"
},
{
- "description": "For Production",
"fieldname": "produced_qty",
"fieldtype": "Float",
- "hidden": 1,
"label": "Produced Quantity",
"oldfieldname": "produced_qty",
"oldfieldtype": "Currency",
@@ -802,7 +800,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-10-05 12:27:25.014789",
+ "modified": "2022-02-21 13:55:08.883104",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
@@ -811,5 +809,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index 9650bc8..4d75e6e 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -180,14 +180,6 @@
() => frm.save(),
() => this.update_totals_section(frm.doc)
]);
- } else {
- frappe.run_serially([
- () => frm.doc.ignore_pricing_rule=1,
- () => frm.trigger('ignore_pricing_rule'),
- () => frm.doc.ignore_pricing_rule=0,
- () => frm.save(),
- () => this.update_totals_section(frm.doc)
- ]);
}
}
});
diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py
index 4441bb9..a4f2207 100644
--- a/erpnext/setup/utils.py
+++ b/erpnext/setup/utils.py
@@ -155,7 +155,7 @@
doc = frappe.new_doc(r.get("doctype"))
doc.update(r)
try:
- doc.insert(ignore_permissions=True)
+ doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
except frappe.DuplicateEntryError as e:
# pass DuplicateEntryError and continue
if e.args and e.args[0]==doc.doctype and e.args[1]==doc.name:
diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json
index fc4cf1d..967c572 100644
--- a/erpnext/stock/doctype/batch/batch.json
+++ b/erpnext/stock/doctype/batch/batch.json
@@ -9,6 +9,8 @@
"field_order": [
"sb_disabled",
"disabled",
+ "column_break_24",
+ "use_batchwise_valuation",
"sb_batch",
"batch_id",
"item",
@@ -186,6 +188,18 @@
"fieldtype": "Float",
"label": "Produced Qty",
"read_only": 1
+ },
+ {
+ "fieldname": "column_break_24",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "use_batchwise_valuation",
+ "fieldtype": "Check",
+ "label": "Use Batch-wise Valuation",
+ "read_only": 1,
+ "set_only_once": 1
}
],
"icon": "fa fa-archive",
@@ -193,10 +207,11 @@
"image_field": "image",
"links": [],
"max_attachments": 5,
- "modified": "2021-07-08 16:22:01.343105",
+ "modified": "2022-02-21 08:08:23.999236",
"modified_by": "Administrator",
"module": "Stock",
"name": "Batch",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -217,6 +232,7 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "batch_id",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index 96751d6..c9b4c14 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -110,11 +110,18 @@
def validate(self):
self.item_has_batch_enabled()
+ self.set_batchwise_valuation()
def item_has_batch_enabled(self):
if frappe.db.get_value("Item", self.item, "has_batch_no") == 0:
frappe.throw(_("The selected item cannot have Batch"))
+ def set_batchwise_valuation(self):
+ from erpnext.stock.stock_ledger import get_valuation_method
+
+ if self.is_new() and get_valuation_method(self.item) != "Moving Average":
+ self.use_batchwise_valuation = 1
+
def before_save(self):
has_expiry_date, shelf_life_in_days = frappe.db.get_value('Item', self.item, ['has_expiry_date', 'shelf_life_in_days'])
if not self.expiry_date and has_expiry_date and shelf_life_in_days:
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index 0a663c2..613dd3f 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -1,13 +1,21 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
+import json
+
import frappe
from frappe.exceptions import ValidationError
from frappe.utils import cint, flt
+from frappe.utils.data import add_to_date, getdate
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
+ create_stock_reconciliation,
+)
from erpnext.stock.get_item_details import get_item_details
+from erpnext.stock.stock_ledger import get_valuation_rate
from erpnext.tests.utils import ERPNextTestCase
@@ -300,6 +308,105 @@
details = get_item_details(args)
self.assertEqual(details.get('price_list_rate'), 400)
+
+ def test_basic_batch_wise_valuation(self, batch_qty = 100):
+ item_code = "_TestBatchWiseVal"
+ warehouse = "_Test Warehouse - _TC"
+ self.make_batch_item(item_code)
+
+ rates = [42, 420]
+
+ batches = {}
+ for rate in rates:
+ se = make_stock_entry(item_code=item_code, qty=10, rate=rate, target=warehouse)
+ batches[se.items[0].batch_no] = rate
+
+ LOW, HIGH = list(batches.keys())
+
+ # consume things out of order
+ consumption_plan = [
+ (HIGH, 1),
+ (LOW, 2),
+ (HIGH, 2),
+ (HIGH, 4),
+ (LOW, 6),
+ ]
+
+ stock_value = sum(rates) * 10
+ qty_after_transaction = 20
+ for batch, qty in consumption_plan:
+ # consume out of order
+ se = make_stock_entry(item_code=item_code, source=warehouse, qty=qty, batch_no=batch)
+
+ sle = frappe.get_last_doc("Stock Ledger Entry", {"is_cancelled": 0, "voucher_no": se.name})
+
+ stock_value_difference = sle.actual_qty * batches[sle.batch_no]
+ self.assertAlmostEqual(sle.stock_value_difference, stock_value_difference)
+
+ stock_value += stock_value_difference
+ self.assertAlmostEqual(sle.stock_value, stock_value)
+
+ qty_after_transaction += sle.actual_qty
+ self.assertAlmostEqual(sle.qty_after_transaction, qty_after_transaction)
+ self.assertAlmostEqual(sle.valuation_rate, stock_value / qty_after_transaction)
+
+ self.assertEqual(json.loads(sle.stock_queue), []) # queues don't apply on batched items
+
+ def test_moving_batch_valuation_rates(self):
+ item_code = "_TestBatchWiseVal"
+ warehouse = "_Test Warehouse - _TC"
+ self.make_batch_item(item_code)
+
+ def assertValuation(expected):
+ actual = get_valuation_rate(item_code, warehouse, "voucher_type", "voucher_no", batch_no=batch_no)
+ self.assertAlmostEqual(actual, expected)
+
+ se = make_stock_entry(item_code=item_code, qty=100, rate=10, target=warehouse)
+ batch_no = se.items[0].batch_no
+ assertValuation(10)
+
+ # consumption should never affect current valuation rate
+ make_stock_entry(item_code=item_code, qty=20, source=warehouse)
+ assertValuation(10)
+
+ make_stock_entry(item_code=item_code, qty=30, source=warehouse)
+ assertValuation(10)
+
+ # 50 * 10 = 500 current value, add more item with higher valuation
+ make_stock_entry(item_code=item_code, qty=50, rate=20, target=warehouse, batch_no=batch_no)
+ assertValuation(15)
+
+ # consuming again shouldn't do anything
+ make_stock_entry(item_code=item_code, qty=20, source=warehouse)
+ assertValuation(15)
+
+ # reset rate with stock reconiliation
+ create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=10, rate=25, batch_no=batch_no)
+ assertValuation(25)
+
+ make_stock_entry(item_code=item_code, qty=20, rate=20, target=warehouse, batch_no=batch_no)
+ assertValuation((20 * 20 + 10 * 25) / (10 + 20))
+
+
+ def test_update_batch_properties(self):
+ item_code = "_TestBatchWiseVal"
+ self.make_batch_item(item_code)
+
+ se = make_stock_entry(item_code=item_code, qty=100, rate=10, target="_Test Warehouse - _TC")
+ batch_no = se.items[0].batch_no
+ batch = frappe.get_doc("Batch", batch_no)
+
+ expiry_date = add_to_date(batch.manufacturing_date, days=30)
+
+ batch.expiry_date = expiry_date
+ batch.save()
+
+ batch.reload()
+
+ self.assertEqual(getdate(batch.expiry_date), getdate(expiry_date))
+
+
+
def create_batch(item_code, rate, create_item_price_for_batch):
pi = make_purchase_invoice(company="_Test Company",
warehouse= "Stores - _TC", cost_center = "Main - _TC", update_stock=1,
@@ -326,14 +433,13 @@
def make_new_batch(**args):
args = frappe._dict(args)
- try:
+ if frappe.db.exists("Batch", args.batch_id):
+ batch = frappe.get_doc("Batch", args.batch_id)
+ else:
batch = frappe.get_doc({
"doctype": "Batch",
"batch_id": args.batch_id,
"item": args.item_code,
}).insert()
- except frappe.DuplicateEntryError:
- batch = frappe.get_doc("Batch", args.batch_id)
-
return batch
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index dfc0918..ffea9c2 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -594,7 +594,7 @@
const increment = r.message.increment;
let values = [];
- for(var i = from; i <= to; i += increment) {
+ for(var i = from; i <= to; i = flt(i + increment, 6)) {
values.push(i);
}
attr_val_fields[d.attribute] = values;
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index b9e8b3f..494fb3b 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -398,6 +398,7 @@
if merge:
self.validate_properties_before_merge(new_name)
+ self.validate_duplicate_product_bundles_before_merge(old_name, new_name)
self.validate_duplicate_website_item_before_merge(old_name, new_name)
def after_rename(self, old_name, new_name, merge):
@@ -462,6 +463,20 @@
msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])
frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
+ def validate_duplicate_product_bundles_before_merge(self, old_name, new_name):
+ "Block merge if both old and new items have product bundles."
+ old_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": old_name})
+ new_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": new_name})
+
+ if old_bundle and new_bundle:
+ bundle_link = get_link_to_form("Product Bundle", old_bundle)
+ old_name, new_name = frappe.bold(old_name), frappe.bold(new_name)
+
+ msg = _("Please delete Product Bundle {0}, before merging {1} into {2}").format(
+ bundle_link, old_name, new_name
+ )
+ frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
+
def validate_duplicate_website_item_before_merge(self, old_name, new_name):
"""
Block merge if both old and new items have website items against them.
@@ -479,8 +494,9 @@
old_web_item = [d.get("name") for d in web_items if d.get("item_code") == old_name][0]
web_item_link = get_link_to_form("Website Item", old_web_item)
+ old_name, new_name = frappe.bold(old_name), frappe.bold(new_name)
- msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} and {new_name}"
+ msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} into {new_name}"
frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError)
def set_last_purchase_rate(self, new_name):
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index fd4df42..9491e17 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -15,6 +15,7 @@
get_variant,
)
from erpnext.stock.doctype.item.item import (
+ DataValidationError,
InvalidBarcode,
StockExistsForTemplate,
get_item_attribute,
@@ -388,6 +389,26 @@
self.assertTrue(frappe.db.get_value("Bin",
{"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"}))
+ def test_item_merging_with_product_bundle(self):
+ from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
+
+ create_item("Test Item Bundle Item 1", is_stock_item=False)
+ create_item("Test Item Bundle Item 2", is_stock_item=False)
+ create_item("Test Item inside Bundle")
+ bundle_items = ["Test Item inside Bundle"]
+
+ # make bundles for both items
+ bundle1 = make_product_bundle("Test Item Bundle Item 1", bundle_items, qty=2)
+ make_product_bundle("Test Item Bundle Item 2", bundle_items, qty=2)
+
+ with self.assertRaises(DataValidationError):
+ frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True)
+
+ bundle1.delete()
+ frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True)
+
+ self.assertFalse(frappe.db.exists("Item", "Test Item Bundle Item 1"))
+
def test_uom_conversion_factor(self):
if frappe.db.exists('Item', 'Test Item UOM'):
frappe.delete_doc('Item', 'Test Item UOM')
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index b39328f..51209ac 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -56,14 +56,13 @@
if actual_so_qty and (flt(so_items[so_no][item]) + already_indented > actual_so_qty):
frappe.throw(_("Material Request of maximum {0} can be made for Item {1} against Sales Order {2}").format(actual_so_qty - already_indented, item, so_no))
- # Validate
- # ---------------------
def validate(self):
super(MaterialRequest, self).validate()
self.validate_schedule_date()
self.check_for_on_hold_or_closed_status('Sales Order', 'sales_order')
self.validate_uom_is_integer("uom", "qty")
+ self.validate_material_request_type()
if not self.status:
self.status = "Draft"
@@ -83,6 +82,12 @@
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
+ def validate_material_request_type(self):
+ """ Validate fields in accordance with selected type """
+
+ if self.material_request_type != "Customer Provided":
+ self.customer = None
+
def set_title(self):
'''Set title as comma separated list of items'''
if not self.title:
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 5ab7929..d481689 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -1540,6 +1540,7 @@
"conversion_factor": args.conversion_factor or 1.0,
"stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0),
"serial_no": args.serial_no,
+ "batch_no": args.batch_no,
"stock_uom": args.stock_uom or "_Test UOM",
"uom": uom,
"cost_center": args.cost_center or frappe.get_cached_value('Company', pr.company, 'cost_center'),
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index c4b8131..5c9da3a 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -425,6 +425,7 @@
'posting_time' : frm.doc.posting_time,
'warehouse' : cstr(item.s_warehouse) || cstr(item.t_warehouse),
'serial_no' : item.serial_no,
+ 'batch_no' : item.batch_no,
'company' : frm.doc.company,
'qty' : item.s_warehouse ? -1*flt(item.transfer_qty) : flt(item.transfer_qty),
'voucher_type' : frm.doc.doctype,
@@ -457,6 +458,7 @@
'warehouse': cstr(child.s_warehouse) || cstr(child.t_warehouse),
'transfer_qty': child.transfer_qty,
'serial_no': child.serial_no,
+ 'batch_no': child.batch_no,
'qty': child.s_warehouse ? -1* child.transfer_qty : child.transfer_qty,
'posting_date': frm.doc.posting_date,
'posting_time': frm.doc.posting_time,
@@ -680,6 +682,7 @@
'warehouse' : cstr(d.s_warehouse) || cstr(d.t_warehouse),
'transfer_qty' : d.transfer_qty,
'serial_no' : d.serial_no,
+ 'batch_no' : d.batch_no,
'bom_no' : d.bom_no,
'expense_account' : d.expense_account,
'cost_center' : d.cost_center,
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 9ba007a..99cf4de 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -510,7 +510,7 @@
d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse,
self.doctype, self.name, d.allow_zero_valuation_rate,
currency=erpnext.get_company_currency(self.company), company=self.company,
- raise_error_if_no_rate=raise_error_if_no_rate)
+ raise_error_if_no_rate=raise_error_if_no_rate, batch_no=d.batch_no)
d.basic_rate = flt(d.basic_rate, d.precision("basic_rate"))
if d.is_process_loss:
@@ -541,6 +541,7 @@
"posting_time": self.posting_time,
"qty": item.s_warehouse and -1*flt(item.transfer_qty) or flt(item.transfer_qty),
"serial_no": item.serial_no,
+ "batch_no": item.batch_no,
"voucher_type": self.doctype,
"voucher_no": self.name,
"company": self.company,
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 306f2c3..c5afa49 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -44,6 +44,7 @@
class TestStockEntry(ERPNextTestCase):
def tearDown(self):
+ frappe.db.rollback()
frappe.set_user("Administrator")
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
@@ -565,6 +566,7 @@
st1.set_stock_entry_type()
st1.insert()
st1.submit()
+ st1.cancel()
frappe.set_user("Administrator")
remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com")
@@ -689,6 +691,8 @@
bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item",
"is_default": 1, "docstatus": 1})
+ make_item_variant() # make variant of _Test Variant Item if absent
+
work_order = frappe.new_doc("Work Order")
work_order.update({
"company": "_Test Company",
@@ -1023,13 +1027,10 @@
# Check if FG cost is calculated based on RM total cost
# RM total cost = 200, FG rate = 200/4(FG qty) = 50
- self.assertEqual(se.items[1].basic_rate, 50)
+ self.assertEqual(se.items[1].basic_rate, flt(se.items[0].basic_rate/4))
self.assertEqual(se.value_difference, 0.0)
self.assertEqual(se.total_incoming_value, se.total_outgoing_value)
- # teardown
- se.delete()
-
@change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_future_negative_sle(self):
# Initialize item, batch, warehouse, opening qty
@@ -1107,6 +1108,52 @@
posting_date='2021-09-02', # backdated consumption of 2nd batch
purpose='Material Issue')
+ def test_multi_batch_value_diff(self):
+ """ Test value difference on stock entry in case of multi-batch.
+ | Stock entry | batch | qty | rate | value diff on SE |
+ | --- | --- | --- | --- | --- |
+ | receipt | A | 1 | 10 | 30 |
+ | receipt | B | 1 | 20 | |
+ | issue | A | -1 | 10 | -30 (to assert after submit) |
+ | issue | B | -1 | 20 | |
+ """
+ from erpnext.stock.doctype.batch.test_batch import TestBatch
+
+ batch_nos = []
+
+ item_code = '_TestMultibatchFifo'
+ TestBatch.make_batch_item(item_code)
+ warehouse = '_Test Warehouse - _TC'
+ receipt = make_stock_entry(
+ item_code=item_code,
+ qty=1,
+ rate=10,
+ to_warehouse=warehouse,
+ purpose='Material Receipt',
+ do_not_save=True
+ )
+ receipt.append("items", frappe.copy_doc(receipt.items[0], ignore_no_copy=False).update({"basic_rate": 20}) )
+ receipt.save()
+ receipt.submit()
+ batch_nos.extend(row.batch_no for row in receipt.items)
+ self.assertEqual(receipt.value_difference, 30)
+
+ issue = make_stock_entry(
+ item_code=item_code,
+ qty=1,
+ from_warehouse=warehouse,
+ purpose='Material Issue',
+ do_not_save=True
+ )
+ issue.append("items", frappe.copy_doc(issue.items[0], ignore_no_copy=False))
+ for row, batch_no in zip(issue.items, batch_nos):
+ row.batch_no = batch_no
+ issue.save()
+ issue.submit()
+
+ issue.reload() # reload because reposting current voucher updates rate
+ self.assertEqual(issue.value_difference, -30)
+
def make_serialized_item(**args):
args = frappe._dict(args)
se = frappe.copy_doc(test_records[0])
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index a1030d5..0864ece 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -1,6 +1,10 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
+import json
+from operator import itemgetter
+from uuid import uuid4
+
import frappe
from frappe.core.page.permission_manager.permission_manager import reset
from frappe.utils import add_days, today
@@ -349,6 +353,317 @@
frappe.set_user("Administrator")
user.remove_roles("Stock Manager")
+ def test_batchwise_item_valuation_moving_average(self):
+ item, warehouses, batches = setup_item_valuation_test(valuation_method="Moving Average")
+
+ # Incoming Entries for Stock Value check
+ pr_entry_list = [
+ (item, warehouses[0], batches[0], 1, 100),
+ (item, warehouses[0], batches[1], 1, 50),
+ (item, warehouses[0], batches[0], 1, 150),
+ (item, warehouses[0], batches[1], 1, 100),
+ ]
+ prs = create_purchase_receipt_entries_for_batchwise_item_valuation_test(pr_entry_list)
+ sle_details = fetch_sle_details_for_doc_list(prs, ['stock_value'])
+ sv_list = [d['stock_value'] for d in sle_details]
+ expected_sv = [100, 150, 300, 400]
+ self.assertEqual(expected_sv, sv_list, "Incorrect 'Stock Value' values")
+
+ # Outgoing Entries for Stock Value Difference check
+ dn_entry_list = [
+ (item, warehouses[0], batches[1], 1, 200),
+ (item, warehouses[0], batches[0], 1, 200),
+ (item, warehouses[0], batches[1], 1, 200),
+ (item, warehouses[0], batches[0], 1, 200)
+ ]
+ dns = create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list)
+ sle_details = fetch_sle_details_for_doc_list(dns, ['stock_value_difference'])
+ svd_list = [-1 * d['stock_value_difference'] for d in sle_details]
+ expected_incoming_rates = expected_abs_svd = [75, 125, 75, 125]
+
+ self.assertEqual(expected_abs_svd, svd_list, "Incorrect 'Stock Value Difference' values")
+ for dn, incoming_rate in zip(dns, expected_incoming_rates):
+ self.assertEqual(
+ dn.items[0].incoming_rate, incoming_rate,
+ "Incorrect 'Incoming Rate' values fetched for DN items"
+ )
+
+
+ def assertSLEs(self, doc, expected_sles):
+ """ Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
+ sles = frappe.get_all("Stock Ledger Entry", fields=["*"],
+ filters={"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled":0},
+ order_by="timestamp(posting_date, posting_time), creation")
+
+ for exp_sle, act_sle in zip(expected_sles, sles):
+ for k, v in exp_sle.items():
+ act_value = act_sle[k]
+ if k == "stock_queue":
+ act_value = json.loads(act_value)
+ if act_value and act_value[0][0] == 0:
+ # ignore empty fifo bins
+ continue
+
+ self.assertEqual(v, act_value, msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}")
+
+
+ def test_batchwise_item_valuation_stock_reco(self):
+ item, warehouses, batches = setup_item_valuation_test()
+ state = {
+ "stock_value" : 0.0,
+ "qty": 0.0
+ }
+ def update_invariants(exp_sles):
+ for sle in exp_sles:
+ state["stock_value"] += sle["stock_value_difference"]
+ state["qty"] += sle["actual_qty"]
+ sle["stock_value"] = state["stock_value"]
+ sle["qty_after_transaction"] = state["qty"]
+
+ osr1 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=10, rate=100, batch_no=batches[1])
+ expected_sles = [
+ {"actual_qty": 10, "stock_value_difference": 1000},
+ ]
+ update_invariants(expected_sles)
+ self.assertSLEs(osr1, expected_sles)
+
+ osr2 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=13, rate=200, batch_no=batches[0])
+ expected_sles = [
+ {"actual_qty": 13, "stock_value_difference": 200*13},
+ ]
+ update_invariants(expected_sles)
+ self.assertSLEs(osr2, expected_sles)
+
+ sr1 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=5, rate=50, batch_no=batches[1])
+
+ expected_sles = [
+ {"actual_qty": -10, "stock_value_difference": -10 * 100},
+ {"actual_qty": 5, "stock_value_difference": 250}
+ ]
+ update_invariants(expected_sles)
+ self.assertSLEs(sr1, expected_sles)
+
+ sr2 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=20, rate=75, batch_no=batches[0])
+ expected_sles = [
+ {"actual_qty": -13, "stock_value_difference": -13 * 200},
+ {"actual_qty": 20, "stock_value_difference": 20 * 75}
+ ]
+ update_invariants(expected_sles)
+ self.assertSLEs(sr2, expected_sles)
+
+ def test_batch_wise_valuation_across_warehouse(self):
+ item_code, warehouses, batches = setup_item_valuation_test()
+ source = warehouses[0]
+ target = warehouses[1]
+
+ unrelated_batch = make_stock_entry(item_code=item_code, target=source, batch_no=batches[1],
+ qty=5, rate=10)
+ self.assertSLEs(unrelated_batch, [
+ {"actual_qty": 5, "stock_value_difference": 10 * 5},
+ ])
+
+ reciept = make_stock_entry(item_code=item_code, target=source, batch_no=batches[0], qty=5, rate=10)
+ self.assertSLEs(reciept, [
+ {"actual_qty": 5, "stock_value_difference": 10 * 5},
+ ])
+
+ transfer = make_stock_entry(item_code=item_code, source=source, target=target, batch_no=batches[0], qty=5)
+ self.assertSLEs(transfer, [
+ {"actual_qty": -5, "stock_value_difference": -10 * 5, "warehouse": source},
+ {"actual_qty": 5, "stock_value_difference": 10 * 5, "warehouse": target}
+ ])
+
+ backdated_receipt = make_stock_entry(item_code=item_code, target=source, batch_no=batches[0],
+ qty=5, rate=20, posting_date=add_days(today(), -1))
+ self.assertSLEs(backdated_receipt, [
+ {"actual_qty": 5, "stock_value_difference": 20 * 5},
+ ])
+
+ # check reposted average rate in *future* transfer
+ self.assertSLEs(transfer, [
+ {"actual_qty": -5, "stock_value_difference": -15 * 5, "warehouse": source, "stock_value": 15 * 5 + 10 * 5},
+ {"actual_qty": 5, "stock_value_difference": 15 * 5, "warehouse": target, "stock_value": 15 * 5}
+ ])
+
+ transfer_unrelated = make_stock_entry(item_code=item_code, source=source,
+ target=target, batch_no=batches[1], qty=5)
+ self.assertSLEs(transfer_unrelated, [
+ {"actual_qty": -5, "stock_value_difference": -10 * 5, "warehouse": source, "stock_value": 15 * 5},
+ {"actual_qty": 5, "stock_value_difference": 10 * 5, "warehouse": target, "stock_value": 15 * 5 + 10 * 5}
+ ])
+
+ def test_intermediate_average_batch_wise_valuation(self):
+ """ A batch has moving average up until posting time,
+ check if same is respected when backdated entry is inserted in middle"""
+ item_code, warehouses, batches = setup_item_valuation_test()
+ warehouse = warehouses[0]
+
+ batch = batches[0]
+
+ yesterday = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batch,
+ qty=1, rate=10, posting_date=add_days(today(), -1))
+ self.assertSLEs(yesterday, [
+ {"actual_qty": 1, "stock_value_difference": 10},
+ ])
+
+ tomorrow = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0],
+ qty=1, rate=30, posting_date=add_days(today(), 1))
+ self.assertSLEs(tomorrow, [
+ {"actual_qty": 1, "stock_value_difference": 30},
+ ])
+
+ create_today = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0],
+ qty=1, rate=20)
+ self.assertSLEs(create_today, [
+ {"actual_qty": 1, "stock_value_difference": 20},
+ ])
+
+ consume_today = make_stock_entry(item_code=item_code, source=warehouse, batch_no=batches[0],
+ qty=1)
+ self.assertSLEs(consume_today, [
+ {"actual_qty": -1, "stock_value_difference": -15},
+ ])
+
+ consume_tomorrow = make_stock_entry(item_code=item_code, source=warehouse, batch_no=batches[0],
+ qty=2, posting_date=add_days(today(), 2))
+ self.assertSLEs(consume_tomorrow, [
+ {"stock_value_difference": -(30 + 15), "stock_value": 0, "qty_after_transaction": 0},
+ ])
+
+ def test_legacy_item_valuation_stock_entry(self):
+ columns = [
+ 'stock_value_difference',
+ 'stock_value',
+ 'actual_qty',
+ 'qty_after_transaction',
+ 'stock_queue',
+ ]
+ item, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0)
+
+ def check_sle_details_against_expected(sle_details, expected_sle_details, detail, columns):
+ for i, (sle_vals, ex_sle_vals) in enumerate(zip(sle_details, expected_sle_details)):
+ for col, sle_val, ex_sle_val in zip(columns, sle_vals, ex_sle_vals):
+ if col == 'stock_queue':
+ sle_val = get_stock_value_from_q(sle_val)
+ ex_sle_val = get_stock_value_from_q(ex_sle_val)
+ self.assertEqual(
+ sle_val, ex_sle_val,
+ f"Incorrect {col} value on transaction #: {i} in {detail}"
+ )
+
+ # List used to defer assertions to prevent commits cause of error skipped rollback
+ details_list = []
+
+
+ # Test Material Receipt Entries
+ se_entry_list_mr = [
+ (item, None, warehouses[0], batches[0], 1, 50, "2021-01-21"),
+ (item, None, warehouses[0], batches[1], 1, 100, "2021-01-23"),
+ ]
+ ses = create_stock_entry_entries_for_batchwise_item_valuation_test(
+ se_entry_list_mr, "Material Receipt"
+ )
+ sle_details = fetch_sle_details_for_doc_list(ses, columns=columns, as_dict=0)
+ expected_sle_details = [
+ (50.0, 50.0, 1.0, 1.0, '[[1.0, 50.0]]'),
+ (100.0, 150.0, 1.0, 2.0, '[[1.0, 50.0], [1.0, 100.0]]'),
+ ]
+ details_list.append((
+ sle_details, expected_sle_details,
+ "Material Receipt Entries", columns
+ ))
+
+
+ # Test Material Issue Entries
+ se_entry_list_mi = [
+ (item, warehouses[0], None, batches[1], 1, None, "2021-01-29"),
+ ]
+ ses = create_stock_entry_entries_for_batchwise_item_valuation_test(
+ se_entry_list_mi, "Material Issue"
+ )
+ sle_details = fetch_sle_details_for_doc_list(ses, columns=columns, as_dict=0)
+ expected_sle_details = [
+ (-50.0, 100.0, -1.0, 1.0, '[[1, 100.0]]')
+ ]
+ details_list.append((
+ sle_details, expected_sle_details,
+ "Material Issue Entries", columns
+ ))
+
+
+ # Run assertions
+ for details in details_list:
+ check_sle_details_against_expected(*details)
+
+ def test_mixed_valuation_batches_fifo(self):
+ item_code, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0)
+ warehouse = warehouses[0]
+
+ state = {
+ "qty": 0.0,
+ "stock_value": 0.0
+ }
+ def update_invariants(exp_sles):
+ for sle in exp_sles:
+ state["stock_value"] += sle["stock_value_difference"]
+ state["qty"] += sle["actual_qty"]
+ sle["stock_value"] = state["stock_value"]
+ sle["qty_after_transaction"] = state["qty"]
+ return exp_sles
+
+ old1 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0],
+ qty=10, rate=10)
+ self.assertSLEs(old1, update_invariants([
+ {"actual_qty": 10, "stock_value_difference": 10*10, "stock_queue": [[10, 10]]},
+ ]))
+ old2 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[1],
+ qty=10, rate=20)
+ self.assertSLEs(old2, update_invariants([
+ {"actual_qty": 10, "stock_value_difference": 10*20, "stock_queue": [[10, 10], [10, 20]]},
+ ]))
+ old3 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0],
+ qty=5, rate=15)
+
+ self.assertSLEs(old3, update_invariants([
+ {"actual_qty": 5, "stock_value_difference": 5*15, "stock_queue": [[10, 10], [10, 20], [5, 15]]},
+ ]))
+
+ new1 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=40)
+ batches.append(new1.items[0].batch_no)
+ # assert old queue remains
+ self.assertSLEs(new1, update_invariants([
+ {"actual_qty": 10, "stock_value_difference": 10*40, "stock_queue": [[10, 10], [10, 20], [5, 15]]},
+ ]))
+
+ new2 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=42)
+ batches.append(new2.items[0].batch_no)
+ self.assertSLEs(new2, update_invariants([
+ {"actual_qty": 10, "stock_value_difference": 10*42, "stock_queue": [[10, 10], [10, 20], [5, 15]]},
+ ]))
+
+ # consume old batch as per FIFO
+ consume_old1 = make_stock_entry(item_code=item_code, source=warehouse, qty=15, batch_no=batches[0])
+ self.assertSLEs(consume_old1, update_invariants([
+ {"actual_qty": -15, "stock_value_difference": -10*10 - 5*20, "stock_queue": [[5, 20], [5, 15]]},
+ ]))
+
+ # consume new batch as per batch
+ consume_new2 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-1])
+ self.assertSLEs(consume_new2, update_invariants([
+ {"actual_qty": -10, "stock_value_difference": -10*42, "stock_queue": [[5, 20], [5, 15]]},
+ ]))
+
+ # finish all old batches
+ consume_old2 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[1])
+ self.assertSLEs(consume_old2, update_invariants([
+ {"actual_qty": -10, "stock_value_difference": -5*20 - 5*15, "stock_queue": []},
+ ]))
+
+ # finish all new batches
+ consume_new1 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-2])
+ self.assertSLEs(consume_new1, update_invariants([
+ {"actual_qty": -10, "stock_value_difference": -10*40, "stock_queue": []},
+ ]))
def create_repack_entry(**args):
args = frappe._dict(args)
@@ -412,3 +727,118 @@
make_item(d, properties=properties)
return items
+
+def setup_item_valuation_test(valuation_method="FIFO", suffix=None, use_batchwise_valuation=1, batches_list=['X', 'Y']):
+ from erpnext.stock.doctype.batch.batch import make_batch
+ from erpnext.stock.doctype.item.test_item import make_item
+ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+ if not suffix:
+ suffix = get_unique_suffix()
+
+ item = make_item(
+ f"IV - Test Item {valuation_method} {suffix}",
+ dict(valuation_method=valuation_method, has_batch_no=1, create_new_batch=1)
+ )
+ warehouses = [create_warehouse(f"IV - Test Warehouse {i}") for i in ['J', 'K']]
+ batches = [f"IV - Test Batch {i} {valuation_method} {suffix}" for i in batches_list]
+
+ for i, batch_id in enumerate(batches):
+ if not frappe.db.exists("Batch", batch_id):
+ ubw = use_batchwise_valuation
+ if isinstance(use_batchwise_valuation, (list, tuple)):
+ ubw = use_batchwise_valuation[i]
+ batch = frappe.get_doc(frappe._dict(
+ doctype="Batch",
+ batch_id=batch_id,
+ item=item.item_code,
+ use_batchwise_valuation=ubw
+ )
+ ).insert()
+ batch.use_batchwise_valuation = ubw
+ batch.db_update()
+
+ return item.item_code, warehouses, batches
+
+def create_purchase_receipt_entries_for_batchwise_item_valuation_test(pr_entry_list):
+ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+ prs = []
+
+ for item, warehouse, batch_no, qty, rate in pr_entry_list:
+ pr = make_purchase_receipt(item=item, warehouse=warehouse, qty=qty, rate=rate, batch_no=batch_no)
+ prs.append(pr)
+
+ return prs
+
+def create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list):
+ from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
+ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
+ dns = []
+ for item, warehouse, batch_no, qty, rate in dn_entry_list:
+ so = make_sales_order(
+ rate=rate,
+ qty=qty,
+ item=item,
+ warehouse=warehouse,
+ against_blanket_order=0
+ )
+
+ dn = make_delivery_note(so.name)
+ dn.items[0].batch_no = batch_no
+ dn.insert()
+ dn.submit()
+ dns.append(dn)
+ return dns
+
+def fetch_sle_details_for_doc_list(doc_list, columns, as_dict=1):
+ return frappe.db.sql(f"""
+ SELECT { ', '.join(columns)}
+ FROM `tabStock Ledger Entry`
+ WHERE
+ voucher_no IN %(voucher_nos)s
+ and docstatus = 1
+ ORDER BY timestamp(posting_date, posting_time) ASC, CREATION ASC
+ """, dict(
+ voucher_nos=[doc.name for doc in doc_list]
+ ), as_dict=as_dict)
+
+def get_stock_value_from_q(q):
+ return sum(r*q for r,q in json.loads(q))
+
+def create_stock_entry_entries_for_batchwise_item_valuation_test(se_entry_list, purpose):
+ ses = []
+ for item, source, target, batch, qty, rate, posting_date in se_entry_list:
+ args = dict(
+ item_code=item,
+ qty=qty,
+ company="_Test Company",
+ batch_no=batch,
+ posting_date=posting_date,
+ purpose=purpose
+ )
+
+ if purpose == "Material Receipt":
+ args.update(
+ dict(to_warehouse=target, rate=rate)
+ )
+
+ elif purpose == "Material Issue":
+ args.update(
+ dict(from_warehouse=source)
+ )
+
+ elif purpose == "Material Transfer":
+ args.update(
+ dict(from_warehouse=source, to_warehouse=target)
+ )
+
+ else:
+ raise ValueError(f"Invalid purpose: {purpose}")
+ ses.append(make_stock_entry(**args))
+
+ return ses
+
+def get_unique_suffix():
+ # Used to isolate valuation sensitive
+ # tests to prevent future tests from failing.
+ return str(uuid4())[:8].upper()
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 86af0a0..2ffe127 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -200,7 +200,6 @@
def test_stock_reco_for_batch_item(self):
to_delete_records = []
- to_delete_serial_nos = []
# Add new serial nos
item_code = "Stock-Reco-batch-Item-1"
@@ -208,20 +207,22 @@
sr = create_stock_reconciliation(item_code=item_code,
warehouse = warehouse, qty=5, rate=200, do_not_submit=1)
- sr.save(ignore_permissions=True)
+ sr.save()
sr.submit()
- self.assertTrue(sr.items[0].batch_no)
+ batch_no = sr.items[0].batch_no
+ self.assertTrue(batch_no)
to_delete_records.append(sr.name)
sr1 = create_stock_reconciliation(item_code=item_code,
- warehouse = warehouse, qty=6, rate=300, batch_no=sr.items[0].batch_no)
+ warehouse = warehouse, qty=6, rate=300, batch_no=batch_no)
args = {
"item_code": item_code,
"warehouse": warehouse,
"posting_date": nowdate(),
"posting_time": nowtime(),
+ "batch_no": batch_no,
}
valuation_rate = get_incoming_rate(args)
@@ -230,7 +231,7 @@
sr2 = create_stock_reconciliation(item_code=item_code,
- warehouse = warehouse, qty=0, rate=0, batch_no=sr.items[0].batch_no)
+ warehouse = warehouse, qty=0, rate=0, batch_no=batch_no)
stock_value = get_stock_value_on(warehouse, nowdate(), item_code)
self.assertEqual(stock_value, 0)
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
index cb35bf7..7826d34 100644
--- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
+++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
@@ -60,6 +60,9 @@
fifo_qty += qty
fifo_value += qty * rate
+ if sle.actual_qty < 0:
+ sle.consumption_rate = sle.stock_value_difference / sle.actual_qty
+
balance_qty += sle.actual_qty
balance_stock_value += sle.stock_value_difference
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
@@ -90,6 +93,9 @@
sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value
sle.fifo_difference_diff = sle.fifo_stock_diff - sle.stock_value_difference
+ if sle.batch_no:
+ sle.use_batchwise_valuation = frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True)
+
return sles
@@ -135,6 +141,11 @@
"options": "Batch",
},
{
+ "fieldname": "use_batchwise_valuation",
+ "fieldtype": "Check",
+ "label": "Batchwise Valuation",
+ },
+ {
"fieldname": "actual_qty",
"fieldtype": "Float",
"label": "Qty Change",
@@ -145,9 +156,9 @@
"label": "Incoming Rate",
},
{
- "fieldname": "outgoing_rate",
+ "fieldname": "consumption_rate",
"fieldtype": "Float",
- "label": "Outgoing Rate",
+ "label": "Consumption Rate",
},
{
"fieldname": "qty_after_transaction",
diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py
index 525af40..76c2079 100644
--- a/erpnext/stock/report/test_reports.py
+++ b/erpnext/stock/report/test_reports.py
@@ -73,10 +73,11 @@
def test_execute_all_stock_reports(self):
"""Test that all script report in stock modules are executable with supported filters"""
for report, filter in REPORT_FILTER_TEST_CASES:
- execute_script_report(
- report_name=report,
- module="Stock",
- filters=filter,
- default_filters=DEFAULT_FILTERS,
- optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
- )
+ with self.subTest(report=report):
+ execute_script_report(
+ report_name=report,
+ module="Stock",
+ filters=filter,
+ default_filters=DEFAULT_FILTERS,
+ optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
+ )
diff --git a/erpnext/stock/spec/README.md b/erpnext/stock/spec/README.md
new file mode 100644
index 0000000..f5a3501
--- /dev/null
+++ b/erpnext/stock/spec/README.md
@@ -0,0 +1,103 @@
+# Implementation notes for Stock Ledger
+
+
+## Important files
+
+- `stock/stock_ledger.py`
+- `controllers/stock_controller.py`
+- `stock/valuation.py`
+
+## What is in an Stock Ledger Entry (SLE)?
+
+Stock Ledger Entry is a single row in the Stock Ledger. It signifies some
+modification of stock for a particular Item in the specified warehouse.
+
+- `item_code`: item for which ledger entry is made
+- `warehouse`: warehouse where inventory is affected
+- `actual_qty`: change in qty
+- `qty_after_transaction`: quantity available after the transaction is processed
+- `incoming_rate`: rate at which inventory was received.
+- `is_cancelled`: if 1 then stock ledger entry is cancelled and should not be used
+for any business logic except for the code that handles cancellation.
+- `posting_date` & `posting_time`: Specify the temporal ordering of stock ledger
+ entries. Ties are broken by `creation` timestamp.
+- `voucher_type`: Many transaction can create SLE, e.g. Stock Entry, Purchase
+ Invoice
+- `voucher_no`: `name` of the transaction that created SLE
+- `voucher_detail_no`: `name` of the child table row from parent transaction
+ that created the SLE.
+- `dependant_sle_voucher_detail_no`: cross-warehouse transfers need this
+ reference in order to update dependent warehouse rates in case of change in
+ rate.
+- `recalculate_rate`: if this is checked in/out rates are recomputed on
+ transactions.
+- `valuation_rate`: current average valuation rate.
+- `stock_value`: current total stock value
+- `stock_value_difference`: stock value difference made between last and current
+ entry. This value is booked in accounting ledger.
+- `stock_queue`: if FIFO/LIFO is used this represents queue/stack maintained for
+ computing incoming rate for inventory getting consumed.
+- `batch_no`: batch no for which stock entry is made; each stock entry can only
+ affect one batch number.
+- `serial_no`: newline separated list of serial numbers that were added (if
+ actual_qty > 0) or else removed. Currently multiple serial nos can have single
+ SLE but this will likely change in future.
+
+
+## Implementation of Stock Ledger
+
+Stock Ledger Entry affects stock of combinations of (item_code, warehouse) and
+optionally batch no if specified. For simplicity, lets avoid batch no. for now.
+
+
+Stock Ledger Entry table stores stock ledger for all combinations of item_code
+and warehouse. So whenever any operations are to be performed on said
+item-warehouse combination stock ledger is filtered and sorted by posting
+datetime. A typical query that will give you individual ledger looks like this:
+
+```sql
+select *
+from `tabStock Ledger Entry` as sle
+where
+ is_cancelled = 0 --- cancelled entries don't affect ledger
+ and item_code = 'item_code' and warehouse = 'warehouse_name'
+order by timestamp(posting_date, posting_time), creation
+```
+
+New entry is just an update to the last entry which is found by looking at last
+row in the filter ledger.
+
+
+### Serial nos
+
+Serial numbers do not follow any valuation method configuration and they are
+consumed at rate they were produced unless they are grouped in which case they
+are consumed at weighted average rate.
+
+
+### Batch Nos
+
+Batches are currently NOT consumed as per batch wise valuation rate, instead
+global FIFO queue for the item is used for valuation rate.
+
+
+## Creation process of SLEs
+
+- SLE creation is usually triggered by Stock Transactions using a method
+ conventionally named `update_stock_ledger()` This might not be defined for
+ stock transaction and could be specified somewhere in inheritance hierarchy of
+ controllers.
+- This method produces SLE objects which are processed by `make_sl_entries` in
+ `stock_ledger.py` which commits the SLE to database.
+- `update_entries_after` class is used to process ONLY the inserted SLE's queue
+ and valuation.
+- The change in qty is propagated to future entries immediately. Valuation and
+ queue for future entries is processed in background using repost item
+ valuation.
+
+
+## Accounting impact
+
+- Accounting impact for stock transaction is handled by `get_gl_entries()`
+ method on controllers. Each transaction has different business logic for
+ booking the accounting impact.
diff --git a/erpnext/stock/spec/reposting.md b/erpnext/stock/spec/reposting.md
new file mode 100644
index 0000000..b0d59fe
--- /dev/null
+++ b/erpnext/stock/spec/reposting.md
@@ -0,0 +1,38 @@
+# Stock Reposting
+
+Stock "reposting" is process of re-processing Stock Ledger Entry and GL Entries
+in event of backdated stock transaction.
+
+*Backdated stock transaction*: Any stock transaction for which some
+item-warehouse combination has a future transactions.
+
+## Why is this required?
+Stock Ledger is stateful, it maintains queue, qty at any
+point in time. So if you do a backdated transaction all future values change,
+queues need to be re-evaluated etc. Watch Nabin and Rohit's conference
+presentation for explanation: https://www.youtube.com/watch?v=mw3WAnekGIM
+
+## How is this implemented?
+Whenever backdated transaction is detected, instead of
+fully processing it while submitting, the processing is queued using "Repost
+Item Valuation" doctype. Every hour a scheduled job runs and processes this
+queue (for up to maximum of 25 minutes)
+
+
+## Queue implementation
+- "Repost item valuation" (RIV) is automatically submitted from backdated transactions. (check stock_controller.py)
+- Draft and cancelled RIV are ignored.
+- Keep filter of "submitted" documents when doing anything with RIVs.
+- The default status is "Queued".
+- When background job runs, it picks the oldest pending reposts and changes the status to "In Progress" and when it finishes it
+changes to "Completed"
+- There are two more status: "Failed" when reposting failed and "Skipped" when reposting is deemed not necessary so it's skipped.
+- technical detail: Entry point for whole process is "repost_entries" function in repost_item_valuation.py
+
+
+## How to identify broken stock data:
+There are 4 major reports for checking broken stock data:
+- Incorrect balance qty after the transaction - to check if the running total of qty isn't correct.
+- Incorrect stock value report - to check incorrect value books in accounts for stock transactions
+- Incorrect serial no valuation -specific to serial nos
+- Stock ledger invariant check - combined report for checking qty, running total, queue, balance value etc
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 00ca81f..1b90086 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -8,7 +8,9 @@
import frappe
from frappe import _
from frappe.model.meta import get_field_precision
+from frappe.query_builder.functions import Sum
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
+from pypika import CustomFunction
import erpnext
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
@@ -17,14 +19,13 @@
get_or_make_bin,
get_valuation_method,
)
-from erpnext.stock.valuation import FIFOValuation, LIFOValuation
+from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
class NegativeStockError(frappe.ValidationError): pass
class SerialNoExistsInFutureTransaction(frappe.ValidationError):
pass
-_exceptions = frappe.local('stockledger_exceptions')
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
from erpnext.controllers.stock_controller import future_sle_exists
@@ -447,6 +448,8 @@
self.wh_data.qty_after_transaction = sle.qty_after_transaction
self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
+ elif sle.batch_no and frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True):
+ self.update_batched_values(sle)
else:
if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no:
# assert
@@ -462,10 +465,11 @@
self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
else:
self.update_queue_values(sle)
- self.wh_data.qty_after_transaction += flt(sle.actual_qty)
# rounding as per precision
self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision)
+ if not self.wh_data.qty_after_transaction:
+ self.wh_data.stock_value = 0.0
stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
self.wh_data.prev_stock_value = self.wh_data.stock_value
@@ -481,6 +485,7 @@
if not self.args.get("sle_id"):
self.update_outgoing_rate_on_transaction(sle)
+
def validate_negative_stock(self, sle):
"""
validate negative stock for entries current datetime onwards
@@ -629,9 +634,7 @@
if not self.wh_data.valuation_rate and sle.voucher_detail_no:
allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_rate:
- self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
- sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
- currency=erpnext.get_company_currency(sle.company), company=sle.company)
+ self.wh_data.valuation_rate = self.get_fallback_rate(sle)
def get_incoming_value_for_serial_nos(self, sle, serial_nos):
# get rate from serial nos within same company
@@ -697,46 +700,70 @@
if not self.wh_data.valuation_rate and sle.voucher_detail_no:
allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_valuation_rate:
- self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
- sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
- currency=erpnext.get_company_currency(sle.company), company=sle.company)
+ self.wh_data.valuation_rate = self.get_fallback_rate(sle)
def update_queue_values(self, sle):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
outgoing_rate = flt(sle.outgoing_rate)
+ self.wh_data.qty_after_transaction = round_off_if_near_zero(self.wh_data.qty_after_transaction + actual_qty)
+
if self.valuation_method == "LIFO":
stock_queue = LIFOValuation(self.wh_data.stock_queue)
else:
stock_queue = FIFOValuation(self.wh_data.stock_queue)
+ _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
+
if actual_qty > 0:
stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
else:
def rate_generator() -> float:
allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_valuation_rate:
- return get_valuation_rate(sle.item_code, sle.warehouse,
- sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
- currency=erpnext.get_company_currency(sle.company), company=sle.company)
+ return self.get_fallback_rate(sle)
else:
return 0.0
stock_queue.remove_stock(qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator)
- stock_qty, stock_value = stock_queue.get_total_stock_and_value()
+ _qty, stock_value = stock_queue.get_total_stock_and_value()
+
+ stock_value_difference = stock_value - prev_stock_value
self.wh_data.stock_queue = stock_queue.state
- self.wh_data.stock_value = stock_value
- if stock_qty:
- self.wh_data.valuation_rate = stock_value / stock_qty
-
+ self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference)
if not self.wh_data.stock_queue:
self.wh_data.stock_queue.append([0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate])
+ if self.wh_data.qty_after_transaction:
+ self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
+ def update_batched_values(self, sle):
+ incoming_rate = flt(sle.incoming_rate)
+ actual_qty = flt(sle.actual_qty)
+
+ self.wh_data.qty_after_transaction = round_off_if_near_zero(self.wh_data.qty_after_transaction + actual_qty)
+
+ if actual_qty > 0:
+ stock_value_difference = incoming_rate * actual_qty
+ else:
+ outgoing_rate = get_batch_incoming_rate(item_code=sle.item_code,
+ warehouse=sle.warehouse, batch_no=sle.batch_no, posting_date=sle.posting_date,
+ posting_time=sle.posting_time, creation=sle.creation)
+ if outgoing_rate is None:
+ # This can *only* happen if qty available for the batch is zero.
+ # in such case fall back various other rates.
+ # future entries will correct the overall accounting as each
+ # batch individually uses moving average rates.
+ outgoing_rate = self.get_fallback_rate(sle)
+ stock_value_difference = outgoing_rate * actual_qty
+
+ self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference)
+ if self.wh_data.qty_after_transaction:
+ self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
ref_item_dt = ""
@@ -751,6 +778,13 @@
else:
return 0
+ def get_fallback_rate(self, sle) -> float:
+ """When exact incoming rate isn't available use any of other "average" rates as fallback.
+ This should only get used for negative stock."""
+ return get_valuation_rate(sle.item_code, sle.warehouse,
+ sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
+ currency=erpnext.get_company_currency(sle.company), company=sle.company, batch_no=sle.batch_no)
+
def get_sle_before_datetime(self, args):
"""get previous stock ledger entry before current time-bucket"""
sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
@@ -897,22 +931,72 @@
['item_code', 'warehouse', 'posting_date', 'posting_time', 'timestamp(posting_date, posting_time) as timestamp'],
as_dict=1)
+def get_batch_incoming_rate(item_code, warehouse, batch_no, posting_date, posting_time, creation=None):
+
+ Timestamp = CustomFunction('timestamp', ['date', 'time'])
+
+ sle = frappe.qb.DocType("Stock Ledger Entry")
+
+ timestamp_condition = (Timestamp(sle.posting_date, sle.posting_time) < Timestamp(posting_date, posting_time))
+ if creation:
+ timestamp_condition |= (
+ (Timestamp(sle.posting_date, sle.posting_time) == Timestamp(posting_date, posting_time))
+ & (sle.creation < creation)
+ )
+
+ batch_details = (
+ frappe.qb
+ .from_(sle)
+ .select(
+ Sum(sle.stock_value_difference).as_("batch_value"),
+ Sum(sle.actual_qty).as_("batch_qty")
+ )
+ .where(
+ (sle.item_code == item_code)
+ & (sle.warehouse == warehouse)
+ & (sle.batch_no == batch_no)
+ & (sle.is_cancelled == 0)
+ )
+ .where(timestamp_condition)
+ ).run(as_dict=True)
+
+ if batch_details and batch_details[0].batch_qty:
+ return batch_details[0].batch_value / batch_details[0].batch_qty
+
+
def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
- allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True):
+ allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True, batch_no=None):
if not company:
company = frappe.get_cached_value("Warehouse", warehouse, "company")
+ last_valuation_rate = None
+
+ # Get moving average rate of a specific batch number
+ if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
+ last_valuation_rate = frappe.db.sql("""
+ select sum(stock_value_difference) / sum(actual_qty)
+ from `tabStock Ledger Entry`
+ where
+ item_code = %s
+ AND warehouse = %s
+ AND batch_no = %s
+ AND is_cancelled = 0
+ AND NOT (voucher_no = %s AND voucher_type = %s)
+ """,
+ (item_code, warehouse, batch_no, voucher_no, voucher_type))
+
# Get valuation rate from last sle for the same item and warehouse
- last_valuation_rate = frappe.db.sql("""select valuation_rate
- from `tabStock Ledger Entry` force index (item_warehouse)
- where
- item_code = %s
- AND warehouse = %s
- AND valuation_rate >= 0
- AND is_cancelled = 0
- AND NOT (voucher_no = %s AND voucher_type = %s)
- order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type))
+ if not last_valuation_rate or last_valuation_rate[0][0] is None:
+ last_valuation_rate = frappe.db.sql("""select valuation_rate
+ from `tabStock Ledger Entry` force index (item_warehouse)
+ where
+ item_code = %s
+ AND warehouse = %s
+ AND valuation_rate >= 0
+ AND is_cancelled = 0
+ AND NOT (voucher_no = %s AND voucher_type = %s)
+ order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type))
if not last_valuation_rate:
# Get valuation rate from last sle for the item against any warehouse
diff --git a/erpnext/stock/tests/test_valuation.py b/erpnext/stock/tests/test_valuation.py
index 648d440..bdb768f 100644
--- a/erpnext/stock/tests/test_valuation.py
+++ b/erpnext/stock/tests/test_valuation.py
@@ -7,7 +7,7 @@
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
-from erpnext.stock.valuation import FIFOValuation, LIFOValuation, _round_off_if_near_zero
+from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
from erpnext.tests.utils import ERPNextTestCase
qty_gen = st.floats(min_value=-1e6, max_value=1e6)
@@ -113,11 +113,11 @@
self.assertTotalQty(0)
def test_rounding_off_near_zero(self):
- self.assertEqual(_round_off_if_near_zero(0), 0)
- self.assertEqual(_round_off_if_near_zero(1), 1)
- self.assertEqual(_round_off_if_near_zero(-1), -1)
- self.assertEqual(_round_off_if_near_zero(-1e-8), 0)
- self.assertEqual(_round_off_if_near_zero(1e-8), 0)
+ self.assertEqual(round_off_if_near_zero(0), 0)
+ self.assertEqual(round_off_if_near_zero(1), 1)
+ self.assertEqual(round_off_if_near_zero(-1), -1)
+ self.assertEqual(round_off_if_near_zero(-1e-8), 0)
+ self.assertEqual(round_off_if_near_zero(1e-8), 0)
def test_totals(self):
self.queue.add_stock(1, 10)
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 7263e39..f85a04f 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -209,13 +209,28 @@
@frappe.whitelist()
def get_incoming_rate(args, raise_error_if_no_rate=True):
"""Get Incoming Rate based on valuation method"""
- from erpnext.stock.stock_ledger import get_previous_sle, get_valuation_rate
+ from erpnext.stock.stock_ledger import (
+ get_batch_incoming_rate,
+ get_previous_sle,
+ get_valuation_rate,
+ )
if isinstance(args, str):
args = json.loads(args)
- in_rate = 0
+ voucher_no = args.get('voucher_no') or args.get('name')
+
+ in_rate = None
if (args.get("serial_no") or "").strip():
in_rate = get_avg_purchase_rate(args.get("serial_no"))
+ elif args.get("batch_no") and \
+ frappe.db.get_value("Batch", args.get("batch_no"), "use_batchwise_valuation", cache=True):
+ in_rate = get_batch_incoming_rate(
+ item_code=args.get('item_code'),
+ warehouse=args.get('warehouse'),
+ batch_no=args.get("batch_no"),
+ posting_date=args.get("posting_date"),
+ posting_time=args.get("posting_time"),
+ )
else:
valuation_method = get_valuation_method(args.get("item_code"))
previous_sle = get_previous_sle(args)
@@ -226,12 +241,11 @@
elif valuation_method == 'Moving Average':
in_rate = previous_sle.get('valuation_rate') or 0
- if not in_rate:
- voucher_no = args.get('voucher_no') or args.get('name')
+ if in_rate is None:
in_rate = get_valuation_rate(args.get('item_code'), args.get('warehouse'),
args.get('voucher_type'), voucher_no, args.get('allow_zero_valuation'),
currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'),
- raise_error_if_no_rate=raise_error_if_no_rate)
+ raise_error_if_no_rate=raise_error_if_no_rate, batch_no=args.get("batch_no"))
return flt(in_rate)
@@ -247,7 +261,7 @@
"""get valuation method from item or default"""
val_method = frappe.db.get_value('Item', item_code, 'valuation_method', cache=True)
if not val_method:
- val_method = frappe.db.get_value("Stock Settings", None, "valuation_method") or "FIFO"
+ val_method = frappe.db.get_value("Stock Settings", None, "valuation_method", cache=True) or "FIFO"
return val_method
def get_fifo_rate(previous_stock_queue, qty):
diff --git a/erpnext/stock/valuation.py b/erpnext/stock/valuation.py
index ee9477e..e2bd1ad 100644
--- a/erpnext/stock/valuation.py
+++ b/erpnext/stock/valuation.py
@@ -34,7 +34,7 @@
total_qty += flt(qty)
total_value += flt(qty) * flt(rate)
- return _round_off_if_near_zero(total_qty), _round_off_if_near_zero(total_value)
+ return round_off_if_near_zero(total_qty), round_off_if_near_zero(total_value)
def __repr__(self):
return str(self.state)
@@ -136,7 +136,7 @@
fifo_bin = self.queue[index]
if qty >= fifo_bin[QTY]:
# consume current bin
- qty = _round_off_if_near_zero(qty - fifo_bin[QTY])
+ qty = round_off_if_near_zero(qty - fifo_bin[QTY])
to_consume = self.queue.pop(index)
consumed_bins.append(list(to_consume))
@@ -148,7 +148,7 @@
break
else:
# qty found in current bin consume it and exit
- fifo_bin[QTY] = _round_off_if_near_zero(fifo_bin[QTY] - qty)
+ fifo_bin[QTY] = round_off_if_near_zero(fifo_bin[QTY] - qty)
consumed_bins.append([qty, fifo_bin[RATE]])
qty = 0
@@ -231,7 +231,7 @@
stock_bin = self.stack[index]
if qty >= stock_bin[QTY]:
# consume current bin
- qty = _round_off_if_near_zero(qty - stock_bin[QTY])
+ qty = round_off_if_near_zero(qty - stock_bin[QTY])
to_consume = self.stack.pop(index)
consumed_bins.append(list(to_consume))
@@ -243,14 +243,14 @@
break
else:
# qty found in current bin consume it and exit
- stock_bin[QTY] = _round_off_if_near_zero(stock_bin[QTY] - qty)
+ stock_bin[QTY] = round_off_if_near_zero(stock_bin[QTY] - qty)
consumed_bins.append([qty, stock_bin[RATE]])
qty = 0
return consumed_bins
-def _round_off_if_near_zero(number: float, precision: int = 7) -> float:
+def round_off_if_near_zero(number: float, precision: int = 7) -> float:
"""Rounds off the number to zero only if number is close to zero for decimal
specified in precision. Precision defaults to 7.
"""
diff --git a/erpnext/templates/includes/navbar/navbar_items.html b/erpnext/templates/includes/navbar/navbar_items.html
index 3275521..d7adae5 100644
--- a/erpnext/templates/includes/navbar/navbar_items.html
+++ b/erpnext/templates/includes/navbar/navbar_items.html
@@ -13,7 +13,7 @@
<li class="wishlist wishlist-icon hidden">
<a class="nav-link" href="/wishlist">
<svg class="icon icon-lg">
- <use href="#icon-heart-active"></use>
+ <use href="#icon-heart"></use>
</svg>
<span class="badge badge-primary shopping-badge" id="wish-count"></span>
</a>
diff --git a/erpnext/tests/test_zform_loads.py b/erpnext/tests/test_zform_loads.py
new file mode 100644
index 0000000..b6fb636
--- /dev/null
+++ b/erpnext/tests/test_zform_loads.py
@@ -0,0 +1,30 @@
+""" dumb test to check all function calls on known form loads """
+
+import unittest
+
+import frappe
+from frappe.desk.form.load import getdoc
+
+
+class TestFormLoads(unittest.TestCase):
+
+ def test_load(self):
+ erpnext_modules = frappe.get_all("Module Def", filters={"app_name": "erpnext"}, pluck="name")
+ doctypes = frappe.get_all("DocType", {"istable": 0, "issingle": 0, "is_virtual": 0, "module": ("in", erpnext_modules)}, pluck="name")
+
+ for doctype in doctypes:
+ last_doc = frappe.db.get_value(doctype, {}, "name", order_by="modified desc")
+ if not last_doc:
+ continue
+ with self.subTest(msg=f"Loading {doctype} - {last_doc}", doctype=doctype, last_doc=last_doc):
+ try:
+ # reset previous response
+ frappe.response = frappe._dict({"docs":[]})
+ frappe.response.docinfo = None
+
+ getdoc(doctype, last_doc)
+ except Exception as e:
+ self.fail(f"Failed to load {doctype} - {last_doc}: {e}")
+
+ self.assertTrue(frappe.response.docs, msg=f"expected document in reponse, found: {frappe.response.docs}")
+ self.assertTrue(frappe.response.docinfo, msg=f"expected docinfo in reponse, found: {frappe.response.docinfo}")
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index f345a87..b882b9d 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -3731,7 +3731,7 @@
Edit Details,Details bearbeiten,
Edit Profile,Profil bearbeiten,
Either GST Transporter ID or Vehicle No is required if Mode of Transport is Road,Bei Straßentransport ist entweder die GST-Transporter-ID oder die Fahrzeug-Nr. Erforderlich,
-Email,Email,
+Email,E-Mail,
Email Campaigns,E-Mail-Kampagnen,
Employee ID is linked with another instructor,Die Mitarbeiter-ID ist mit einem anderen Ausbilder verknüpft,
Employee Tax and Benefits,Mitarbeitersteuern und -leistungen,
@@ -6487,7 +6487,7 @@
Send Emails At,Die E-Mails senden um,
Reminder,Erinnerung,
Daily Work Summary Group User,Tägliche Arbeit Zusammenfassung Gruppenbenutzer,
-email,Email,
+email,E-Mail,
Parent Department,Elternabteilung,
Leave Block List,Urlaubssperrenliste,
Days for which Holidays are blocked for this department.,"Tage, an denen eine Urlaubssperre für diese Abteilung gilt.",
diff --git a/erpnext/www/lms/macros/hero.html b/erpnext/www/lms/macros/hero.html
index e72bfc8..95ba8f7 100644
--- a/erpnext/www/lms/macros/hero.html
+++ b/erpnext/www/lms/macros/hero.html
@@ -11,7 +11,7 @@
{% if frappe.session.user == 'Guest' %}
<a id="signup" class="btn btn-primary btn-lg" href="/login#signup">{{_('Sign Up')}}</a>
{% elif not has_access %}
- <button id="enroll" class="btn btn-primary btn-lg" onclick="enroll()" disabled>{{_('Enroll')}}</button>
+ <button id="enroll" class="btn btn-primary btn-lg" onclick="enroll()">{{_('Enroll')}}</button>
{% endif %}
</p>
</div>
@@ -20,34 +20,35 @@
<script type="text/javascript">
frappe.ready(() => {
btn = document.getElementById('enroll');
- if (btn) btn.disabled = false;
})
function enroll() {
let params = frappe.utils.get_query_params()
let btn = document.getElementById('enroll');
- btn.disbaled = true;
- btn.innerText = __('Enrolling...')
let opts = {
method: 'erpnext.education.utils.enroll_in_program',
args: {
program_name: params.program
- }
+ },
+ freeze: true,
+ freeze_message: __('Enrolling...')
}
frappe.call(opts).then(res => {
let success_dialog = new frappe.ui.Dialog({
title: __('Success'),
+ primary_action_label: __('View Program Content'),
+ primary_action: function() {
+ window.location.reload();
+ },
secondary_action: function() {
- window.location.reload()
+ window.location.reload();
}
})
- success_dialog.set_message(__('You have successfully enrolled for the program '));
- success_dialog.$message.show()
success_dialog.show();
- btn.disbaled = false;
+ success_dialog.set_message(__('You have successfully enrolled for the program '));
})
}
</script>
diff --git a/erpnext/www/shop-by-category/index.py b/erpnext/www/shop-by-category/index.py
index 3946212..09f97ba 100644
--- a/erpnext/www/shop-by-category/index.py
+++ b/erpnext/www/shop-by-category/index.py
@@ -62,8 +62,7 @@
"parent_item_group": "All Item Groups",
"show_in_website": 1
},
- fields=["name", "parent_item_group", "is_group", "image", "route"],
- as_dict=True
+ fields=["name", "parent_item_group", "is_group", "image", "route"]
)
else:
doctype = frappe.unscrub(category)
@@ -71,7 +70,7 @@
if frappe.get_meta(doctype, cached=True).get_field("image"):
fields += ["image"]
- categorical_data[category] = frappe.db.get_all(doctype, fields=fields, as_dict=True)
+ categorical_data[category] = frappe.db.get_all(doctype, fields=fields)
return categorical_data