Merge branch 'develop' into psoa_account_filter
This commit is contained in:
commit
5464ba6430
@ -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"""
|
||||||
|
@ -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))
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
})
|
})
|
||||||
|
@ -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"
|
||||||
|
@ -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"),
|
||||||
|
@ -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")
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
)
|
||||||
|
@ -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?
|
||||||
|
@ -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")
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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")
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
)
|
||||||
|
@ -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
|
||||||
|
37
erpnext/patches/v13_0/update_accounts_in_loan_docs.py
Normal file
37
erpnext/patches/v13_0/update_accounts_in_loan_docs.py
Normal 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()
|
@ -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):
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -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
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -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"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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')
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
)
|
||||||
|
103
erpnext/stock/spec/README.md
Normal file
103
erpnext/stock/spec/README.md
Normal 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.
|
38
erpnext/stock/spec/reposting.md
Normal file
38
erpnext/stock/spec/reposting.md
Normal 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
|
@ -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>
|
||||||
|
@ -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.
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user