Merge branch 'develop' into psoa_account_filter

This commit is contained in:
Deepesh Garg 2022-02-25 15:55:06 +05:30 committed by GitHub
commit 5464ba6430
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 879 additions and 328 deletions

View File

@ -7,6 +7,7 @@ import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt from frappe.utils import flt
from erpnext import get_company_currency from erpnext import get_company_currency
@ -275,6 +276,10 @@ def check_matching(bank_account, company, transaction, document_types):
} }
matching_vouchers = [] matching_vouchers = []
matching_vouchers.extend(get_loan_vouchers(bank_account, transaction,
document_types, filters))
for query in subquery: for query in subquery:
matching_vouchers.extend( matching_vouchers.extend(
frappe.db.sql(query, filters,) frappe.db.sql(query, filters,)
@ -311,6 +316,114 @@ def get_queries(bank_account, company, transaction, document_types):
return queries 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): def get_pe_matching_query(amount_condition, account_from_to, transaction):
# get matching payment entries query # get matching payment entries query
if transaction.deposit > 0: if transaction.deposit > 0:
@ -348,7 +461,6 @@ def get_je_matching_query(amount_condition, transaction):
# We have mapping at the bank level # We have mapping at the bank level
# So one bank could have both types of bank accounts like asset and liability # 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 # 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" cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
return f""" return f"""

View File

@ -49,7 +49,8 @@ class BankTransaction(StatusUpdater):
def clear_linked_payment_entries(self, for_cancel=False): def clear_linked_payment_entries(self, for_cancel=False):
for payment_entry in self.payment_entries: 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) self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
elif payment_entry.payment_document == "Sales Invoice": elif payment_entry.payment_document == "Sales Invoice":
@ -116,11 +117,18 @@ def get_paid_amount(payment_entry, currency, bank_account):
payment_entry.payment_entry, paid_amount_field) payment_entry.payment_entry, paid_amount_field)
elif payment_entry.payment_document == "Journal Entry": 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": elif payment_entry.payment_document == "Expense Claim":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed") 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: else:
frappe.throw("Please reconcile {0}: {1} manually".format(payment_entry.payment_document, payment_entry.payment_entry)) frappe.throw("Please reconcile {0}: {1} manually".format(payment_entry.payment_document, payment_entry.payment_entry))

View File

@ -109,7 +109,7 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
frappe.get_doc({ frappe.get_doc({
"doctype": "Bank", "doctype": "Bank",
"bank_name":bank_name, "bank_name":bank_name,
}).insert() }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@ -119,7 +119,7 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
"account_name":"Checking Account", "account_name":"Checking Account",
"bank": bank_name, "bank": bank_name,
"account": account_name "account": account_name
}).insert() }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@ -184,7 +184,7 @@ def add_vouchers():
"supplier_group":"All Supplier Groups", "supplier_group":"All Supplier Groups",
"supplier_type": "Company", "supplier_type": "Company",
"supplier_name": "Conrad Electronic" "supplier_name": "Conrad Electronic"
}).insert() }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@ -203,7 +203,7 @@ def add_vouchers():
"supplier_group":"All Supplier Groups", "supplier_group":"All Supplier Groups",
"supplier_type": "Company", "supplier_type": "Company",
"supplier_name": "Mr G" "supplier_name": "Mr G"
}).insert() }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@ -227,7 +227,7 @@ def add_vouchers():
"supplier_group":"All Supplier Groups", "supplier_group":"All Supplier Groups",
"supplier_type": "Company", "supplier_type": "Company",
"supplier_name": "Poore Simon's" "supplier_name": "Poore Simon's"
}).insert() }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@ -237,7 +237,7 @@ def add_vouchers():
"customer_group":"All Customer Groups", "customer_group":"All Customer Groups",
"customer_type": "Company", "customer_type": "Company",
"customer_name": "Poore Simon's" "customer_name": "Poore Simon's"
}).insert() }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@ -266,7 +266,7 @@ def add_vouchers():
"customer_group":"All Customer Groups", "customer_group":"All Customer Groups",
"customer_type": "Company", "customer_type": "Company",
"customer_name": "Fayva" "customer_name": "Fayva"
}).insert() }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass

View File

@ -166,7 +166,7 @@ class OpeningInvoiceCreationTool(Document):
frappe.scrub(row.party_type): row.party, frappe.scrub(row.party_type): row.party,
"is_pos": 0, "is_pos": 0,
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice", "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, "invoice_number": row.invoice_number,
"disable_rounded_total": 1 "disable_rounded_total": 1
}) })

View File

@ -1,11 +1,7 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
import frappe 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 ( from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension, create_dimension,
@ -14,14 +10,17 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import ( from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
get_temporary_opening_account, get_temporary_opening_account,
) )
from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"] test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
class TestOpeningInvoiceCreationTool(unittest.TestCase): class TestOpeningInvoiceCreationTool(ERPNextTestCase):
def setUp(self): @classmethod
def setUpClass(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"): if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company() make_company()
create_dimension() 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): 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") doc = frappe.get_single("Opening Invoice Creation Tool")
@ -31,26 +30,20 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
return doc.make_invoices() return doc.make_invoices()
def test_opening_sales_invoice_creation(self): def test_opening_sales_invoice_creation(self):
property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check") invoices = self.make_invoices(company="_Test Opening Invoice Company")
try:
invoices = self.make_invoices(company="_Test Opening Invoice Company")
self.assertEqual(len(invoices), 2) self.assertEqual(len(invoices), 2)
expected_value = { expected_value = {
"keys": ["customer", "outstanding_amount", "status"], "keys": ["customer", "outstanding_amount", "status"],
0: ["_Test Customer", 300, "Overdue"], 0: ["_Test Customer", 300, "Overdue"],
1: ["_Test Customer 1", 250, "Overdue"], 1: ["_Test Customer 1", 250, "Overdue"],
} }
self.check_expected_values(invoices, expected_value) 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 # Check if update stock is not enabled
self.assertEqual(si.update_stock, 0) self.assertEqual(si.update_stock, 0)
finally:
property_setter.delete()
clear_doctype_cache("Sales Invoice")
def check_expected_values(self, invoices, expected_value, invoice_type="Sales"): def check_expected_values(self, invoices, expected_value, invoice_type="Sales"):
doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice" doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice"

View File

@ -1077,7 +1077,7 @@ def get_outstanding_reference_documents(args):
if d.voucher_type in ("Purchase Invoice"): if d.voucher_type in ("Purchase Invoice"):
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no") 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 = [] orders_to_be_billed = []
if (args.get("party_type") != "Student"): if (args.get("party_type") != "Student"):
orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"), orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"),

View File

@ -439,7 +439,6 @@ class POSInvoice(SalesInvoice):
self.paid_amount = 0 self.paid_amount = 0
def set_account_for_mode_of_payment(self): def set_account_for_mode_of_payment(self):
self.payments = [d for d in self.payments if d.amount or d.base_amount or d.default]
for pay in self.payments: for pay in self.payments:
if not pay.account: if not pay.account:
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account") pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")

View File

@ -46,7 +46,7 @@ def valdiate_taxes_and_charges_template(doc):
for tax in doc.get("taxes"): for tax in doc.get("taxes"):
validate_taxes_and_charges(tax) 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_cost_center(tax, doc)
validate_inclusive_tax(tax, doc) validate_inclusive_tax(tax, doc)

View File

@ -307,7 +307,7 @@ def validate_party_gle_currency(party_type, party, company, party_account_curren
.format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency) .format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency)
def validate_party_accounts(doc): def validate_party_accounts(doc):
from erpnext.controllers.accounts_controller import validate_account_head
companies = [] companies = []
for account in doc.get("accounts"): for account in doc.get("accounts"):
@ -330,6 +330,9 @@ def validate_party_accounts(doc):
if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency: 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")) 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() @frappe.whitelist()
def get_due_date(posting_date, party_type, party, company=None, bill_date=None): def get_due_date(posting_date, party_type, party, company=None, bill_date=None):

View File

@ -4,7 +4,12 @@
import frappe import frappe
from frappe import _ 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): def execute(filters=None):
@ -18,7 +23,6 @@ def execute(filters=None):
data = get_entries(filters) data = get_entries(filters)
from erpnext.accounts.utils import get_balance_on
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"]) balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
total_debit, total_credit = 0,0 total_debit, total_credit = 0,0
@ -118,7 +122,21 @@ def get_columns():
] ]
def get_entries(filters): 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, select "Journal Entry" as payment_document, jv.posting_date,
jv.name as payment_entry, jvd.debit_in_account_currency as debit, jv.name as payment_entry, jvd.debit_in_account_currency as debit,
jvd.credit_in_account_currency as credit, jvd.against_account, jvd.credit_in_account_currency as credit, jvd.against_account,
@ -130,7 +148,8 @@ def get_entries(filters):
and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s
and ifnull(jv.is_opening, 'No') = 'No'""", filters, as_dict=1) 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 select
"Payment Entry" as payment_document, name as payment_entry, "Payment Entry" as payment_document, name as payment_entry,
reference_no, reference_date as ref_date, reference_no, reference_date as ref_date,
@ -145,9 +164,8 @@ def get_entries(filters):
and ifnull(clearance_date, '4000-01-01') > %(report_date)s and ifnull(clearance_date, '4000-01-01') > %(report_date)s
""", filters, as_dict=1) """, filters, as_dict=1)
pos_entries = [] def get_pos_entries(filters):
if filters.include_pos_transactions: return frappe.db.sql("""
pos_entries = frappe.db.sql("""
select select
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit, "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, si.posting_date, si.debit_to as against_account, sip.clearance_date,
@ -161,8 +179,42 @@ def get_entries(filters):
si.posting_date ASC, si.name DESC si.posting_date ASC, si.name DESC
""", filters, as_dict=1) """, filters, as_dict=1)
return sorted(list(payment_entries)+list(journal_entries+list(pos_entries)), def get_loan_entries(filters):
key=lambda k: k['posting_date'] or getdate(nowdate())) 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): def get_amounts_not_reflected_in_system(filters):
je_amount = frappe.db.sql(""" je_amount = frappe.db.sql("""
@ -182,7 +234,40 @@ def get_amounts_not_reflected_in_system(filters):
pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0 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): def get_balance_row(label, amount, account_currency):
if amount > 0: if amount > 0:

View File

@ -61,7 +61,7 @@ class TestTaxDetail(unittest.TestCase):
# Create GL Entries: # Create GL Entries:
db_doc.submit() db_doc.submit()
else: else:
db_doc.insert() db_doc.insert(ignore_if_duplicate=True)
except frappe.exceptions.DuplicateEntryError: except frappe.exceptions.DuplicateEntryError:
pass pass

View File

@ -39,10 +39,11 @@ class TestReports(unittest.TestCase):
def test_execute_all_accounts_reports(self): def test_execute_all_accounts_reports(self):
"""Test that all script report in stock modules are executable with supported filters""" """Test that all script report in stock modules are executable with supported filters"""
for report, filter in REPORT_FILTER_TEST_CASES: for report, filter in REPORT_FILTER_TEST_CASES:
execute_script_report( with self.subTest(report=report):
report_name=report, execute_script_report(
module="Accounts", report_name=report,
filters=filter, module="Accounts",
default_filters=DEFAULT_FILTERS, filters=filter,
optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None, default_filters=DEFAULT_FILTERS,
) optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
)

View File

@ -847,7 +847,7 @@ def create_payment_gateway_account(gateway, payment_channel="Email"):
"payment_account": bank_account.name, "payment_account": bank_account.name,
"currency": bank_account.account_currency, "currency": bank_account.account_currency,
"payment_channel": payment_channel "payment_channel": payment_channel
}).insert(ignore_permissions=True) }).insert(ignore_permissions=True, ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
# already exists, due to a reinstall? # already exists, due to a reinstall?

View File

@ -417,11 +417,12 @@ class Asset(AccountsController):
def validate_asset_finance_books(self, row): def validate_asset_finance_books(self, row):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): 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") 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 row.depreciation_start_date:
if not self.available_for_use_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) row.depreciation_start_date = get_last_day(self.available_for_use_date)
if not self.is_existing_asset: if not self.is_existing_asset:
@ -439,8 +440,9 @@ class Asset(AccountsController):
else: else:
self.number_of_depreciations_booked = 0 self.number_of_depreciations_booked = 0
if cint(self.number_of_depreciations_booked) > cint(row.total_number_of_depreciations): if flt(row.total_number_of_depreciations) <= cint(self.number_of_depreciations_booked):
frappe.throw(_("Number of Depreciations Booked cannot be greater than Total Number of Depreciations")) 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): 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") frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date")

View File

@ -873,8 +873,9 @@ class TestDepreciationBasics(AssetSetup):
self.assertRaises(frappe.ValidationError, asset.save) self.assertRaises(frappe.ValidationError, asset.save)
def test_number_of_depreciations(self): 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( asset = create_asset(
item_code = "Macbook Pro", item_code = "Macbook Pro",
calculate_depreciation = 1, calculate_depreciation = 1,
@ -889,6 +890,21 @@ class TestDepreciationBasics(AssetSetup):
self.assertRaises(frappe.ValidationError, asset.save) 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): def test_depreciation_start_date_is_before_purchase_date(self):
asset = create_asset( asset = create_asset(
item_code = "Macbook Pro", item_code = "Macbook Pro",
@ -1264,7 +1280,7 @@ def create_asset(**args):
if not args.do_not_save: if not args.do_not_save:
try: try:
asset.save() asset.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@ -1305,7 +1321,7 @@ def create_fixed_asset_item(item_code=None, auto_create_assets=1, is_grouped_ass
"is_grouped_asset": is_grouped_asset, "is_grouped_asset": is_grouped_asset,
"asset_naming_series": naming_series "asset_naming_series": naming_series
}) })
item.insert() item.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
return item return item

View File

@ -23,7 +23,7 @@ class TestAssetCategory(unittest.TestCase):
}) })
try: try:
asset_category.insert() asset_category.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass

View File

@ -14,151 +14,150 @@ test_records = frappe.get_test_records('Supplier')
class TestSupplier(unittest.TestCase): class TestSupplier(unittest.TestCase):
def test_get_supplier_group_details(self): def test_get_supplier_group_details(self):
doc = frappe.new_doc("Supplier Group") doc = frappe.new_doc("Supplier Group")
doc.supplier_group_name = "_Testing Supplier Group" doc.supplier_group_name = "_Testing Supplier Group"
doc.payment_terms = "_Test Payment Term Template 3" doc.payment_terms = "_Test Payment Term Template 3"
doc.accounts = [] doc.accounts = []
test_account_details = { test_account_details = {
"company": "_Test Company", "company": "_Test Company",
"account": "Creditors - _TC", "account": "Creditors - _TC",
} }
doc.append("accounts", test_account_details) doc.append("accounts", test_account_details)
doc.save() doc.save()
s_doc = frappe.new_doc("Supplier") s_doc = frappe.new_doc("Supplier")
s_doc.supplier_name = "Testing Supplier" s_doc.supplier_name = "Testing Supplier"
s_doc.supplier_group = "_Testing Supplier Group" s_doc.supplier_group = "_Testing Supplier Group"
s_doc.payment_terms = "" s_doc.payment_terms = ""
s_doc.accounts = [] s_doc.accounts = []
s_doc.insert() s_doc.insert()
s_doc.get_supplier_group_details() s_doc.get_supplier_group_details()
self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3") 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].company, "_Test Company")
self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC") self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC")
s_doc.delete() s_doc.delete()
doc.delete() doc.delete()
def test_supplier_default_payment_terms(self): def test_supplier_default_payment_terms(self):
# Payment Term based on Days after invoice date # Payment Term based on Days after invoice date
frappe.db.set_value( frappe.db.set_value(
"Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 3") "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") due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
self.assertEqual(due_date, "2016-02-21") self.assertEqual(due_date, "2016-02-21")
due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1") due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
self.assertEqual(due_date, "2017-02-21") self.assertEqual(due_date, "2017-02-21")
# Payment Term based on last day of month # Payment Term based on last day of month
frappe.db.set_value( frappe.db.set_value(
"Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 1") "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") due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
self.assertEqual(due_date, "2016-02-29") self.assertEqual(due_date, "2016-02-29")
due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1") due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
self.assertEqual(due_date, "2017-02-28") 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 # 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") 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") due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
self.assertEqual(due_date, "2016-02-21") self.assertEqual(due_date, "2016-02-21")
# Payment terms for Supplier Group instead of supplier and evaluate the due date # 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") frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 1")
# Leap year # Leap year
due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
self.assertEqual(due_date, "2016-02-29") self.assertEqual(due_date, "2016-02-29")
# # Non Leap year # # Non Leap year
due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1") due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
self.assertEqual(due_date, "2017-02-28") self.assertEqual(due_date, "2017-02-28")
# Supplier with no default Payment Terms Template # Supplier with no default Payment Terms Template
frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "") frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "")
frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", "") frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", "")
due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier") due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier")
self.assertEqual(due_date, "2016-01-22") self.assertEqual(due_date, "2016-01-22")
# # Non Leap year # # Non Leap year
due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier") due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier")
self.assertEqual(due_date, "2017-01-22") self.assertEqual(due_date, "2017-01-22")
def test_supplier_disabled(self): def test_supplier_disabled(self):
make_test_records("Item") 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): def test_supplier_country(self):
# Test that country field exists in Supplier DocType # Test that country field exists in Supplier DocType
supplier = frappe.get_doc('Supplier', '_Test Supplier with Country') supplier = frappe.get_doc('Supplier', '_Test Supplier with Country')
self.assertTrue('country' in supplier.as_dict()) self.assertTrue('country' in supplier.as_dict())
# Test if test supplier field record is 'Greece' # Test if test supplier field record is 'Greece'
self.assertEqual(supplier.country, "Greece") self.assertEqual(supplier.country, "Greece")
# Test update Supplier instance country value # Test update Supplier instance country value
supplier = frappe.get_doc('Supplier', '_Test Supplier') supplier = frappe.get_doc('Supplier', '_Test Supplier')
supplier.country = 'Greece' supplier.country = 'Greece'
supplier.save() supplier.save()
self.assertEqual(supplier.country, "Greece") self.assertEqual(supplier.country, "Greece")
def test_party_details_tax_category(self): def test_party_details_tax_category(self):
from erpnext.accounts.party import get_party_details 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 # Tax Category without Address
details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier") details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier")
self.assertEqual(details.tax_category, "_Test Tax Category 1") self.assertEqual(details.tax_category, "_Test Tax Category 1")
address = frappe.get_doc(dict( address = frappe.get_doc(dict(
doctype='Address', doctype='Address',
address_title='_Test Address With Tax Category', address_title='_Test Address With Tax Category',
tax_category='_Test Tax Category 2', tax_category='_Test Tax Category 2',
address_type='Billing', address_type='Billing',
address_line1='Station Road', address_line1='Station Road',
city='_Test City', city='_Test City',
country='India', country='India',
links=[dict( links=[dict(
link_doctype='Supplier', link_doctype='Supplier',
link_name='_Test Supplier With Tax Category' link_name='_Test Supplier With Tax Category'
)] )]
)).insert() )).insert()
# Tax Category with Address # Tax Category with Address
details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier") details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier")
self.assertEqual(details.tax_category, "_Test Tax Category 2") self.assertEqual(details.tax_category, "_Test Tax Category 2")
# Rollback # Rollback
address.delete() address.delete()
def create_supplier(**args): def create_supplier(**args):
args = frappe._dict(args) args = frappe._dict(args)
try: if frappe.db.exists("Supplier", args.supplier_name):
doc = frappe.get_doc({ return frappe.get_doc("Supplier", args.supplier_name)
"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()
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 doc
return frappe.get_doc("Supplier", args.supplier_name)

View File

@ -1566,13 +1566,12 @@ def validate_taxes_and_charges(tax):
tax.rate = None tax.rate = None
def validate_account_head(tax, doc): def validate_account_head(idx, account, company):
company = frappe.get_cached_value('Account', account_company = frappe.get_cached_value('Account', account, 'company')
tax.account_head, 'company')
if company != doc.company: if account_company != company:
frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}') 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): def validate_cost_center(tax, doc):

View File

@ -104,11 +104,11 @@ class EmployeeBoardingController(Document):
def get_task_dates(self, activity, holiday_list): def get_task_dates(self, activity, holiday_list):
start_date = end_date = None 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 = add_days(self.boarding_begins_on, activity.begin_on)
start_date = self.update_if_holiday(start_date, holiday_list) 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 = add_days(self.boarding_begins_on, activity.begin_on + activity.duration)
end_date = self.update_if_holiday(end_date, holiday_list) end_date = self.update_if_holiday(end_date, holiday_list)

View File

@ -175,7 +175,7 @@ class TestShoppingCart(unittest.TestCase):
def create_tax_rule(self): def create_tax_rule(self):
tax_rule = frappe.get_test_records("Tax Rule")[0] tax_rule = frappe.get_test_records("Tax Rule")[0]
try: try:
frappe.get_doc(tax_rule).insert() frappe.get_doc(tax_rule).insert(ignore_if_duplicate=True)
except (frappe.DuplicateEntryError, ConflictingTaxRule): except (frappe.DuplicateEntryError, ConflictingTaxRule):
pass pass

View File

@ -82,7 +82,7 @@ class TallyMigration(Document):
"is_private": True "is_private": True
}) })
try: try:
f.insert() f.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
setattr(self, key, f.file_url) setattr(self, key, f.file_url)

View File

@ -8,10 +8,6 @@ from frappe.utils import cint, flt
from erpnext import get_default_company, get_region 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", 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", "FR", "GB", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT", "NL", "PL", "PT", "RO",
"SE", "SI", "SK", "US"] "SE", "SI", "SK", "US"]
@ -35,12 +31,14 @@ def get_client():
if api_key and api_url: if api_key and api_url:
client = taxjar.Client(api_key=api_key, api_url=api_url) client = taxjar.Client(api_key=api_key, api_url=api_url)
client.set_api_config('headers', { client.set_api_config('headers', {
'x-api-version': '2020-08-07' 'x-api-version': '2022-01-24'
}) })
return client return client
def create_transaction(doc, method): def create_transaction(doc, method):
TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
"""Create an order transaction in TaxJar""" """Create an order transaction in TaxJar"""
if not TAXJAR_CREATE_TRANSACTIONS: if not TAXJAR_CREATE_TRANSACTIONS:
@ -51,6 +49,7 @@ def create_transaction(doc, method):
if not client: if not client:
return 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]) sales_tax = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == TAX_ACCOUNT_HEAD])
if not sales_tax: if not sales_tax:
@ -79,6 +78,7 @@ def create_transaction(doc, method):
def delete_transaction(doc, method): def delete_transaction(doc, method):
"""Delete an existing TaxJar order transaction""" """Delete an existing TaxJar order transaction"""
TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
if not TAXJAR_CREATE_TRANSACTIONS: if not TAXJAR_CREATE_TRANSACTIONS:
return return
@ -92,6 +92,8 @@ def delete_transaction(doc, method):
def get_tax_data(doc): 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_address = get_company_address_details(doc)
from_shipping_state = from_address.get("state") from_shipping_state = from_address.get("state")
from_country_code = frappe.db.get_value("Country", from_address.country, "code") from_country_code = frappe.db.get_value("Country", from_address.country, "code")
@ -113,20 +115,20 @@ def get_tax_data(doc):
to_shipping_state = get_state_code(to_address, 'Shipping') to_shipping_state = get_state_code(to_address, 'Shipping')
tax_dict = { tax_dict = {
'from_country': from_country_code, "from_country": from_country_code,
'from_zip': from_address.pincode, "from_zip": from_address.pincode,
'from_state': from_shipping_state, "from_state": from_shipping_state,
'from_city': from_address.city, "from_city": from_address.city,
'from_street': from_address.address_line1, "from_street": from_address.address_line1,
'to_country': to_country_code, "to_country": to_country_code,
'to_zip': to_address.pincode, "to_zip": to_address.pincode,
'to_city': to_address.city, "to_city": to_address.city,
'to_street': to_address.address_line1, "to_street": to_address.address_line1,
'to_state': to_shipping_state, "to_state": to_shipping_state,
'shipping': shipping, "shipping": shipping,
'amount': doc.net_total, "amount": doc.net_total,
'plugin': 'erpnext', "plugin": "erpnext",
'line_items': line_items "line_items": line_items
} }
return tax_dict return tax_dict
@ -156,6 +158,9 @@ def get_line_item_dict(item, docstatus):
return tax_dict return tax_dict
def set_sales_tax(doc, method): 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: if not TAXJAR_CALCULATE_TAX:
return return
@ -206,6 +211,7 @@ def set_sales_tax(doc, method):
doc.run_method("calculate_taxes_and_totals") doc.run_method("calculate_taxes_and_totals")
def check_for_nexus(doc, tax_dict): 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"]}): if not frappe.db.get_value('TaxJar Nexus', {'region_code': tax_dict["to_state"]}):
for item in doc.get("items"): for item in doc.get("items"):
item.tax_collectable = flt(0) item.tax_collectable = flt(0)
@ -218,6 +224,8 @@ def check_for_nexus(doc, tax_dict):
def check_sales_tax_exemption(doc): def check_sales_tax_exemption(doc):
# if the party is exempt from sales tax, then set all tax account heads to zero # 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 \ 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") \ or frappe.db.has_column("Customer", "exempt_from_sales_tax") \
and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax") and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax")

View File

@ -142,7 +142,7 @@ class Employee(NestedSet):
"file_url": self.image, "file_url": self.image,
"attached_to_doctype": "User", "attached_to_doctype": "User",
"attached_to_name": self.user_id "attached_to_name": self.user_id
}).insert() }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
# already exists # already exists
pass pass

View File

@ -4,7 +4,7 @@
import unittest import unittest
import frappe import frappe
from frappe.utils import getdate from frappe.utils import add_days, getdate
from erpnext.hr.doctype.employee_onboarding.employee_onboarding import ( from erpnext.hr.doctype.employee_onboarding.employee_onboarding import (
IncompleteTaskError, IncompleteTaskError,
@ -35,6 +35,15 @@ class TestEmployeeOnboarding(unittest.TestCase):
# boarding status # boarding status
self.assertEqual(onboarding.boarding_status, 'Pending') 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 # complete the task
project = frappe.get_doc('Project', onboarding.project) project = frappe.get_doc('Project', onboarding.project)
for task in frappe.get_all('Task', dict(project=project.name)): for task in frappe.get_all('Task', dict(project=project.name)):
@ -57,10 +66,7 @@ class TestEmployeeOnboarding(unittest.TestCase):
self.assertEqual(employee.employee_name, 'Test Researcher') self.assertEqual(employee.employee_name, 'Test Researcher')
def tearDown(self): def tearDown(self):
for entry in frappe.get_all('Employee Onboarding'): frappe.db.rollback()
doc = frappe.get_doc('Employee Onboarding', entry.name)
doc.cancel()
doc.delete()
def get_job_applicant(): def get_job_applicant():
@ -87,23 +93,31 @@ def get_job_offer(applicant_name):
def create_employee_onboarding(): def create_employee_onboarding():
applicant = get_job_applicant() applicant = get_job_applicant()
job_offer = get_job_offer(applicant.name) 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 = frappe.new_doc('Employee Onboarding')
onboarding.job_applicant = applicant.name onboarding.job_applicant = applicant.name
onboarding.job_offer = job_offer.name onboarding.job_offer = job_offer.name
onboarding.date_of_joining = onboarding.boarding_begins_on = getdate() onboarding.date_of_joining = onboarding.boarding_begins_on = getdate()
onboarding.company = '_Test Company' onboarding.company = '_Test Company'
onboarding.holiday_list = holiday_list onboarding.holiday_list = holiday_list.name
onboarding.designation = 'Researcher' onboarding.designation = 'Researcher'
onboarding.append('activities', { onboarding.append('activities', {
'activity_name': 'Assign ID Card', 'activity_name': 'Assign ID Card',
'role': 'HR User', 'role': 'HR User',
'required_for_employee_creation': 1 'required_for_employee_creation': 1,
'begin_on': 0,
'duration': 1
}) })
onboarding.append('activities', { onboarding.append('activities', {
'activity_name': 'Assign a laptop', 'activity_name': 'Assign a laptop',
'role': 'HR User' 'role': 'HR User',
'begin_on': 1,
'duration': 1
}) })
onboarding.status = 'Pending' onboarding.status = 'Pending'
onboarding.insert() onboarding.insert()

View File

@ -128,4 +128,4 @@ def show_email_summary(email_success, email_failure):
message += _('{0} due to missing email information for employee(s): {1}').format( message += _('{0} due to missing email information for employee(s): {1}').format(
frappe.bold('Sending Failed'), ', '.join(email_failure)) frappe.bold('Sending Failed'), ', '.join(email_failure))
frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True) frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True)

View File

@ -82,7 +82,7 @@ def get_vehicle(employee_id):
"vehicle_value": flt(500000) "vehicle_value": flt(500000)
}) })
try: try:
vehicle.insert() vehicle.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
return license_plate return license_plate

View File

@ -14,11 +14,15 @@
"applicant", "applicant",
"section_break_7", "section_break_7",
"disbursement_date", "disbursement_date",
"clearance_date",
"column_break_8", "column_break_8",
"disbursed_amount", "disbursed_amount",
"accounting_dimensions_section", "accounting_dimensions_section",
"cost_center", "cost_center",
"customer_details_section", "accounting_details",
"disbursement_account",
"column_break_16",
"loan_account",
"bank_account", "bank_account",
"disbursement_references_section", "disbursement_references_section",
"reference_date", "reference_date",
@ -106,11 +110,6 @@
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Disbursement Details" "label": "Disbursement Details"
}, },
{
"fieldname": "customer_details_section",
"fieldtype": "Section Break",
"label": "Customer Details"
},
{ {
"fetch_from": "against_loan.applicant_type", "fetch_from": "against_loan.applicant_type",
"fieldname": "applicant_type", "fieldname": "applicant_type",
@ -149,15 +148,48 @@
"fieldname": "reference_number", "fieldname": "reference_number",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Reference Number" "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, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-19 18:09:32.175355", "modified": "2022-02-17 18:23:44.157598",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Disbursement", "name": "Loan Disbursement",
"naming_rule": "Expression (old style)",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -194,5 +226,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -42,9 +42,6 @@ class LoanDisbursement(AccountsController):
if not self.posting_date: if not self.posting_date:
self.posting_date = self.disbursement_date or nowdate() 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): def validate_disbursal_amount(self):
possible_disbursal_amount = get_disbursal_amount(self.against_loan) possible_disbursal_amount = get_disbursal_amount(self.against_loan)
@ -117,12 +114,11 @@ class LoanDisbursement(AccountsController):
def make_gl_entries(self, cancel=0, adv_adj=0): def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = [] gle_map = []
loan_details = frappe.get_doc("Loan", self.against_loan)
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
"account": loan_details.loan_account, "account": self.loan_account,
"against": loan_details.disbursement_account, "against": self.disbursement_account,
"debit": self.disbursed_amount, "debit": self.disbursed_amount,
"debit_in_account_currency": self.disbursed_amount, "debit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
@ -137,8 +133,8 @@ class LoanDisbursement(AccountsController):
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
"account": loan_details.disbursement_account, "account": self.disbursement_account,
"against": loan_details.loan_account, "against": self.loan_account,
"credit": self.disbursed_amount, "credit": self.disbursed_amount,
"credit_in_account_currency": self.disbursed_amount, "credit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",

View File

@ -1,7 +1,7 @@
{ {
"actions": [], "actions": [],
"autoname": "LM-REP-.####", "autoname": "LM-REP-.####",
"creation": "2019-09-03 14:44:39.977266", "creation": "2022-01-25 10:30:02.767941",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
@ -13,6 +13,7 @@
"column_break_3", "column_break_3",
"company", "company",
"posting_date", "posting_date",
"clearance_date",
"rate_of_interest", "rate_of_interest",
"payroll_payable_account", "payroll_payable_account",
"is_term_loan", "is_term_loan",
@ -37,7 +38,12 @@
"total_penalty_paid", "total_penalty_paid",
"total_interest_paid", "total_interest_paid",
"repayment_details", "repayment_details",
"amended_from" "amended_from",
"accounting_details_section",
"payment_account",
"penalty_income_account",
"column_break_36",
"loan_account"
], ],
"fields": [ "fields": [
{ {
@ -260,12 +266,52 @@
"fieldname": "repay_from_salary", "fieldname": "repay_from_salary",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Repay From Salary" "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, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-01-06 01:51:06.707782", "modified": "2022-02-18 19:10:07.742298",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Repayment", "name": "Loan Repayment",

View File

@ -310,7 +310,6 @@ class LoanRepayment(AccountsController):
def make_gl_entries(self, cancel=0, adv_adj=0): def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = [] gle_map = []
loan_details = frappe.get_doc("Loan", self.against_loan)
if self.shortfall_amount and self.amount_paid > self.shortfall_amount: if self.shortfall_amount and self.amount_paid > self.shortfall_amount:
remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(self.shortfall_amount, remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(self.shortfall_amount,
@ -323,13 +322,13 @@ class LoanRepayment(AccountsController):
if self.repay_from_salary: if self.repay_from_salary:
payment_account = self.payroll_payable_account payment_account = self.payroll_payable_account
else: else:
payment_account = loan_details.payment_account payment_account = self.payment_account
if self.total_penalty_paid: if self.total_penalty_paid:
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
"account": loan_details.loan_account, "account": self.loan_account,
"against": loan_details.payment_account, "against": payment_account,
"debit": self.total_penalty_paid, "debit": self.total_penalty_paid,
"debit_in_account_currency": self.total_penalty_paid, "debit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
@ -344,8 +343,8 @@ class LoanRepayment(AccountsController):
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
"account": loan_details.penalty_income_account, "account": self.penalty_income_account,
"against": loan_details.loan_account, "against": self.loan_account,
"credit": self.total_penalty_paid, "credit": self.total_penalty_paid,
"credit_in_account_currency": self.total_penalty_paid, "credit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
@ -359,8 +358,7 @@ class LoanRepayment(AccountsController):
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
"account": payment_account, "account": payment_account,
"against": loan_details.loan_account + ", " + loan_details.interest_income_account "against": self.loan_account + ", " + self.penalty_income_account,
+ ", " + loan_details.penalty_income_account,
"debit": self.amount_paid, "debit": self.amount_paid,
"debit_in_account_currency": self.amount_paid, "debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
@ -368,16 +366,16 @@ class LoanRepayment(AccountsController):
"remarks": remarks, "remarks": remarks,
"cost_center": self.cost_center, "cost_center": self.cost_center,
"posting_date": getdate(self.posting_date), "posting_date": getdate(self.posting_date),
"party_type": loan_details.applicant_type if self.repay_from_salary else '', "party_type": self.applicant_type if self.repay_from_salary else '',
"party": loan_details.applicant if self.repay_from_salary else '' "party": self.applicant if self.repay_from_salary else ''
}) })
) )
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
"account": loan_details.loan_account, "account": self.loan_account,
"party_type": loan_details.applicant_type, "party_type": self.applicant_type,
"party": loan_details.applicant, "party": self.applicant,
"against": payment_account, "against": payment_account,
"credit": self.amount_paid, "credit": self.amount_paid,
"credit_in_account_currency": self.amount_paid, "credit_in_account_currency": self.amount_paid,

View File

@ -55,10 +55,11 @@ class TestManufacturingReports(unittest.TestCase):
def test_execute_all_manufacturing_reports(self): def test_execute_all_manufacturing_reports(self):
"""Test that all script report in manufacturing modules are executable with supported filters""" """Test that all script report in manufacturing modules are executable with supported filters"""
for report, filter in REPORT_FILTER_TEST_CASES: for report, filter in REPORT_FILTER_TEST_CASES:
execute_script_report( with self.subTest(report=report):
report_name=report, execute_script_report(
module="Manufacturing", report_name=report,
filters=filter, module="Manufacturing",
default_filters=DEFAULT_FILTERS, filters=filter,
optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None, default_filters=DEFAULT_FILTERS,
) optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
)

View File

@ -353,4 +353,5 @@ erpnext.patches.v13_0.update_reserved_qty_closed_wo
erpnext.patches.v13_0.update_exchange_rate_settings erpnext.patches.v13_0.update_exchange_rate_settings
erpnext.patches.v14_0.delete_amazon_mws_doctype erpnext.patches.v14_0.delete_amazon_mws_doctype
erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
erpnext.patches.v13_0.update_accounts_in_loan_docs
erpnext.patches.v14_0.update_batch_valuation_flag erpnext.patches.v14_0.update_batch_valuation_flag

View File

@ -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()

View File

@ -1268,7 +1268,7 @@ class SalarySlip(TransactionBase):
for i, earning in enumerate(self.earnings): for i, earning in enumerate(self.earnings):
if earning.salary_component == salary_component: if earning.salary_component == salary_component:
self.earnings[i].amount = wages_amount 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) self.net_pay = flt(self.gross_pay) - flt(self.total_deduction)
def compute_year_to_date(self): def compute_year_to_date(self):

View File

@ -1019,13 +1019,13 @@ def setup_test():
frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None) frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None)
frappe.db.set_value('HR Settings', None, 'leave_approval_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()) 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: if not holiday_list:
holiday_list = frappe.get_doc({ holiday_list = frappe.get_doc({
"doctype": "Holiday List", "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], "from_date": fiscal_year[1],
"to_date": fiscal_year[2], "to_date": fiscal_year[2],
"weekly_off": "Sunday" "weekly_off": "Sunday"

View File

@ -21,7 +21,7 @@ class TestHomepageSection(unittest.TestCase):
{'title': 'Card 2', 'subtitle': 'Subtitle 2', 'content': 'This is test card 2', 'image': 'test.jpg'}, {'title': 'Card 2', 'subtitle': 'Subtitle 2', 'content': 'This is test card 2', 'image': 'test.jpg'},
], ],
'no_of_columns': 3 'no_of_columns': 3
}).insert() }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass

View File

@ -181,6 +181,12 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
fieldname: "journal_entry", fieldname: "journal_entry",
onchange: () => this.update_options(), onchange: () => this.update_options(),
}, },
{
fieldtype: "Check",
label: "Loan Repayment",
fieldname: "loan_repayment",
onchange: () => this.update_options(),
},
{ {
fieldname: "column_break_5", fieldname: "column_break_5",
fieldtype: "Column Break", fieldtype: "Column Break",
@ -191,13 +197,18 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
fieldname: "sales_invoice", fieldname: "sales_invoice",
onchange: () => this.update_options(), onchange: () => this.update_options(),
}, },
{ {
fieldtype: "Check", fieldtype: "Check",
label: "Purchase Invoice", label: "Purchase Invoice",
fieldname: "purchase_invoice", fieldname: "purchase_invoice",
onchange: () => this.update_options(), onchange: () => this.update_options(),
}, },
{
fieldtype: "Check",
label: "Show Only Exact Amount",
fieldname: "exact_match",
onchange: () => this.update_options(),
},
{ {
fieldname: "column_break_5", fieldname: "column_break_5",
fieldtype: "Column Break", fieldtype: "Column Break",
@ -210,8 +221,8 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
}, },
{ {
fieldtype: "Check", fieldtype: "Check",
label: "Show Only Exact Amount", label: "Loan Disbursement",
fieldname: "exact_match", fieldname: "loan_disbursement",
onchange: () => this.update_options(), onchange: () => this.update_options(),
}, },
{ {

View File

@ -525,6 +525,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
item.weight_per_unit = 0; item.weight_per_unit = 0;
item.weight_uom = ''; item.weight_uom = '';
item.conversion_factor = 0;
if(['Sales Invoice'].includes(this.frm.doc.doctype)) { if(['Sales Invoice'].includes(this.frm.doc.doctype)) {
update_stock = cint(me.frm.doc.update_stock); update_stock = cint(me.frm.doc.update_stock);
@ -2284,13 +2285,17 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
} }
coupon_code() { coupon_code() {
var me = this; if (this.frm.doc.coupon_code || this.frm._last_coupon_code) {
frappe.run_serially([ // reset pricing rules if coupon code is set or is unset
() => this.frm.doc.ignore_pricing_rule=1, const _ignore_pricing_rule = this.frm.doc.ignore_pricing_rule;
() => me.ignore_pricing_rule(), return frappe.run_serially([
() => this.frm.doc.ignore_pricing_rule=0, () => this.frm.doc.ignore_pricing_rule=1,
() => me.apply_pricing_rule() () => this.frm.trigger('ignore_pricing_rule'),
]); () => this.frm.doc.ignore_pricing_rule=_ignore_pricing_rule,
() => this.frm.trigger('apply_pricing_rule'),
() => this.frm._last_coupon_code = this.frm.doc.coupon_code
]);
}
} }
}; };

View File

@ -78,11 +78,11 @@ erpnext.setup.slides_settings = [
slide.get_input("company_name").on("change", function () { slide.get_input("company_name").on("change", function () {
var parts = slide.get_input("company_name").val().split(" "); var parts = slide.get_input("company_name").val().split(" ");
var abbr = $.map(parts, function (p) { return p ? p.substr(0, 1) : null }).join(""); var abbr = $.map(parts, function (p) { return p ? p.substr(0, 1) : null }).join("");
slide.get_field("company_abbr").set_value(abbr.slice(0, 5).toUpperCase()); slide.get_field("company_abbr").set_value(abbr.slice(0, 10).toUpperCase());
}).val(frappe.boot.sysdefaults.company_name || "").trigger("change"); }).val(frappe.boot.sysdefaults.company_name || "").trigger("change");
slide.get_input("company_abbr").on("change", function () { slide.get_input("company_abbr").on("change", function () {
if (slide.get_input("company_abbr").val().length > 5) { if (slide.get_input("company_abbr").val().length > 10) {
frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters")); frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters"));
slide.get_field("company_abbr").set_value(""); slide.get_field("company_abbr").set_value("");
} }
@ -96,7 +96,7 @@ erpnext.setup.slides_settings = [
if (!this.values.company_abbr) { if (!this.values.company_abbr) {
return false; return false;
} }
if (this.values.company_abbr.length > 5) { if (this.values.company_abbr.length > 10) {
return false; return false;
} }
return true; return true;

View File

@ -338,14 +338,14 @@ body.product-page {
.btn-add-to-wishlist { .btn-add-to-wishlist {
svg use { svg use {
stroke: #F47A7A; --icon-stroke: #F47A7A;
} }
} }
.btn-view-in-wishlist { .btn-view-in-wishlist {
svg use { svg use {
fill: #F47A7A; fill: #F47A7A;
stroke: none; --icon-stroke: none;
} }
} }
@ -1022,7 +1022,7 @@ body.product-page {
.not-wished { .not-wished {
cursor: pointer; cursor: pointer;
stroke: #F47A7A !important; --icon-stroke: #F47A7A !important;
&:hover { &:hover {
fill: #F47A7A; fill: #F47A7A;
@ -1030,7 +1030,7 @@ body.product-page {
} }
.wished { .wished {
stroke: none; --icon-stroke: none;
fill: #F47A7A !important; fill: #F47A7A !important;
} }

View File

@ -53,10 +53,7 @@ def create_hsn_codes(data, code_field):
hsn_code.description = d["description"] hsn_code.description = d["description"]
hsn_code.hsn_code = d[code_field] hsn_code.hsn_code = d[code_field]
hsn_code.name = d[code_field] hsn_code.name = d[code_field]
try: hsn_code.db_insert(ignore_if_duplicate=True)
hsn_code.db_insert()
except frappe.DuplicateEntryError:
pass
def add_custom_roles_for_reports(): def add_custom_roles_for_reports():
for report_name in ('GST Sales Register', 'GST Purchase Register', for report_name in ('GST Sales Register', 'GST Purchase Register',

View File

@ -170,17 +170,20 @@ erpnext.PointOfSale.Payment = class {
}); });
frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => { frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => {
if (!frm.doc.ignore_pricing_rule) { if (!frm.doc.ignore_pricing_rule && frm.doc.coupon_code) {
if (frm.doc.coupon_code) { frappe.run_serially([
frappe.run_serially([ () => frm.doc.ignore_pricing_rule=1,
() => frm.doc.ignore_pricing_rule=1, () => frm.trigger('ignore_pricing_rule'),
() => frm.trigger('ignore_pricing_rule'), () => frm.doc.ignore_pricing_rule=0,
() => frm.doc.ignore_pricing_rule=0, () => frm.trigger('apply_pricing_rule'),
() => frm.trigger('apply_pricing_rule'), () => frm.save(),
() => frm.save(), () => this.update_totals_section(frm.doc)
() => this.update_totals_section(frm.doc) ]);
]); } else if (frm.doc.ignore_pricing_rule && frm.doc.coupon_code) {
} frappe.show_alert({
message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."),
indicator: "orange"
});
} }
}); });

View File

@ -155,7 +155,7 @@ def insert_record(records):
doc = frappe.new_doc(r.get("doctype")) doc = frappe.new_doc(r.get("doctype"))
doc.update(r) doc.update(r)
try: try:
doc.insert(ignore_permissions=True) doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
except frappe.DuplicateEntryError as e: except frappe.DuplicateEntryError as e:
# pass DuplicateEntryError and continue # pass DuplicateEntryError and continue
if e.args and e.args[0]==doc.doctype and e.args[1]==doc.name: if e.args and e.args[0]==doc.doctype and e.args[1]==doc.name:

View File

@ -433,14 +433,13 @@ def create_price_list_for_batch(item_code, batch, rate):
def make_new_batch(**args): def make_new_batch(**args):
args = frappe._dict(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({ batch = frappe.get_doc({
"doctype": "Batch", "doctype": "Batch",
"batch_id": args.batch_id, "batch_id": args.batch_id,
"item": args.item_code, "item": args.item_code,
}).insert() }).insert()
except frappe.DuplicateEntryError:
batch = frappe.get_doc("Batch", args.batch_id)
return batch return batch

View File

@ -398,6 +398,7 @@ class Item(Document):
if merge: if merge:
self.validate_properties_before_merge(new_name) 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) self.validate_duplicate_website_item_before_merge(old_name, new_name)
def after_rename(self, old_name, new_name, merge): def after_rename(self, old_name, new_name, merge):
@ -462,6 +463,20 @@ class Item(Document):
msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list]) msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])
frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError) 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): 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. Block merge if both old and new items have website items against them.
@ -479,8 +494,9 @@ class Item(Document):
old_web_item = [d.get("name") for d in web_items if d.get("item_code") == old_name][0] 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) 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) frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError)
def set_last_purchase_rate(self, new_name): def set_last_purchase_rate(self, new_name):

View File

@ -15,6 +15,7 @@ from erpnext.controllers.item_variant import (
get_variant, get_variant,
) )
from erpnext.stock.doctype.item.item import ( from erpnext.stock.doctype.item.item import (
DataValidationError,
InvalidBarcode, InvalidBarcode,
StockExistsForTemplate, StockExistsForTemplate,
get_item_attribute, get_item_attribute,
@ -388,6 +389,26 @@ class TestItem(ERPNextTestCase):
self.assertTrue(frappe.db.get_value("Bin", self.assertTrue(frappe.db.get_value("Bin",
{"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"})) {"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): def test_uom_conversion_factor(self):
if frappe.db.exists('Item', 'Test Item UOM'): if frappe.db.exists('Item', 'Test Item UOM'):
frappe.delete_doc('Item', 'Test Item UOM') frappe.delete_doc('Item', 'Test Item UOM')

View File

@ -56,14 +56,13 @@ class MaterialRequest(BuyingController):
if actual_so_qty and (flt(so_items[so_no][item]) + already_indented > actual_so_qty): 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)) 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): def validate(self):
super(MaterialRequest, self).validate() super(MaterialRequest, self).validate()
self.validate_schedule_date() self.validate_schedule_date()
self.check_for_on_hold_or_closed_status('Sales Order', 'sales_order') self.check_for_on_hold_or_closed_status('Sales Order', 'sales_order')
self.validate_uom_is_integer("uom", "qty") self.validate_uom_is_integer("uom", "qty")
self.validate_material_request_type()
if not self.status: if not self.status:
self.status = "Draft" self.status = "Draft"
@ -83,6 +82,12 @@ class MaterialRequest(BuyingController):
self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_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): def set_title(self):
'''Set title as comma separated list of items''' '''Set title as comma separated list of items'''
if not self.title: if not self.title:

View File

@ -44,6 +44,7 @@ def get_sle(**args):
class TestStockEntry(ERPNextTestCase): class TestStockEntry(ERPNextTestCase):
def tearDown(self): def tearDown(self):
frappe.db.rollback()
frappe.set_user("Administrator") frappe.set_user("Administrator")
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0") frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
@ -565,6 +566,7 @@ class TestStockEntry(ERPNextTestCase):
st1.set_stock_entry_type() st1.set_stock_entry_type()
st1.insert() st1.insert()
st1.submit() st1.submit()
st1.cancel()
frappe.set_user("Administrator") frappe.set_user("Administrator")
remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com") remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com")
@ -689,6 +691,8 @@ class TestStockEntry(ERPNextTestCase):
bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item", bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item",
"is_default": 1, "docstatus": 1}) "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 = frappe.new_doc("Work Order")
work_order.update({ work_order.update({
"company": "_Test Company", "company": "_Test Company",
@ -1023,13 +1027,10 @@ class TestStockEntry(ERPNextTestCase):
# Check if FG cost is calculated based on RM total cost # Check if FG cost is calculated based on RM total cost
# RM total cost = 200, FG rate = 200/4(FG qty) = 50 # 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.value_difference, 0.0)
self.assertEqual(se.total_incoming_value, se.total_outgoing_value) self.assertEqual(se.total_incoming_value, se.total_outgoing_value)
# teardown
se.delete()
@change_settings("Stock Settings", {"allow_negative_stock": 0}) @change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_future_negative_sle(self): def test_future_negative_sle(self):
# Initialize item, batch, warehouse, opening qty # Initialize item, batch, warehouse, opening qty

View File

@ -73,10 +73,11 @@ class TestReports(unittest.TestCase):
def test_execute_all_stock_reports(self): def test_execute_all_stock_reports(self):
"""Test that all script report in stock modules are executable with supported filters""" """Test that all script report in stock modules are executable with supported filters"""
for report, filter in REPORT_FILTER_TEST_CASES: for report, filter in REPORT_FILTER_TEST_CASES:
execute_script_report( with self.subTest(report=report):
report_name=report, execute_script_report(
module="Stock", report_name=report,
filters=filter, module="Stock",
default_filters=DEFAULT_FILTERS, filters=filter,
optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None, default_filters=DEFAULT_FILTERS,
) optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
)

View File

@ -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.

View File

@ -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

View File

@ -13,7 +13,7 @@
<li class="wishlist wishlist-icon hidden"> <li class="wishlist wishlist-icon hidden">
<a class="nav-link" href="/wishlist"> <a class="nav-link" href="/wishlist">
<svg class="icon icon-lg"> <svg class="icon icon-lg">
<use href="#icon-heart-active"></use> <use href="#icon-heart"></use>
</svg> </svg>
<span class="badge badge-primary shopping-badge" id="wish-count"></span> <span class="badge badge-primary shopping-badge" id="wish-count"></span>
</a> </a>

View File

@ -3731,7 +3731,7 @@ Earliest Age,Frühestes Alter,
Edit Details,Details bearbeiten, Edit Details,Details bearbeiten,
Edit Profile,Profil 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, 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, Email Campaigns,E-Mail-Kampagnen,
Employee ID is linked with another instructor,Die Mitarbeiter-ID ist mit einem anderen Ausbilder verknüpft, Employee ID is linked with another instructor,Die Mitarbeiter-ID ist mit einem anderen Ausbilder verknüpft,
Employee Tax and Benefits,Mitarbeitersteuern und -leistungen, Employee Tax and Benefits,Mitarbeitersteuern und -leistungen,
@ -6487,7 +6487,7 @@ Select Users,Wählen Sie Benutzer aus,
Send Emails At,Die E-Mails senden um, Send Emails At,Die E-Mails senden um,
Reminder,Erinnerung, Reminder,Erinnerung,
Daily Work Summary Group User,Tägliche Arbeit Zusammenfassung Gruppenbenutzer, Daily Work Summary Group User,Tägliche Arbeit Zusammenfassung Gruppenbenutzer,
email,Email, email,E-Mail,
Parent Department,Elternabteilung, Parent Department,Elternabteilung,
Leave Block List,Urlaubssperrenliste, Leave Block List,Urlaubssperrenliste,
Days for which Holidays are blocked for this department.,"Tage, an denen eine Urlaubssperre für diese Abteilung gilt.", Days for which Holidays are blocked for this department.,"Tage, an denen eine Urlaubssperre für diese Abteilung gilt.",

Can't render this file because it is too large.

View File

@ -11,7 +11,7 @@
{% if frappe.session.user == 'Guest' %} {% if frappe.session.user == 'Guest' %}
<a id="signup" class="btn btn-primary btn-lg" href="/login#signup">{{_('Sign Up')}}</a> <a id="signup" class="btn btn-primary btn-lg" href="/login#signup">{{_('Sign Up')}}</a>
{% elif not has_access %} {% 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 %} {% endif %}
</p> </p>
</div> </div>
@ -20,34 +20,35 @@
<script type="text/javascript"> <script type="text/javascript">
frappe.ready(() => { frappe.ready(() => {
btn = document.getElementById('enroll'); btn = document.getElementById('enroll');
if (btn) btn.disabled = false;
}) })
function enroll() { function enroll() {
let params = frappe.utils.get_query_params() let params = frappe.utils.get_query_params()
let btn = document.getElementById('enroll'); let btn = document.getElementById('enroll');
btn.disbaled = true;
btn.innerText = __('Enrolling...')
let opts = { let opts = {
method: 'erpnext.education.utils.enroll_in_program', method: 'erpnext.education.utils.enroll_in_program',
args: { args: {
program_name: params.program program_name: params.program
} },
freeze: true,
freeze_message: __('Enrolling...')
} }
frappe.call(opts).then(res => { frappe.call(opts).then(res => {
let success_dialog = new frappe.ui.Dialog({ let success_dialog = new frappe.ui.Dialog({
title: __('Success'), title: __('Success'),
primary_action_label: __('View Program Content'),
primary_action: function() {
window.location.reload();
},
secondary_action: function() { 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(); success_dialog.show();
btn.disbaled = false; success_dialog.set_message(__('You have successfully enrolled for the program '));
}) })
} }
</script> </script>

View File

@ -62,8 +62,7 @@ def get_category_records(categories):
"parent_item_group": "All Item Groups", "parent_item_group": "All Item Groups",
"show_in_website": 1 "show_in_website": 1
}, },
fields=["name", "parent_item_group", "is_group", "image", "route"], fields=["name", "parent_item_group", "is_group", "image", "route"]
as_dict=True
) )
else: else:
doctype = frappe.unscrub(category) doctype = frappe.unscrub(category)
@ -71,7 +70,7 @@ def get_category_records(categories):
if frappe.get_meta(doctype, cached=True).get_field("image"): if frappe.get_meta(doctype, cached=True).get_field("image"):
fields += ["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 return categorical_data