Merge branch 'develop' of https://github.com/frappe/erpnext into opening_entry
diff --git a/CODEOWNERS b/CODEOWNERS
index e406f8f..c4ea163 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -4,7 +4,7 @@
# the repo. Unless a later match takes precedence,
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
-erpnext/assets/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
+erpnext/assets/ @anandbaburajan @deepeshgarg007
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
@@ -16,6 +16,7 @@
erpnext/manufacturing/ @rohitwaghchaure @s-aga-r
erpnext/quality_management/ @rohitwaghchaure @s-aga-r
erpnext/stock/ @rohitwaghchaure @s-aga-r
+erpnext/subcontracting @rohitwaghchaure @s-aga-r
erpnext/crm/ @NagariaHussain
erpnext/education/ @rutwikhdev
diff --git a/README.md b/README.md
index 0708266..44bd729 100644
--- a/README.md
+++ b/README.md
@@ -65,7 +65,7 @@
1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext.
3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers.
-4. [Telegram Group](https://t.me/erpnexthelp) - Get instant help from huge community of users.
+4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users.
## Contributing
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
index 75f8f06..9e67c4c 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
@@ -29,6 +29,7 @@
"root_type",
"is_group",
"tax_rate",
+ "account_currency",
]:
account_number = cstr(child.get("account_number")).strip()
@@ -95,7 +96,17 @@
is_group = child.get("is_group")
elif len(
set(child.keys())
- - set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])
+ - set(
+ [
+ "account_name",
+ "account_type",
+ "root_type",
+ "is_group",
+ "tax_rate",
+ "account_number",
+ "account_currency",
+ ]
+ )
):
is_group = 1
else:
@@ -185,6 +196,7 @@
"root_type",
"tax_rate",
"account_number",
+ "account_currency",
],
order_by="lft, rgt",
)
@@ -267,6 +279,7 @@
"root_type",
"is_group",
"tax_rate",
+ "account_currency",
]:
continue
diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js
index 059e1d3..35d606b 100644
--- a/erpnext/accounts/doctype/bank/bank.js
+++ b/erpnext/accounts/doctype/bank/bank.js
@@ -118,6 +118,10 @@
}
plaid_success(token, response) {
- frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
+ frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.update_bank_account_ids', {
+ response: response,
+ }).then(() => {
+ frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
+ });
}
};
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
index c083189..ae84154 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
@@ -155,7 +155,7 @@
}
},
- render_chart: frappe.utils.debounce((frm) => {
+ render_chart(frm) {
frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager(
{
$reconciliation_tool_cards: frm.get_field(
@@ -167,7 +167,7 @@
currency: frm.currency,
}
);
- }, 500),
+ },
render(frm) {
if (frm.doc.bank_account) {
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index 4ba6146..c4a23a6 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -10,7 +10,7 @@
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import cint, flt
-from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_paid_amount
+from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
get_amounts_not_reflected_in_system,
get_entries,
@@ -28,7 +28,7 @@
filters = []
filters.append(["bank_account", "=", bank_account])
filters.append(["docstatus", "=", 1])
- filters.append(["unallocated_amount", ">", 0])
+ filters.append(["unallocated_amount", ">", 0.0])
if to_date:
filters.append(["date", "<=", to_date])
if from_date:
@@ -58,7 +58,7 @@
@frappe.whitelist()
def get_account_balance(bank_account, till_date):
# returns account balance till the specified date
- account = frappe.get_cached_value("Bank Account", bank_account, "account")
+ account = frappe.db.get_value("Bank Account", bank_account, "account")
filters = frappe._dict(
{"account": account, "report_date": till_date, "include_pos_transactions": 1}
)
@@ -66,7 +66,7 @@
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
- total_debit, total_credit = 0, 0
+ total_debit, total_credit = 0.0, 0.0
for d in data:
total_debit += flt(d.debit)
total_credit += flt(d.credit)
@@ -131,10 +131,8 @@
fieldname=["name", "deposit", "withdrawal", "bank_account"],
as_dict=True,
)[0]
- company_account = frappe.get_cached_value(
- "Bank Account", bank_transaction.bank_account, "account"
- )
- account_type = frappe.get_cached_value("Account", second_account, "account_type")
+ company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
+ account_type = frappe.db.get_value("Account", second_account, "account_type")
if account_type in ["Receivable", "Payable"]:
if not (party_type and party):
frappe.throw(
@@ -147,10 +145,8 @@
accounts.append(
{
"account": second_account,
- "credit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
- "debit_in_account_currency": bank_transaction.withdrawal
- if bank_transaction.withdrawal > 0
- else 0,
+ "credit_in_account_currency": bank_transaction.deposit,
+ "debit_in_account_currency": bank_transaction.withdrawal,
"party_type": party_type,
"party": party,
}
@@ -160,14 +156,12 @@
{
"account": company_account,
"bank_account": bank_transaction.bank_account,
- "credit_in_account_currency": bank_transaction.withdrawal
- if bank_transaction.withdrawal > 0
- else 0,
- "debit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
+ "credit_in_account_currency": bank_transaction.withdrawal,
+ "debit_in_account_currency": bank_transaction.deposit,
}
)
- company = frappe.get_cached_value("Account", company_account, "company")
+ company = frappe.get_value("Account", company_account, "company")
journal_entry_dict = {
"voucher_type": entry_type,
@@ -187,16 +181,22 @@
journal_entry.insert()
journal_entry.submit()
- if bank_transaction.deposit > 0:
+ if bank_transaction.deposit > 0.0:
paid_amount = bank_transaction.deposit
else:
paid_amount = bank_transaction.withdrawal
vouchers = json.dumps(
- [{"payment_doctype": "Journal Entry", "payment_name": journal_entry.name, "amount": paid_amount}]
+ [
+ {
+ "payment_doctype": "Journal Entry",
+ "payment_name": journal_entry.name,
+ "amount": paid_amount,
+ }
+ ]
)
- return reconcile_vouchers(bank_transaction.name, vouchers)
+ return reconcile_vouchers(bank_transaction_name, vouchers)
@frappe.whitelist()
@@ -220,12 +220,10 @@
as_dict=True,
)[0]
paid_amount = bank_transaction.unallocated_amount
- payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay"
+ payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay"
- company_account = frappe.get_cached_value(
- "Bank Account", bank_transaction.bank_account, "account"
- )
- company = frappe.get_cached_value("Account", company_account, "company")
+ company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
+ company = frappe.get_value("Account", company_account, "company")
payment_entry_dict = {
"company": company,
"payment_type": payment_type,
@@ -261,9 +259,15 @@
payment_entry.submit()
vouchers = json.dumps(
- [{"payment_doctype": "Payment Entry", "payment_name": payment_entry.name, "amount": paid_amount}]
+ [
+ {
+ "payment_doctype": "Payment Entry",
+ "payment_name": payment_entry.name,
+ "amount": paid_amount,
+ }
+ ]
)
- return reconcile_vouchers(bank_transaction.name, vouchers)
+ return reconcile_vouchers(bank_transaction_name, vouchers)
@frappe.whitelist()
@@ -345,59 +349,7 @@
# updated clear date of all the vouchers based on the bank transaction
vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
- company_account = frappe.get_cached_value("Bank Account", transaction.bank_account, "account")
-
- if transaction.unallocated_amount == 0:
- frappe.throw(_("This bank transaction is already fully reconciled"))
- total_amount = 0
- for voucher in vouchers:
- voucher["payment_entry"] = frappe.get_doc(voucher["payment_doctype"], voucher["payment_name"])
- total_amount += get_paid_amount(
- frappe._dict(
- {
- "payment_document": voucher["payment_doctype"],
- "payment_entry": voucher["payment_name"],
- }
- ),
- transaction.currency,
- company_account,
- )
-
- if total_amount > transaction.unallocated_amount:
- frappe.throw(
- _(
- "The sum total of amounts of all selected vouchers should be less than the unallocated amount of the bank transaction"
- )
- )
- account = frappe.get_cached_value("Bank Account", transaction.bank_account, "account")
-
- for voucher in vouchers:
- gl_entry = frappe.db.get_value(
- "GL Entry",
- dict(
- account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
- ),
- ["credit_in_account_currency as credit", "debit_in_account_currency as debit"],
- as_dict=1,
- )
- gl_amount, transaction_amount = (
- (gl_entry.credit, transaction.deposit)
- if gl_entry.credit > 0
- else (gl_entry.debit, transaction.withdrawal)
- )
- allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount
-
- transaction.append(
- "payment_entries",
- {
- "payment_document": voucher["payment_entry"].doctype,
- "payment_entry": voucher["payment_entry"].name,
- "allocated_amount": allocated_amount,
- },
- )
-
- transaction.save()
- transaction.update_allocations()
+ transaction.add_payment_entries(vouchers)
return frappe.get_doc("Bank Transaction", bank_transaction_name)
@@ -416,9 +368,9 @@
bank_account = frappe.db.get_values(
"Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
)[0]
- (account, company) = (bank_account.account, bank_account.company)
+ (gl_account, company) = (bank_account.account, bank_account.company)
matching = check_matching(
- account,
+ gl_account,
company,
transaction,
document_types,
@@ -428,7 +380,27 @@
from_reference_date,
to_reference_date,
)
- return matching
+ return subtract_allocations(gl_account, matching)
+
+
+def subtract_allocations(gl_account, vouchers):
+ "Look up & subtract any existing Bank Transaction allocations"
+ copied = []
+ for voucher in vouchers:
+ rows = get_total_allocated_amount(voucher[1], voucher[2])
+ amount = None
+ for row in rows:
+ if row["gl_account"] == gl_account:
+ amount = row["total"]
+ break
+
+ if amount:
+ l = list(voucher)
+ l[3] -= amount
+ copied.append(tuple(l))
+ else:
+ copied.append(voucher)
+ return copied
def check_matching(
@@ -442,6 +414,7 @@
from_reference_date,
to_reference_date,
):
+ exact_match = True if "exact_match" in document_types else False
# combine all types of vouchers
subquery = get_queries(
bank_account,
@@ -453,10 +426,11 @@
filter_by_reference_date,
from_reference_date,
to_reference_date,
+ exact_match,
)
filters = {
"amount": transaction.unallocated_amount,
- "payment_type": "Receive" if transaction.deposit > 0 else "Pay",
+ "payment_type": "Receive" if transaction.deposit > 0.0 else "Pay",
"reference_no": transaction.reference_number,
"party_type": transaction.party_type,
"party": transaction.party,
@@ -465,7 +439,9 @@
matching_vouchers = []
- matching_vouchers.extend(get_loan_vouchers(bank_account, transaction, document_types, filters))
+ matching_vouchers.extend(
+ get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match)
+ )
for query in subquery:
matching_vouchers.extend(
@@ -487,10 +463,10 @@
filter_by_reference_date,
from_reference_date,
to_reference_date,
+ exact_match,
):
# get queries to get matching vouchers
- amount_condition = "=" if "exact_match" in document_types else "<="
- account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from"
+ account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
queries = []
# get matching queries from all the apps
@@ -501,7 +477,7 @@
company,
transaction,
document_types,
- amount_condition,
+ exact_match,
account_from_to,
from_date,
to_date,
@@ -520,7 +496,7 @@
company,
transaction,
document_types,
- amount_condition,
+ exact_match,
account_from_to,
from_date,
to_date,
@@ -530,8 +506,8 @@
):
queries = []
if "payment_entry" in document_types:
- pe_amount_matching = get_pe_matching_query(
- amount_condition,
+ query = get_pe_matching_query(
+ exact_match,
account_from_to,
transaction,
from_date,
@@ -540,11 +516,11 @@
from_reference_date,
to_reference_date,
)
- queries.extend([pe_amount_matching])
+ queries.append(query)
if "journal_entry" in document_types:
- je_amount_matching = get_je_matching_query(
- amount_condition,
+ query = get_je_matching_query(
+ exact_match,
transaction,
from_date,
to_date,
@@ -552,34 +528,70 @@
from_reference_date,
to_reference_date,
)
- queries.extend([je_amount_matching])
+ queries.append(query)
- if transaction.deposit > 0 and "sales_invoice" in document_types:
- si_amount_matching = get_si_matching_query(amount_condition)
- queries.extend([si_amount_matching])
+ if transaction.deposit > 0.0 and "sales_invoice" in document_types:
+ query = get_si_matching_query(exact_match)
+ queries.append(query)
- if transaction.withdrawal > 0:
+ if transaction.withdrawal > 0.0:
if "purchase_invoice" in document_types:
- pi_amount_matching = get_pi_matching_query(amount_condition)
- queries.extend([pi_amount_matching])
+ query = get_pi_matching_query(exact_match)
+ queries.append(query)
+
+ if "bank_transaction" in document_types:
+ query = get_bt_matching_query(exact_match, transaction)
+ queries.append(query)
return queries
-def get_loan_vouchers(bank_account, transaction, document_types, filters):
+def get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match):
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.withdrawal > 0.0 and "loan_disbursement" in document_types:
+ vouchers.extend(get_ld_matching_query(bank_account, exact_match, filters))
- if transaction.deposit > 0 and "loan_repayment" in document_types:
- vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters))
+ if transaction.deposit > 0.0 and "loan_repayment" in document_types:
+ vouchers.extend(get_lr_matching_query(bank_account, exact_match, filters))
return vouchers
-def get_ld_matching_query(bank_account, amount_condition, filters):
+def get_bt_matching_query(exact_match, transaction):
+ # get matching bank transaction query
+ # find bank transactions in the same bank account with opposite sign
+ # same bank account must have same company and currency
+ field = "deposit" if transaction.withdrawal > 0.0 else "withdrawal"
+
+ return f"""
+
+ SELECT
+ (CASE WHEN reference_number = %(reference_no)s THEN 1 ELSE 0 END
+ + CASE WHEN {field} = %(amount)s THEN 1 ELSE 0 END
+ + CASE WHEN ( party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
+ + CASE WHEN unallocated_amount = %(amount)s THEN 1 ELSE 0 END
+ + 1) AS rank,
+ 'Bank Transaction' AS doctype,
+ name,
+ unallocated_amount AS paid_amount,
+ reference_number AS reference_no,
+ date AS reference_date,
+ party,
+ party_type,
+ date AS posting_date,
+ currency
+ FROM
+ `tabBank Transaction`
+ WHERE
+ status != 'Reconciled'
+ AND name != '{transaction.name}'
+ AND bank_account = '{transaction.bank_account}'
+ AND {field} {'= %(amount)s' if exact_match else '> 0.0'}
+ """
+
+
+def get_ld_matching_query(bank_account, exact_match, 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(
@@ -607,17 +619,17 @@
.where(loan_disbursement.disbursement_account == bank_account)
)
- if amount_condition:
+ if exact_match:
query.where(loan_disbursement.disbursed_amount == filters.get("amount"))
else:
- query.where(loan_disbursement.disbursed_amount <= filters.get("amount"))
+ query.where(loan_disbursement.disbursed_amount > 0.0)
vouchers = query.run(as_list=True)
return vouchers
-def get_lr_matching_query(bank_account, amount_condition, filters):
+def get_lr_matching_query(bank_account, exact_match, 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(
@@ -648,10 +660,10 @@
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_repayment.repay_from_salary == 0))
- if amount_condition:
+ if exact_match:
query.where(loan_repayment.amount_paid == filters.get("amount"))
else:
- query.where(loan_repayment.amount_paid <= filters.get("amount"))
+ query.where(loan_repayment.amount_paid > 0.0)
vouchers = query.run()
@@ -659,7 +671,7 @@
def get_pe_matching_query(
- amount_condition,
+ exact_match,
account_from_to,
transaction,
from_date,
@@ -669,7 +681,7 @@
to_reference_date,
):
# get matching payment entries query
- if transaction.deposit > 0:
+ if transaction.deposit > 0.0:
currency_field = "paid_to_account_currency as currency"
else:
currency_field = "paid_from_account_currency as currency"
@@ -684,7 +696,8 @@
return f"""
SELECT
(CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
- + CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
+ + CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
+ + CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
+ 1 ) AS rank,
'Payment Entry' as doctype,
name,
@@ -698,20 +711,19 @@
FROM
`tabPayment Entry`
WHERE
- paid_amount {amount_condition} %(amount)s
- AND docstatus = 1
+ docstatus = 1
AND payment_type IN (%(payment_type)s, 'Internal Transfer')
AND ifnull(clearance_date, '') = ""
AND {account_from_to} = %(bank_account)s
+ AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
{filter_by_date}
{filter_by_reference_no}
order by{order_by}
-
"""
def get_je_matching_query(
- amount_condition,
+ exact_match,
transaction,
from_date,
to_date,
@@ -723,7 +735,7 @@
# We have mapping at the bank level
# So one bank could have both types of bank accounts like asset and liability
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
- cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
+ cr_or_dr = "credit" if transaction.withdrawal > 0.0 else "debit"
filter_by_date = f"AND je.posting_date between '{from_date}' and '{to_date}'"
order_by = " je.posting_date"
filter_by_reference_no = ""
@@ -735,26 +747,29 @@
return f"""
SELECT
(CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END
+ + CASE WHEN jea.{cr_or_dr}_in_account_currency = %(amount)s THEN 1 ELSE 0 END
+ 1) AS rank ,
- 'Journal Entry' as doctype,
+ 'Journal Entry' AS doctype,
je.name,
- jea.{cr_or_dr}_in_account_currency as paid_amount,
- je.cheque_no as reference_no,
- je.cheque_date as reference_date,
- je.pay_to_recd_from as party,
+ jea.{cr_or_dr}_in_account_currency AS paid_amount,
+ je.cheque_no AS reference_no,
+ je.cheque_date AS reference_date,
+ je.pay_to_recd_from AS party,
jea.party_type,
je.posting_date,
- jea.account_currency as currency
+ jea.account_currency AS currency
FROM
- `tabJournal Entry Account` as jea
+ `tabJournal Entry Account` AS jea
JOIN
- `tabJournal Entry` as je
+ `tabJournal Entry` AS je
ON
jea.parent = je.name
WHERE
- (je.clearance_date is null or je.clearance_date='0000-00-00')
+ je.docstatus = 1
+ AND je.voucher_type NOT IN ('Opening Entry')
+ AND (je.clearance_date IS NULL OR je.clearance_date='0000-00-00')
AND jea.account = %(bank_account)s
- AND jea.{cr_or_dr}_in_account_currency {amount_condition} %(amount)s
+ AND jea.{cr_or_dr}_in_account_currency {'= %(amount)s' if exact_match else '> 0.0'}
AND je.docstatus = 1
{filter_by_date}
{filter_by_reference_no}
@@ -762,11 +777,12 @@
"""
-def get_si_matching_query(amount_condition):
- # get matchin sales invoice query
+def get_si_matching_query(exact_match):
+ # get matching sales invoice query
return f"""
SELECT
- ( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END
+ ( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END
+ + CASE WHEN sip.amount = %(amount)s THEN 1 ELSE 0 END
+ 1 ) AS rank,
'Sales Invoice' as doctype,
si.name,
@@ -784,18 +800,20 @@
`tabSales Invoice` as si
ON
sip.parent = si.name
- WHERE (sip.clearance_date is null or sip.clearance_date='0000-00-00')
+ WHERE
+ si.docstatus = 1
+ AND (sip.clearance_date is null or sip.clearance_date='0000-00-00')
AND sip.account = %(bank_account)s
- AND sip.amount {amount_condition} %(amount)s
- AND si.docstatus = 1
+ AND sip.amount {'= %(amount)s' if exact_match else '> 0.0'}
"""
-def get_pi_matching_query(amount_condition):
- # get matching purchase invoice query
+def get_pi_matching_query(exact_match):
+ # get matching purchase invoice query when they are also used as payment entries (is_paid)
return f"""
SELECT
( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END
+ + CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
+ 1 ) AS rank,
'Purchase Invoice' as doctype,
name,
@@ -809,9 +827,9 @@
FROM
`tabPurchase Invoice`
WHERE
- paid_amount {amount_condition} %(amount)s
- AND docstatus = 1
+ docstatus = 1
AND is_paid = 1
AND ifnull(clearance_date, '') = ""
- AND cash_bank_account = %(bank_account)s
+ AND cash_bank_account = %(bank_account)s
+ AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
"""
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.js b/erpnext/accounts/doctype/bank_transaction/bank_transaction.js
index 6f2900a..e548b4c 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.js
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.js
@@ -12,8 +12,13 @@
};
});
},
-
- bank_account: function(frm) {
+ refresh(frm) {
+ frm.add_custom_button(__('Unreconcile Transaction'), () => {
+ frm.call('remove_payment_entries')
+ .then( () => frm.refresh() );
+ });
+ },
+ bank_account: function (frm) {
set_bank_statement_filter(frm);
},
@@ -34,6 +39,7 @@
"Journal Entry",
"Sales Invoice",
"Purchase Invoice",
+ "Bank Transaction",
];
}
});
@@ -49,7 +55,7 @@
frappe
.xcall(
"erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment",
- { doctype: cdt, docname: cdn }
+ { doctype: cdt, docname: cdn, bt_name: frm.doc.name }
)
.then((e) => {
if (e == "success") {
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
index 2bdaa10..768d2f0 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
@@ -20,9 +20,11 @@
"currency",
"section_break_10",
"description",
- "section_break_14",
"reference_number",
+ "column_break_10",
"transaction_id",
+ "transaction_type",
+ "section_break_14",
"payment_entries",
"section_break_18",
"allocated_amount",
@@ -190,11 +192,21 @@
"label": "Withdrawal",
"oldfieldname": "credit",
"options": "currency"
+ },
+ {
+ "fieldname": "column_break_10",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "transaction_type",
+ "fieldtype": "Data",
+ "label": "Transaction Type",
+ "length": 50
}
],
"is_submittable": 1,
"links": [],
- "modified": "2022-03-21 19:05:04.208222",
+ "modified": "2022-05-29 18:36:50.475964",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",
@@ -248,4 +260,4 @@
"states": [],
"title_field": "bank_account",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 9b36c93..1516237 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -1,9 +1,6 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-
-from functools import reduce
-
import frappe
from frappe.utils import flt
@@ -18,72 +15,137 @@
self.clear_linked_payment_entries()
self.set_status()
+ _saving_flag = False
+
+ # nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
def on_update_after_submit(self):
- self.update_allocations()
- self.clear_linked_payment_entries()
- self.set_status(update=True)
+ "Run on save(). Avoid recursion caused by multiple saves"
+ if not self._saving_flag:
+ self._saving_flag = True
+ self.clear_linked_payment_entries()
+ self.update_allocations()
+ self._saving_flag = False
def on_cancel(self):
self.clear_linked_payment_entries(for_cancel=True)
self.set_status(update=True)
def update_allocations(self):
+ "The doctype does not allow modifications after submission, so write to the db direct"
if self.payment_entries:
- allocated_amount = reduce(
- lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries]
- )
+ allocated_amount = sum(p.allocated_amount for p in self.payment_entries)
else:
- allocated_amount = 0
+ allocated_amount = 0.0
- if allocated_amount:
- frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount))
- frappe.db.set_value(
- self.doctype,
- self.name,
- "unallocated_amount",
- abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount),
- )
+ amount = abs(flt(self.withdrawal) - flt(self.deposit))
+ self.db_set("allocated_amount", flt(allocated_amount))
+ self.db_set("unallocated_amount", amount - flt(allocated_amount))
+ self.reload()
+ self.set_status(update=True)
- else:
- frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0)
- frappe.db.set_value(
- self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit))
- )
+ def add_payment_entries(self, vouchers):
+ "Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
+ if 0.0 >= self.unallocated_amount:
+ frappe.throw(frappe._(f"Bank Transaction {self.name} is already fully reconciled"))
- amount = self.deposit or self.withdrawal
- if amount == self.allocated_amount:
- frappe.db.set_value(self.doctype, self.name, "status", "Reconciled")
+ added = False
+ for voucher in vouchers:
+ # Can't add same voucher twice
+ found = False
+ for pe in self.payment_entries:
+ if (
+ pe.payment_document == voucher["payment_doctype"]
+ and pe.payment_entry == voucher["payment_name"]
+ ):
+ found = True
+
+ if not found:
+ pe = {
+ "payment_document": voucher["payment_doctype"],
+ "payment_entry": voucher["payment_name"],
+ "allocated_amount": 0.0, # Temporary
+ }
+ child = self.append("payment_entries", pe)
+ added = True
+
+ # runs on_update_after_submit
+ if added:
+ self.save()
+
+ def allocate_payment_entries(self):
+ """Refactored from bank reconciliation tool.
+ Non-zero allocations must be amended/cleared manually
+ Get the bank transaction amount (b) and remove as we allocate
+ For each payment_entry if allocated_amount == 0:
+ - get the amount already allocated against all transactions (t), need latest date
+ - get the voucher amount (from gl) (v)
+ - allocate (a = v - t)
+ - a = 0: should already be cleared, so clear & remove payment_entry
+ - 0 < a <= u: allocate a & clear
+ - 0 < a, a > u: allocate u
+ - 0 > a: Error: already over-allocated
+ - clear means: set the latest transaction date as clearance date
+ """
+ gl_bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account")
+ remaining_amount = self.unallocated_amount
+ for payment_entry in self.payment_entries:
+ if payment_entry.allocated_amount == 0.0:
+ unallocated_amount, should_clear, latest_transaction = get_clearance_details(
+ self, payment_entry
+ )
+
+ if 0.0 == unallocated_amount:
+ if should_clear:
+ latest_transaction.clear_linked_payment_entry(payment_entry)
+ self.db_delete_payment_entry(payment_entry)
+
+ elif remaining_amount <= 0.0:
+ self.db_delete_payment_entry(payment_entry)
+
+ elif 0.0 < unallocated_amount and unallocated_amount <= remaining_amount:
+ payment_entry.db_set("allocated_amount", unallocated_amount)
+ remaining_amount -= unallocated_amount
+ if should_clear:
+ latest_transaction.clear_linked_payment_entry(payment_entry)
+
+ elif 0.0 < unallocated_amount and unallocated_amount > remaining_amount:
+ payment_entry.db_set("allocated_amount", remaining_amount)
+ remaining_amount = 0.0
+
+ elif 0.0 > unallocated_amount:
+ self.db_delete_payment_entry(payment_entry)
+ frappe.throw(
+ frappe._(f"Voucher {payment_entry.payment_entry} is over-allocated by {unallocated_amount}")
+ )
self.reload()
- def clear_linked_payment_entries(self, for_cancel=False):
+ def db_delete_payment_entry(self, payment_entry):
+ frappe.db.delete("Bank Transaction Payments", {"name": payment_entry.name})
+
+ @frappe.whitelist()
+ def remove_payment_entries(self):
for payment_entry in self.payment_entries:
- if payment_entry.payment_document == "Sales Invoice":
- self.clear_sales_invoice(payment_entry, for_cancel=for_cancel)
- elif payment_entry.payment_document in get_doctypes_for_bank_reconciliation():
- self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
+ self.remove_payment_entry(payment_entry)
+ # runs on_update_after_submit
+ self.save()
- def clear_simple_entry(self, payment_entry, for_cancel=False):
- if payment_entry.payment_document == "Payment Entry":
- if (
- frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type")
- == "Internal Transfer"
- ):
- if len(get_reconciled_bank_transactions(payment_entry)) < 2:
- return
+ def remove_payment_entry(self, payment_entry):
+ "Clear payment entry and clearance"
+ self.clear_linked_payment_entry(payment_entry, for_cancel=True)
+ self.remove(payment_entry)
- clearance_date = self.date if not for_cancel else None
- frappe.db.set_value(
- payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", clearance_date
- )
+ def clear_linked_payment_entries(self, for_cancel=False):
+ if for_cancel:
+ for payment_entry in self.payment_entries:
+ self.clear_linked_payment_entry(payment_entry, for_cancel)
+ else:
+ self.allocate_payment_entries()
- def clear_sales_invoice(self, payment_entry, for_cancel=False):
- clearance_date = self.date if not for_cancel else None
- frappe.db.set_value(
- "Sales Invoice Payment",
- dict(parenttype=payment_entry.payment_document, parent=payment_entry.payment_entry),
- "clearance_date",
- clearance_date,
+ def clear_linked_payment_entry(self, payment_entry, for_cancel=False):
+ clearance_date = None if for_cancel else self.date
+ set_voucher_clearance(
+ payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self
)
@@ -93,38 +155,112 @@
return frappe.get_hooks("bank_reconciliation_doctypes")
-def get_reconciled_bank_transactions(payment_entry):
- reconciled_bank_transactions = frappe.get_all(
- "Bank Transaction Payments",
- filters={"payment_entry": payment_entry.payment_entry},
- fields=["parent"],
+def get_clearance_details(transaction, payment_entry):
+ """
+ There should only be one bank gle for a voucher.
+ Could be none for a Bank Transaction.
+ But if a JE, could affect two banks.
+ Should only clear the voucher if all bank gles are allocated.
+ """
+ gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
+ gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry)
+ bt_allocations = get_total_allocated_amount(
+ payment_entry.payment_document, payment_entry.payment_entry
)
- return reconciled_bank_transactions
+ unallocated_amount = min(
+ transaction.unallocated_amount,
+ get_paid_amount(payment_entry, transaction.currency, gl_bank_account),
+ )
+ unmatched_gles = len(gles)
+ latest_transaction = transaction
+ for gle in gles:
+ if gle["gl_account"] == gl_bank_account:
+ if gle["amount"] <= 0.0:
+ frappe.throw(
+ frappe._(f"Voucher {payment_entry.payment_entry} value is broken: {gle['amount']}")
+ )
+
+ unmatched_gles -= 1
+ unallocated_amount = gle["amount"]
+ for a in bt_allocations:
+ if a["gl_account"] == gle["gl_account"]:
+ unallocated_amount = gle["amount"] - a["total"]
+ if frappe.utils.getdate(transaction.date) < a["latest_date"]:
+ latest_transaction = frappe.get_doc("Bank Transaction", a["latest_name"])
+ else:
+ # Must be a Journal Entry affecting more than one bank
+ for a in bt_allocations:
+ if a["gl_account"] == gle["gl_account"] and a["total"] == gle["amount"]:
+ unmatched_gles -= 1
+
+ return unallocated_amount, unmatched_gles == 0, latest_transaction
-def get_total_allocated_amount(payment_entry):
- return frappe.db.sql(
+def get_related_bank_gl_entries(doctype, docname):
+ # nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
+ result = frappe.db.sql(
"""
SELECT
- SUM(btp.allocated_amount) as allocated_amount,
- bt.name
+ ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount,
+ gle.account AS gl_account
FROM
- `tabBank Transaction Payments` as btp
+ `tabGL Entry` gle
LEFT JOIN
- `tabBank Transaction` bt ON bt.name=btp.parent
+ `tabAccount` ac ON ac.name=gle.account
WHERE
- btp.payment_document = %s
- AND
- btp.payment_entry = %s
- AND
- bt.docstatus = 1""",
- (payment_entry.payment_document, payment_entry.payment_entry),
+ ac.account_type = 'Bank'
+ AND gle.voucher_type = %(doctype)s
+ AND gle.voucher_no = %(docname)s
+ AND is_cancelled = 0
+ """,
+ dict(doctype=doctype, docname=docname),
as_dict=True,
)
+ return result
-def get_paid_amount(payment_entry, currency, bank_account):
+def get_total_allocated_amount(doctype, docname):
+ """
+ Gets the sum of allocations for a voucher on each bank GL account
+ along with the latest bank transaction name & date
+ NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
+ """
+ # nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
+ result = frappe.db.sql(
+ """
+ SELECT total, latest_name, latest_date, gl_account FROM (
+ SELECT
+ ROW_NUMBER() OVER w AS rownum,
+ SUM(btp.allocated_amount) OVER(PARTITION BY ba.account) AS total,
+ FIRST_VALUE(bt.name) OVER w AS latest_name,
+ FIRST_VALUE(bt.date) OVER w AS latest_date,
+ ba.account AS gl_account
+ FROM
+ `tabBank Transaction Payments` btp
+ LEFT JOIN `tabBank Transaction` bt ON bt.name=btp.parent
+ LEFT JOIN `tabBank Account` ba ON ba.name=bt.bank_account
+ WHERE
+ btp.payment_document = %(doctype)s
+ AND btp.payment_entry = %(docname)s
+ AND bt.docstatus = 1
+ WINDOW w AS (PARTITION BY ba.account ORDER BY bt.date desc)
+ ) temp
+ WHERE
+ rownum = 1
+ """,
+ dict(doctype=doctype, docname=docname),
+ as_dict=True,
+ )
+ for row in result:
+ # Why is this *sometimes* a byte string?
+ if isinstance(row["latest_name"], bytes):
+ row["latest_name"] = row["latest_name"].decode()
+ row["latest_date"] = frappe.utils.getdate(row["latest_date"])
+ return result
+
+
+def get_paid_amount(payment_entry, currency, gl_bank_account):
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
paid_amount_field = "paid_amount"
@@ -147,7 +283,7 @@
elif payment_entry.payment_document == "Journal Entry":
return frappe.db.get_value(
"Journal Entry Account",
- {"parent": payment_entry.payment_entry, "account": bank_account},
+ {"parent": payment_entry.payment_entry, "account": gl_bank_account},
"sum(credit_in_account_currency)",
)
@@ -166,6 +302,12 @@
payment_entry.payment_document, payment_entry.payment_entry, "amount_paid"
)
+ elif payment_entry.payment_document == "Bank Transaction":
+ dep, wth = frappe.db.get_value(
+ "Bank Transaction", payment_entry.payment_entry, ("deposit", "withdrawal")
+ )
+ return abs(flt(wth) - flt(dep))
+
else:
frappe.throw(
"Please reconcile {0}: {1} manually".format(
@@ -174,18 +316,55 @@
)
-@frappe.whitelist()
-def unclear_reference_payment(doctype, docname):
- if frappe.db.exists(doctype, docname):
- doc = frappe.get_doc(doctype, docname)
- if doctype == "Sales Invoice":
- frappe.db.set_value(
- "Sales Invoice Payment",
- dict(parenttype=doc.payment_document, parent=doc.payment_entry),
- "clearance_date",
- None,
- )
- else:
- frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None)
+def set_voucher_clearance(doctype, docname, clearance_date, self):
+ if doctype in [
+ "Payment Entry",
+ "Journal Entry",
+ "Purchase Invoice",
+ "Expense Claim",
+ "Loan Repayment",
+ "Loan Disbursement",
+ ]:
+ if (
+ doctype == "Payment Entry"
+ and frappe.db.get_value("Payment Entry", docname, "payment_type") == "Internal Transfer"
+ and len(get_reconciled_bank_transactions(doctype, docname)) < 2
+ ):
+ return
+ frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
- return doc.payment_entry
+ elif doctype == "Sales Invoice":
+ frappe.db.set_value(
+ "Sales Invoice Payment",
+ dict(parenttype=doctype, parent=docname),
+ "clearance_date",
+ clearance_date,
+ )
+
+ elif doctype == "Bank Transaction":
+ # For when a second bank transaction has fixed another, e.g. refund
+ bt = frappe.get_doc(doctype, docname)
+ if clearance_date:
+ vouchers = [{"payment_doctype": "Bank Transaction", "payment_name": self.name}]
+ bt.add_payment_entries(vouchers)
+ else:
+ for pe in bt.payment_entries:
+ if pe.payment_document == self.doctype and pe.payment_entry == self.name:
+ bt.remove(pe)
+ bt.save()
+ break
+
+
+def get_reconciled_bank_transactions(doctype, docname):
+ return frappe.get_all(
+ "Bank Transaction Payments",
+ filters={"payment_document": doctype, "payment_entry": docname},
+ pluck="parent",
+ )
+
+
+@frappe.whitelist()
+def unclear_reference_payment(doctype, docname, bt_name):
+ bt = frappe.get_doc("Bank Transaction", bt_name)
+ set_voucher_clearance(doctype, docname, None, bt)
+ return docname
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
index 220b747..cb7da17 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
@@ -36,7 +36,7 @@
no_of_columns = max([len(d) for d in data])
- if no_of_columns > 7:
+ if no_of_columns > 8:
frappe.throw(
_("More columns found than expected. Please compare the uploaded file with standard template"),
title=(_("Wrong Template")),
@@ -233,6 +233,7 @@
is_group,
account_type,
root_type,
+ account_currency,
) = i
if not account_name:
@@ -253,6 +254,8 @@
charts_map[account_name]["account_type"] = account_type
if root_type:
charts_map[account_name]["root_type"] = root_type
+ if account_currency:
+ charts_map[account_name]["account_currency"] = account_currency
path = return_parent(data, account_name)[::-1]
paths.append(path) # List of path is created
line_no += 1
@@ -315,6 +318,7 @@
"Is Group",
"Account Type",
"Root Type",
+ "Account Currency",
]
writer = UnicodeWriter()
writer.writerow(fields)
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
index d67d59b..a4f6a74 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
@@ -211,8 +211,7 @@
# Handle Accounts with '0' balance in Account/Base Currency
for d in [x for x in account_details if x.zero_balance]:
- # TODO: Set new balance in Base/Account currency
- if d.balance > 0:
+ if d.balance != 0:
current_exchange_rate = new_exchange_rate = 0
new_balance_in_account_currency = 0 # this will be '0'
@@ -399,6 +398,9 @@
journal_entry_accounts = []
for d in accounts:
+ if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")):
+ continue
+
dr_or_cr = (
"debit_in_account_currency"
if d.get("balance_in_account_currency") > 0
@@ -448,7 +450,13 @@
}
)
- journal_entry_accounts.append(
+ journal_entry.set("accounts", journal_entry_accounts)
+ journal_entry.set_amounts_in_company_currency()
+ journal_entry.set_total_debit_credit()
+
+ self.gain_loss_unbooked += journal_entry.difference - self.gain_loss_unbooked
+ journal_entry.append(
+ "accounts",
{
"account": unrealized_exchange_gain_loss_account,
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
@@ -460,10 +468,9 @@
"exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
- }
+ },
)
- journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_amounts_in_company_currency()
journal_entry.set_total_debit_credit()
journal_entry.save()
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 21f27ae..089f20b 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -8,7 +8,7 @@
frappe.ui.form.on("Journal Entry", {
setup: function(frm) {
frm.add_fetch("bank_account", "account", "account");
- frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry'];
+ frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger"];
},
refresh: function(frm) {
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json
index 498fc7c..80e7222 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.json
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json
@@ -137,7 +137,8 @@
"fieldname": "finance_book",
"fieldtype": "Link",
"label": "Finance Book",
- "options": "Finance Book"
+ "options": "Finance Book",
+ "read_only": 1
},
{
"fieldname": "2_add_edit_gl_entries",
@@ -538,7 +539,7 @@
"idx": 176,
"is_submittable": 1,
"links": [],
- "modified": "2023-01-17 12:53:53.280620",
+ "modified": "2023-03-01 14:58:59.286591",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 5b0322a..db399b7 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -89,7 +89,13 @@
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
unlink_ref_doc_from_payment_entries(self)
- self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
+ self.ignore_linked_doctypes = (
+ "GL Entry",
+ "Stock Ledger Entry",
+ "Payment Ledger Entry",
+ "Repost Payment Ledger",
+ "Repost Payment Ledger Items",
+ )
self.make_gl_entries(1)
self.update_advance_paid()
self.unlink_advance_entry_reference()
@@ -238,21 +244,16 @@
):
processed_assets.append(d.reference_name)
- asset = frappe.db.get_value(
- "Asset", d.reference_name, ["calculate_depreciation", "value_after_depreciation"], as_dict=1
- )
+ asset = frappe.get_doc("Asset", d.reference_name)
if asset.calculate_depreciation:
continue
depr_value = d.debit or d.credit
- frappe.db.set_value(
- "Asset",
- d.reference_name,
- "value_after_depreciation",
- asset.value_after_depreciation - depr_value,
- )
+ asset.db_set("value_after_depreciation", asset.value_after_depreciation - depr_value)
+
+ asset.set_status()
def update_inter_company_jv(self):
if (
@@ -348,12 +349,9 @@
else:
depr_value = d.debit or d.credit
- frappe.db.set_value(
- "Asset",
- d.reference_name,
- "value_after_depreciation",
- asset.value_after_depreciation + depr_value,
- )
+ asset.db_set("value_after_depreciation", asset.value_after_depreciation + depr_value)
+
+ asset.set_status()
def unlink_inter_company_jv(self):
if (
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 6039bdf..91374ae 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -7,7 +7,7 @@
frappe.ui.form.on('Payment Entry', {
onload: function(frm) {
- frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
+ frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Repost Payment Ledger"];
if(frm.doc.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index 4a7a57b..3927eca 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -239,7 +239,7 @@
"depends_on": "paid_from",
"fieldname": "paid_from_account_currency",
"fieldtype": "Link",
- "label": "Account Currency",
+ "label": "Account Currency (From)",
"options": "Currency",
"print_hide": 1,
"read_only": 1,
@@ -249,7 +249,7 @@
"depends_on": "paid_from",
"fieldname": "paid_from_account_balance",
"fieldtype": "Currency",
- "label": "Account Balance",
+ "label": "Account Balance (From)",
"options": "paid_from_account_currency",
"print_hide": 1,
"read_only": 1
@@ -272,7 +272,7 @@
"depends_on": "paid_to",
"fieldname": "paid_to_account_currency",
"fieldtype": "Link",
- "label": "Account Currency",
+ "label": "Account Currency (To)",
"options": "Currency",
"print_hide": 1,
"read_only": 1,
@@ -282,7 +282,7 @@
"depends_on": "paid_to",
"fieldname": "paid_to_account_balance",
"fieldtype": "Currency",
- "label": "Account Balance",
+ "label": "Account Balance (To)",
"options": "paid_to_account_currency",
"print_hide": 1,
"read_only": 1
@@ -304,7 +304,7 @@
{
"fieldname": "source_exchange_rate",
"fieldtype": "Float",
- "label": "Exchange Rate",
+ "label": "Source Exchange Rate",
"precision": "9",
"print_hide": 1,
"reqd": 1
@@ -334,7 +334,7 @@
{
"fieldname": "target_exchange_rate",
"fieldtype": "Float",
- "label": "Exchange Rate",
+ "label": "Target Exchange Rate",
"precision": "9",
"print_hide": 1,
"reqd": 1
@@ -633,14 +633,14 @@
"depends_on": "eval:doc.party_type == 'Supplier'",
"fieldname": "purchase_taxes_and_charges_template",
"fieldtype": "Link",
- "label": "Taxes and Charges Template",
+ "label": "Purchase Taxes and Charges Template",
"options": "Purchase Taxes and Charges Template"
},
{
"depends_on": "eval: doc.party_type == 'Customer'",
"fieldname": "sales_taxes_and_charges_template",
"fieldtype": "Link",
- "label": "Taxes and Charges Template",
+ "label": "Sales Taxes and Charges Template",
"options": "Sales Taxes and Charges Template"
},
{
@@ -733,7 +733,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-12-08 16:25:43.824051",
+ "modified": "2023-02-14 04:52:30.478523",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 1cccbd9..cd5b6d5 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -92,7 +92,13 @@
self.set_status()
def on_cancel(self):
- self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
+ self.ignore_linked_doctypes = (
+ "GL Entry",
+ "Stock Ledger Entry",
+ "Payment Ledger Entry",
+ "Repost Payment Ledger",
+ "Repost Payment Ledger Items",
+ )
self.make_gl_entries(cancel=1)
self.update_outstanding_amounts()
self.update_advance_paid()
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 675a328..e3d9c26 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -368,6 +368,7 @@
"exchange_rate": 1,
"cost_center": erpnext.get_default_cost_center(self.company),
reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount),
+ reverse_dr_or_cr: flt(row.difference_amount),
}
)
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index fc837c7..7005c17 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -45,21 +45,20 @@
frappe.throw(_("To create a Payment Request reference document is required"))
def validate_payment_request_amount(self):
- existing_payment_request_amount = get_existing_payment_request_amount(
- self.reference_doctype, self.reference_name
+ existing_payment_request_amount = flt(
+ get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
)
- if existing_payment_request_amount:
- ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
- if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart":
- ref_amount = get_amount(ref_doc, self.payment_account)
+ ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
+ if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart":
+ ref_amount = get_amount(ref_doc, self.payment_account)
- if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
- frappe.throw(
- _("Total Payment Request amount cannot be greater than {0} amount").format(
- self.reference_doctype
- )
+ if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
+ frappe.throw(
+ _("Total Payment Request amount cannot be greater than {0} amount").format(
+ self.reference_doctype
)
+ )
def validate_currency(self):
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
@@ -496,26 +495,22 @@
"""get amount based on doctype"""
dt = ref_doc.doctype
if dt in ["Sales Order", "Purchase Order"]:
- grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid)
-
+ grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)
elif dt in ["Sales Invoice", "Purchase Invoice"]:
if ref_doc.party_account_currency == ref_doc.currency:
grand_total = flt(ref_doc.outstanding_amount)
else:
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
-
elif dt == "POS Invoice":
for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account:
grand_total = pay.amount
break
-
elif dt == "Fees":
grand_total = ref_doc.outstanding_amount
if grand_total > 0:
return grand_total
-
else:
frappe.throw(_("Payment Entry is already created"))
diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py
index 477c726..4279aa4 100644
--- a/erpnext/accounts/doctype/payment_request/test_payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py
@@ -45,7 +45,10 @@
frappe.get_doc(method).insert(ignore_permissions=True)
def test_payment_request_linkings(self):
- so_inr = make_sales_order(currency="INR")
+ so_inr = make_sales_order(currency="INR", do_not_save=True)
+ so_inr.disable_rounded_total = 1
+ so_inr.save()
+
pr = make_payment_request(
dt="Sales Order",
dn=so_inr.name,
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
index 655c4ec..115b415 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
@@ -21,8 +21,24 @@
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
+ self.validate_duplicate_pos_invoices()
self.validate_pos_invoices()
+ def validate_duplicate_pos_invoices(self):
+ pos_occurences = {}
+ for idx, inv in enumerate(self.pos_transactions, 1):
+ pos_occurences.setdefault(inv.pos_invoice, []).append(idx)
+
+ error_list = []
+ for key, value in pos_occurences.items():
+ if len(value) > 1:
+ error_list.append(
+ _("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value)))
+ )
+
+ if error_list:
+ frappe.throw(error_list, title=_("Duplicate POS Invoices found"), as_list=True)
+
def validate_pos_invoices(self):
invalid_rows = []
for d in self.pos_transactions:
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index a1239d6..b40649b 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -161,7 +161,7 @@
bold_item_name = frappe.bold(item.item_name)
bold_extra_batch_qty_needed = frappe.bold(
- abs(available_batch_qty - reserved_batch_qty - item.qty)
+ abs(available_batch_qty - reserved_batch_qty - item.stock_qty)
)
bold_invalid_batch_no = frappe.bold(item.batch_no)
@@ -172,7 +172,7 @@
).format(item.idx, bold_invalid_batch_no, bold_item_name),
title=_("Item Unavailable"),
)
- elif (available_batch_qty - reserved_batch_qty - item.qty) < 0:
+ elif (available_batch_qty - reserved_batch_qty - item.stock_qty) < 0:
frappe.throw(
_(
"Row #{}: Batch No. {} of item {} has less than required stock available, {} more required"
@@ -246,7 +246,7 @@
),
title=_("Item Unavailable"),
)
- elif is_stock_item and flt(available_stock) < flt(d.qty):
+ elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
frappe.throw(
_(
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
@@ -651,7 +651,7 @@
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
available_qty = item_bin_qty - item_pos_reserved_qty
- max_available_bundles = available_qty / item.qty
+ max_available_bundles = available_qty / item.stock_qty
if bundle_bin_qty > max_available_bundles and frappe.get_value(
"Item", item.item_code, "is_stock_item"
):
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index 3a237a4..b1e2208 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -17,6 +17,22 @@
def validate(self):
self.validate_customer()
self.validate_pos_invoice_status()
+ self.validate_duplicate_pos_invoices()
+
+ def validate_duplicate_pos_invoices(self):
+ pos_occurences = {}
+ for idx, inv in enumerate(self.pos_invoices, 1):
+ pos_occurences.setdefault(inv.pos_invoice, []).append(idx)
+
+ error_list = []
+ for key, value in pos_occurences.items():
+ if len(value) > 1:
+ error_list.append(
+ _("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value)))
+ )
+
+ if error_list:
+ frappe.throw(error_list, title=_("Duplicate POS Invoices found"), as_list=True)
def validate_customer(self):
if self.merge_invoices_based_on == "Customer Group":
@@ -425,6 +441,8 @@
if closing_entry:
closing_entry.set_status(update=True, status="Failed")
+ if type(error_message) == list:
+ error_message = frappe.json.dumps(error_message)
closing_entry.db_set("error_message", error_message)
raise
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json
index ce9ce64..a63039e 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json
@@ -472,7 +472,7 @@
"description": "If rate is zero them item will be treated as \"Free Item\"",
"fieldname": "free_item_rate",
"fieldtype": "Currency",
- "label": "Rate"
+ "label": "Free Item Rate"
},
{
"collapsible": 1,
@@ -608,7 +608,7 @@
"icon": "fa fa-gift",
"idx": 1,
"links": [],
- "modified": "2022-10-13 19:05:35.056304",
+ "modified": "2023-02-14 04:53:34.887358",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index a098e8d..e2b4a1a 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -31,7 +31,7 @@
super.onload();
// Ignore linked advances
- this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice'];
+ this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger"];
if(!this.frm.doc.__islocal) {
// show credit_to in print format
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 0e9f976..b79af71 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -5,6 +5,7 @@
import frappe
from frappe import _, throw
from frappe.model.mapper import get_mapped_doc
+from frappe.query_builder.functions import Sum
from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
import erpnext
@@ -1416,6 +1417,8 @@
"GL Entry",
"Stock Ledger Entry",
"Repost Item Valuation",
+ "Repost Payment Ledger",
+ "Repost Payment Ledger Items",
"Payment Ledger Entry",
"Tax Withheld Vouchers",
)
@@ -1463,19 +1466,16 @@
def update_billing_status_in_pr(self, update_modified=True):
updated_pr = []
po_details = []
+
+ pr_details_billed_amt = self.get_pr_details_billed_amt()
+
for d in self.get("items"):
if d.pr_detail:
- billed_amt = frappe.db.sql(
- """select sum(amount) from `tabPurchase Invoice Item`
- where pr_detail=%s and docstatus=1""",
- d.pr_detail,
- )
- billed_amt = billed_amt and billed_amt[0][0] or 0
frappe.db.set_value(
"Purchase Receipt Item",
d.pr_detail,
"billed_amt",
- billed_amt,
+ flt(pr_details_billed_amt.get(d.pr_detail)),
update_modified=update_modified,
)
updated_pr.append(d.purchase_receipt)
@@ -1485,11 +1485,35 @@
if po_details:
updated_pr += update_billed_amount_based_on_po(po_details, update_modified)
+ adjust_incoming_rate = frappe.db.get_single_value(
+ "Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
+ )
+
for pr in set(updated_pr):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage
pr_doc = frappe.get_doc("Purchase Receipt", pr)
- update_billing_percentage(pr_doc, update_modified=update_modified)
+ update_billing_percentage(
+ pr_doc, update_modified=update_modified, adjust_incoming_rate=adjust_incoming_rate
+ )
+
+ def get_pr_details_billed_amt(self):
+ # Get billed amount based on purchase receipt item reference (pr_detail) in purchase invoice
+
+ pr_details_billed_amt = {}
+ pr_details = [d.get("pr_detail") for d in self.get("items") if d.get("pr_detail")]
+ if pr_details:
+ doctype = frappe.qb.DocType("Purchase Invoice Item")
+ query = (
+ frappe.qb.from_(doctype)
+ .select(doctype.pr_detail, Sum(doctype.amount))
+ .where(doctype.pr_detail.isin(pr_details) & doctype.docstatus == 1)
+ .groupby(doctype.pr_detail)
+ )
+
+ pr_details_billed_amt = frappe._dict(query.run(as_list=1))
+
+ return pr_details_billed_amt
def on_recurring(self, reference_doc, auto_repeat_doc):
self.due_date = None
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index f901257..a6d7df6 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -1523,6 +1523,94 @@
company.enable_provisional_accounting_for_non_stock_items = 0
company.save()
+ def test_adjust_incoming_rate(self):
+ frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
+
+ frappe.db.set_single_value(
+ "Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1
+ )
+
+ # Increase the cost of the item
+
+ pr = make_purchase_receipt(qty=1, rate=100)
+
+ stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
+ "stock_value_difference",
+ )
+ self.assertEqual(stock_value_difference, 100)
+
+ pi = create_purchase_invoice_from_receipt(pr.name)
+ for row in pi.items:
+ row.rate = 150
+
+ pi.save()
+ pi.submit()
+
+ stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
+ "stock_value_difference",
+ )
+ self.assertEqual(stock_value_difference, 150)
+
+ # Reduce the cost of the item
+
+ pr = make_purchase_receipt(qty=1, rate=100)
+
+ stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
+ "stock_value_difference",
+ )
+ self.assertEqual(stock_value_difference, 100)
+
+ pi = create_purchase_invoice_from_receipt(pr.name)
+ for row in pi.items:
+ row.rate = 50
+
+ pi.save()
+ pi.submit()
+
+ stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
+ "stock_value_difference",
+ )
+ self.assertEqual(stock_value_difference, 50)
+
+ frappe.db.set_single_value(
+ "Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0
+ )
+
+ # Don't adjust incoming rate
+
+ pr = make_purchase_receipt(qty=1, rate=100)
+
+ stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
+ "stock_value_difference",
+ )
+ self.assertEqual(stock_value_difference, 100)
+
+ pi = create_purchase_invoice_from_receipt(pr.name)
+ for row in pi.items:
+ row.rate = 50
+
+ pi.save()
+ pi.submit()
+
+ stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
+ "stock_value_difference",
+ )
+ self.assertEqual(stock_value_difference, 100)
+
+ frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
+
def test_item_less_defaults(self):
pi = frappe.new_doc("Purchase Invoice")
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 7abf3f3..47e3f9b 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -34,7 +34,7 @@
super.onload();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
- 'POS Closing Entry', 'Journal Entry', 'Payment Entry'];
+ 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger"];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 31cf120..5cda276 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -397,6 +397,8 @@
"GL Entry",
"Stock Ledger Entry",
"Repost Item Valuation",
+ "Repost Payment Ledger",
+ "Repost Payment Ledger Items",
"Payment Ledger Entry",
)
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index 2c829b2..f0146ea 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -278,7 +278,7 @@
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
if cint(tax_details.round_off_tax_amount):
- tax_amount = round(tax_amount)
+ tax_amount = normal_round(tax_amount)
return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount
@@ -603,3 +603,20 @@
valid = True
return valid
+
+
+def normal_round(number):
+ """
+ Rounds a number to the nearest integer.
+ :param number: The number to round.
+ """
+ decimal_part = number - int(number)
+
+ if decimal_part >= 0.5:
+ decimal_part = 1
+ else:
+ decimal_part = 0
+
+ number = int(number) + decimal_part
+
+ return number
diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
index 43b95dc..5827697 100644
--- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
+++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
@@ -136,6 +136,34 @@
group by a.asset_category
union
SELECT a.asset_category,
+ ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
+ gle.debit
+ else
+ 0
+ end), 0) as accumulated_depreciation_as_on_from_date,
+ ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
+ and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
+ gle.debit
+ else
+ 0
+ end), 0) as depreciation_eliminated_during_the_period,
+ ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
+ and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
+ gle.debit
+ else
+ 0
+ end), 0) as depreciation_amount_during_the_period
+ from `tabGL Entry` gle
+ join `tabAsset` a on
+ gle.against_voucher = a.name
+ join `tabAsset Category Account` aca on
+ aca.parent = a.asset_category and aca.company_name = %(company)s
+ join `tabCompany` company on
+ company.name = %(company)s
+ where a.docstatus=1 and a.company=%(company)s and a.calculate_depreciation=0 and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
+ group by a.asset_category
+ union
+ SELECT a.asset_category,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
0
else
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.html b/erpnext/accounts/report/general_ledger/general_ledger.html
index 475be92..2d5ca49 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.html
+++ b/erpnext/accounts/report/general_ledger/general_ledger.html
@@ -38,8 +38,11 @@
{% if(data[i].posting_date) { %}
<td>{%= frappe.datetime.str_to_user(data[i].posting_date) %}</td>
<td>{%= data[i].voucher_type %}
- <br>{%= data[i].voucher_no %}</td>
- <td>
+ <br>{%= data[i].voucher_no %}
+ </td>
+ {% var longest_word = cstr(data[i].remarks).split(" ").reduce((longest, word) => word.length > longest.length ? word : longest, ""); %}
+ <td {% if longest_word.length > 45 %} class="overflow-wrap-anywhere" {% endif %}>
+ <span>
{% if(!(filters.party || filters.account)) { %}
{%= data[i].party || data[i].account %}
<br>
@@ -49,11 +52,14 @@
{% if(data[i].bill_no) { %}
<br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
{% } %}
- </td>
- <td style="text-align: right">
- {%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %}</td>
- <td style="text-align: right">
- {%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %}</td>
+ </span>
+ </td>
+ <td style="text-align: right">
+ {%= format_currency(data[i].debit, filters.presentation_currency) %}
+ </td>
+ <td style="text-align: right">
+ {%= format_currency(data[i].credit, filters.presentation_currency) %}
+ </td>
{% } else { %}
<td></td>
<td></td>
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index e23265b..fde4de8 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -395,6 +395,7 @@
class GrossProfitGenerator(object):
def __init__(self, filters=None):
+ self.sle = {}
self.data = []
self.average_buying_rate = {}
self.filters = frappe._dict(filters)
@@ -404,7 +405,6 @@
if filters.group_by == "Invoice":
self.group_items_by_invoice()
- self.load_stock_ledger_entries()
self.load_product_bundle()
self.load_non_stock_items()
self.get_returned_invoice_items()
@@ -633,7 +633,7 @@
return flt(row.qty) * item_rate
else:
- my_sle = self.sle.get((item_code, row.warehouse))
+ my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
if (row.update_stock or row.dn_detail) and my_sle:
parenttype, parent = row.parenttype, row.parent
if row.dn_detail:
@@ -651,7 +651,7 @@
dn["item_row"],
dn["warehouse"],
)
- my_sle = self.sle.get((item_code, warehouse))
+ my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, item_row, item_code
)
@@ -667,15 +667,12 @@
def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code):
from frappe.query_builder.functions import Sum
- delivery_note = frappe.qb.DocType("Delivery Note")
delivery_note_item = frappe.qb.DocType("Delivery Note Item")
query = (
- frappe.qb.from_(delivery_note)
- .inner_join(delivery_note_item)
- .on(delivery_note.name == delivery_note_item.parent)
+ frappe.qb.from_(delivery_note_item)
.select(Sum(delivery_note_item.incoming_rate * delivery_note_item.stock_qty))
- .where(delivery_note.docstatus == 1)
+ .where(delivery_note_item.docstatus == 1)
.where(delivery_note_item.item_code == item_code)
.where(delivery_note_item.against_sales_order == sales_order)
.where(delivery_note_item.so_detail == so_detail)
@@ -947,24 +944,36 @@
"Item", item_code, ["item_name", "description", "item_group", "brand"]
)
- def load_stock_ledger_entries(self):
- res = frappe.db.sql(
- """select item_code, voucher_type, voucher_no,
- voucher_detail_no, stock_value, warehouse, actual_qty as qty
- from `tabStock Ledger Entry`
- where company=%(company)s and is_cancelled = 0
- order by
- item_code desc, warehouse desc, posting_date desc,
- posting_time desc, creation desc""",
- self.filters,
- as_dict=True,
- )
- self.sle = {}
- for r in res:
- if (r.item_code, r.warehouse) not in self.sle:
- self.sle[(r.item_code, r.warehouse)] = []
+ def get_stock_ledger_entries(self, item_code, warehouse):
+ if item_code and warehouse:
+ if (item_code, warehouse) not in self.sle:
+ sle = qb.DocType("Stock Ledger Entry")
+ res = (
+ qb.from_(sle)
+ .select(
+ sle.item_code,
+ sle.voucher_type,
+ sle.voucher_no,
+ sle.voucher_detail_no,
+ sle.stock_value,
+ sle.warehouse,
+ sle.actual_qty.as_("qty"),
+ )
+ .where(
+ (sle.company == self.filters.company)
+ & (sle.item_code == item_code)
+ & (sle.warehouse == warehouse)
+ & (sle.is_cancelled == 0)
+ )
+ .orderby(sle.item_code)
+ .orderby(sle.warehouse, sle.posting_date, sle.posting_time, sle.creation, order=Order.desc)
+ .run(as_dict=True)
+ )
- self.sle[(r.item_code, r.warehouse)].append(r)
+ self.sle[(item_code, warehouse)] = res
+
+ return self.sle[(item_code, warehouse)]
+ return []
def load_product_bundle(self):
self.product_bundles = {}
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 4ed99f7..4951385 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -209,62 +209,62 @@
return
}
- var x_intervals = [frm.doc.purchase_date];
+ var x_intervals = [frappe.format(frm.doc.purchase_date, { fieldtype: 'Date' })];
var asset_values = [frm.doc.gross_purchase_amount];
- var last_depreciation_date = frm.doc.purchase_date;
- if(frm.doc.opening_accumulated_depreciation) {
- last_depreciation_date = frappe.datetime.add_months(frm.doc.next_depreciation_date,
- -1*frm.doc.frequency_of_depreciation);
-
- x_intervals.push(last_depreciation_date);
- asset_values.push(flt(frm.doc.gross_purchase_amount) -
- flt(frm.doc.opening_accumulated_depreciation));
- }
if(frm.doc.calculate_depreciation) {
- if (frm.doc.finance_books.length == 1) {
- let depr_schedule = (await frappe.call(
- "erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_depr_schedule",
- {
- asset_name: frm.doc.name,
- status: frm.doc.docstatus ? "Active" : "Draft",
- finance_book: frm.doc.finance_books[0].finance_book || null
- }
- )).message;
-
- $.each(depr_schedule || [], function(i, v) {
- x_intervals.push(v.schedule_date);
- var asset_value = flt(frm.doc.gross_purchase_amount) - flt(v.accumulated_depreciation_amount);
- if(v.journal_entry) {
- last_depreciation_date = v.schedule_date;
- asset_values.push(asset_value);
- } else {
- if (in_list(["Scrapped", "Sold"], frm.doc.status)) {
- asset_values.push(null);
- } else {
- asset_values.push(asset_value)
- }
- }
- });
+ if(frm.doc.opening_accumulated_depreciation) {
+ var depreciation_date = frappe.datetime.add_months(
+ frm.doc.finance_books[0].depreciation_start_date,
+ -1 * frm.doc.finance_books[0].frequency_of_depreciation
+ );
+ x_intervals.push(frappe.format(depreciation_date, { fieldtype: 'Date' }));
+ asset_values.push(flt(frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, precision('gross_purchase_amount')));
}
+
+ let depr_schedule = (await frappe.call(
+ "erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_depr_schedule",
+ {
+ asset_name: frm.doc.name,
+ status: frm.doc.docstatus ? "Active" : "Draft",
+ finance_book: frm.doc.finance_books[0].finance_book || null
+ }
+ )).message;
+
+ $.each(depr_schedule || [], function(i, v) {
+ x_intervals.push(frappe.format(v.schedule_date, { fieldtype: 'Date' }));
+ var asset_value = flt(frm.doc.gross_purchase_amount - v.accumulated_depreciation_amount, precision('gross_purchase_amount'));
+ if(v.journal_entry) {
+ asset_values.push(asset_value);
+ } else {
+ if (in_list(["Scrapped", "Sold"], frm.doc.status)) {
+ asset_values.push(null);
+ } else {
+ asset_values.push(asset_value)
+ }
+ }
+ });
} else {
+ if(frm.doc.opening_accumulated_depreciation) {
+ x_intervals.push(frappe.format(frm.doc.creation.split(" ")[0], { fieldtype: 'Date' }));
+ asset_values.push(flt(frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, precision('gross_purchase_amount')));
+ }
+
let depr_entries = (await frappe.call({
method: "get_manual_depreciation_entries",
doc: frm.doc,
})).message;
$.each(depr_entries || [], function(i, v) {
- x_intervals.push(v.posting_date);
- last_depreciation_date = v.posting_date;
+ x_intervals.push(frappe.format(v.posting_date, { fieldtype: 'Date' }));
let last_asset_value = asset_values[asset_values.length - 1]
- asset_values.push(last_asset_value - v.value);
+ asset_values.push(flt(last_asset_value - v.value, precision('gross_purchase_amount')));
});
}
if(in_list(["Scrapped", "Sold"], frm.doc.status)) {
- x_intervals.push(frm.doc.disposal_date);
+ x_intervals.push(frappe.format(frm.doc.disposal_date, { fieldtype: 'Date' }));
asset_values.push(0);
- last_depreciation_date = frm.doc.disposal_date;
}
frm.dashboard.render_graph({
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 4f1caca..e1d58a0 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -413,11 +413,14 @@
if self.journal_entry_for_scrap:
status = "Scrapped"
- elif self.finance_books:
- idx = self.get_default_finance_book_idx() or 0
+ else:
+ expected_value_after_useful_life = 0
+ value_after_depreciation = self.value_after_depreciation
- expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life
- value_after_depreciation = self.finance_books[idx].value_after_depreciation
+ if self.calculate_depreciation:
+ idx = self.get_default_finance_book_idx() or 0
+ expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life
+ value_after_depreciation = self.finance_books[idx].value_after_depreciation
if flt(value_after_depreciation) <= expected_value_after_useful_life:
status = "Fully Depreciated"
@@ -429,25 +432,16 @@
def get_value_after_depreciation(self, finance_book=None):
if not self.calculate_depreciation:
- return self.value_after_depreciation
+ return flt(self.value_after_depreciation, self.precision("gross_purchase_amount"))
if not finance_book:
- return self.get("finance_books")[0].value_after_depreciation
+ return flt(
+ self.get("finance_books")[0].value_after_depreciation, self.precision("gross_purchase_amount")
+ )
for row in self.get("finance_books"):
if finance_book == row.finance_book:
- return row.value_after_depreciation
-
- def _get_value_after_depreciation_for_making_schedule(self, fb_row):
- # value_after_depreciation - current Asset value
- if self.docstatus == 1 and fb_row.value_after_depreciation:
- value_after_depreciation = flt(fb_row.value_after_depreciation)
- else:
- value_after_depreciation = flt(self.gross_purchase_amount) - flt(
- self.opening_accumulated_depreciation
- )
-
- return value_after_depreciation
+ return flt(row.value_after_depreciation, self.precision("gross_purchase_amount"))
def get_default_finance_book_idx(self):
if not self.get("default_finance_book") and self.company:
@@ -472,6 +466,7 @@
.where(gle.debit != 0)
.where(gle.is_cancelled == 0)
.orderby(gle.posting_date)
+ .orderby(gle.creation)
).run(as_dict=True)
return records
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index e7a2532..fb6e174 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -168,7 +168,7 @@
row.value_after_depreciation -= d.depreciation_amount
row.db_update()
- frappe.db.set_value("Asset", asset_name, "depr_entry_posting_status", "Successful")
+ asset.db_set("depr_entry_posting_status", "Successful")
asset.set_status()
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js
index c28b2b3..3d2dff1 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js
@@ -43,9 +43,9 @@
if(frm.doc.depreciation_method != "Manual") return;
var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation);
- $.each(frm.doc.schedules || [], function(i, row) {
+
+ $.each(frm.doc.depreciation_schedule || [], function(i, row) {
accumulated_depreciation += flt(row.depreciation_amount);
- frappe.model.set_value(row.doctype, row.name,
- "accumulated_depreciation_amount", accumulated_depreciation);
+ frappe.model.set_value(row.doctype, row.name, "accumulated_depreciation_amount", accumulated_depreciation);
})
};
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
index 898c482..d38508d 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
@@ -10,7 +10,9 @@
"asset",
"naming_series",
"column_break_2",
+ "gross_purchase_amount",
"opening_accumulated_depreciation",
+ "number_of_depreciations_booked",
"finance_book",
"finance_book_id",
"depreciation_details_section",
@@ -148,18 +150,36 @@
"read_only": 1
},
{
- "depends_on": "opening_accumulated_depreciation",
"fieldname": "opening_accumulated_depreciation",
"fieldtype": "Currency",
+ "hidden": 1,
"label": "Opening Accumulated Depreciation",
"options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "gross_purchase_amount",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Gross Purchase Amount",
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "number_of_depreciations_booked",
+ "fieldtype": "Int",
+ "hidden": 1,
+ "label": "Number of Depreciations Booked",
+ "print_hide": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-01-16 21:08:21.421260",
+ "modified": "2023-02-26 16:37:23.734806",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Depreciation Schedule",
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
index 7615fbc..b75fbcb 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
@@ -4,7 +4,15 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import add_days, add_months, cint, flt, get_last_day, is_last_day_of_the_month
+from frappe.utils import (
+ add_days,
+ add_months,
+ cint,
+ flt,
+ get_last_day,
+ getdate,
+ is_last_day_of_the_month,
+)
class AssetDepreciationSchedule(Document):
@@ -83,15 +91,58 @@
date_of_return=None,
update_asset_finance_book_row=True,
):
+ have_asset_details_been_modified = self.have_asset_details_been_modified(asset_doc)
+ not_manual_depr_or_have_manual_depr_details_been_modified = (
+ self.not_manual_depr_or_have_manual_depr_details_been_modified(row)
+ )
+
self.set_draft_asset_depr_schedule_details(asset_doc, row)
- self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row)
- self.set_accumulated_depreciation(row, date_of_disposal, date_of_return)
+
+ if self.should_prepare_depreciation_schedule(
+ have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified
+ ):
+ self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row)
+ self.set_accumulated_depreciation(row, date_of_disposal, date_of_return)
+
+ def have_asset_details_been_modified(self, asset_doc):
+ return (
+ asset_doc.gross_purchase_amount != self.gross_purchase_amount
+ or asset_doc.opening_accumulated_depreciation != self.opening_accumulated_depreciation
+ or asset_doc.number_of_depreciations_booked != self.number_of_depreciations_booked
+ )
+
+ def not_manual_depr_or_have_manual_depr_details_been_modified(self, row):
+ return (
+ self.depreciation_method != "Manual"
+ or row.total_number_of_depreciations != self.total_number_of_depreciations
+ or row.frequency_of_depreciation != self.frequency_of_depreciation
+ or getdate(row.depreciation_start_date) != self.get("depreciation_schedule")[0].schedule_date
+ or row.expected_value_after_useful_life != self.expected_value_after_useful_life
+ )
+
+ def should_prepare_depreciation_schedule(
+ self, have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified
+ ):
+ if not self.get("depreciation_schedule"):
+ return True
+
+ old_asset_depr_schedule_doc = self.get_doc_before_save()
+
+ if self.docstatus != 0 and not old_asset_depr_schedule_doc:
+ return True
+
+ if have_asset_details_been_modified or not_manual_depr_or_have_manual_depr_details_been_modified:
+ return True
+
+ return False
def set_draft_asset_depr_schedule_details(self, asset_doc, row):
self.asset = asset_doc.name
self.finance_book = row.finance_book
self.finance_book_id = row.idx
self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation
+ self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked
+ self.gross_purchase_amount = asset_doc.gross_purchase_amount
self.depreciation_method = row.depreciation_method
self.total_number_of_depreciations = row.total_number_of_depreciations
self.frequency_of_depreciation = row.frequency_of_depreciation
@@ -102,7 +153,7 @@
def make_depr_schedule(
self, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True
):
- if row.depreciation_method != "Manual" and not self.get("depreciation_schedule"):
+ if not self.get("depreciation_schedule"):
self.depreciation_schedule = []
if not asset_doc.available_for_use_date:
@@ -134,7 +185,7 @@
):
asset_doc.validate_asset_finance_books(row)
- value_after_depreciation = asset_doc._get_value_after_depreciation_for_making_schedule(row)
+ value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row)
row.value_after_depreciation = value_after_depreciation
if update_asset_finance_book_row:
@@ -293,7 +344,9 @@
ignore_booked_entry=False,
):
straight_line_idx = [
- d.idx for d in self.get("depreciation_schedule") if d.depreciation_method == "Straight Line"
+ d.idx
+ for d in self.get("depreciation_schedule")
+ if d.depreciation_method == "Straight Line" or d.depreciation_method == "Manual"
]
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
@@ -325,6 +378,17 @@
)
+def _get_value_after_depreciation_for_making_schedule(asset_doc, fb_row):
+ if asset_doc.docstatus == 1 and fb_row.value_after_depreciation:
+ value_after_depreciation = flt(fb_row.value_after_depreciation)
+ else:
+ value_after_depreciation = flt(asset_doc.gross_purchase_amount) - flt(
+ asset_doc.opening_accumulated_depreciation
+ )
+
+ return value_after_depreciation
+
+
def make_draft_asset_depr_schedules_if_not_present(asset_doc):
for row in asset_doc.get("finance_books"):
draft_asset_depr_schedule_name = get_asset_depr_schedule_name(
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py
index 9a05a74..a7172a7 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.py
@@ -91,6 +91,9 @@
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
self.asset_doc.save()
+ def after_delete(self):
+ frappe.get_doc("Asset", self.asset).set_status()
+
def check_repair_status(self):
if self.repair_status == "Pending":
frappe.throw(_("Please update Repair Status."))
diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
index cead72e..59d43b1 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
-from frappe.utils import cstr, formatdate, getdate
+from frappe.utils import cstr, flt, formatdate, getdate
from erpnext.accounts.report.financial_statements import (
get_fiscal_year_data,
@@ -102,13 +102,9 @@
]
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
- finance_book_filter = ("is", "not set")
- if filters.finance_book:
- finance_book_filter = ("=", filters.finance_book)
-
assets_linked_to_fb = frappe.db.get_all(
doctype="Asset Finance Book",
- filters={"finance_book": finance_book_filter},
+ filters={"finance_book": filters.finance_book or ("is", "not set")},
pluck="parent",
)
@@ -155,6 +151,7 @@
filters.filter_based_on,
"Monthly",
company=filters.company,
+ ignore_fiscal_year=True,
)
for d in period_list:
@@ -194,7 +191,7 @@
else:
depr_amount = get_manual_depreciation_amount_of_asset(asset, filters)
- return depr_amount
+ return flt(depr_amount, 2)
def get_finance_book_value_map(filters):
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index 34417f7..95857e4 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -18,9 +18,11 @@
"pr_required",
"column_break_12",
"maintain_same_rate",
+ "set_landed_cost_based_on_purchase_invoice_rate",
"allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice",
"disable_last_purchase_rate",
+ "show_pay_button",
"subcontract",
"backflush_raw_materials_of_subcontract_based_on",
"column_break_11",
@@ -140,6 +142,20 @@
"fieldname": "disable_last_purchase_rate",
"fieldtype": "Check",
"label": "Disable Last Purchase Rate"
+ },
+ {
+ "default": "1",
+ "fieldname": "show_pay_button",
+ "fieldtype": "Check",
+ "label": "Show Pay Button in Purchase Order Portal"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: !doc.maintain_same_rate",
+ "description": "Users can enable the checkbox If they want to adjust the incoming rate (set using purchase receipt) based on the purchase invoice rate.",
+ "fieldname": "set_landed_cost_based_on_purchase_invoice_rate",
+ "fieldtype": "Check",
+ "label": "Set Landed Cost Based on Purchase Invoice Rate"
}
],
"icon": "fa fa-cog",
@@ -147,7 +163,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-01-09 17:08:28.828173",
+ "modified": "2023-02-28 15:41:32.686805",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.py b/erpnext/buying/doctype/buying_settings/buying_settings.py
index be1ebde..4680a88 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.py
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.py
@@ -21,3 +21,10 @@
self.get("supp_master_name") == "Naming Series",
hide_name_field=False,
)
+
+ def before_save(self):
+ self.check_maintain_same_rate()
+
+ def check_maintain_same_rate(self):
+ if self.maintain_same_rate:
+ self.set_landed_cost_based_on_purchase_invoice_rate = 0
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
index a9f5afb..2f0b786 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
@@ -124,12 +124,11 @@
frappe.urllib.get_full_url(
"/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?" +
new URLSearchParams({
- doctype: frm.doc.doctype,
name: frm.doc.name,
supplier: data.supplier,
print_format: data.print_format || "Standard",
language: data.language || frappe.boot.lang,
- letter_head: data.letter_head || frm.doc.letter_head || "",
+ letterhead: data.letter_head || frm.doc.letter_head || "",
}).toString()
)
);
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index 8e9ded9..7927beb 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -3,6 +3,7 @@
import json
+from typing import Optional
import frappe
from frappe import _
@@ -388,24 +389,26 @@
@frappe.whitelist()
-def get_pdf(doctype, name, supplier, print_format=None, language=None, letter_head=None):
- # permissions get checked in `download_pdf`
- if doc := get_rfq_doc(doctype, name, supplier):
- download_pdf(
- doctype,
- name,
- print_format,
- doc=doc,
- language=language,
- letter_head=letter_head or None,
- )
-
-
-def get_rfq_doc(doctype, name, supplier):
+def get_pdf(
+ name: str,
+ supplier: str,
+ print_format: Optional[str] = None,
+ language: Optional[str] = None,
+ letterhead: Optional[str] = None,
+):
+ doc = frappe.get_doc("Request for Quotation", name)
if supplier:
- doc = frappe.get_doc(doctype, name)
doc.update_supplier_part_no(supplier)
- return doc
+
+ # permissions get checked in `download_pdf`
+ download_pdf(
+ doc.doctype,
+ doc.name,
+ print_format,
+ doc=doc,
+ language=language,
+ letterhead=letterhead or None,
+ )
@frappe.whitelist()
diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
index 064b806..d250e6f 100644
--- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
@@ -8,6 +8,7 @@
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import (
create_supplier_quotation,
+ get_pdf,
make_supplier_quotation_from_rfq,
)
from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq
@@ -124,6 +125,11 @@
rfq.status = "Draft"
rfq.submit()
+ def test_get_pdf(self):
+ rfq = make_request_for_quotation()
+ get_pdf(rfq.name, rfq.get("suppliers")[0].supplier)
+ self.assertEqual(frappe.local.response.type, "pdf")
+
def make_request_for_quotation(**args):
"""
diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json
index 66eafe9..1bf7f58 100644
--- a/erpnext/buying/doctype/supplier/supplier.json
+++ b/erpnext/buying/doctype/supplier/supplier.json
@@ -23,7 +23,6 @@
"default_bank_account",
"column_break_10",
"default_price_list",
- "payment_terms",
"internal_supplier_section",
"is_internal_supplier",
"represents_company",
@@ -53,6 +52,7 @@
"supplier_primary_address",
"primary_address",
"accounting_tab",
+ "payment_terms",
"accounts",
"settings_tab",
"allow_purchase_invoice_creation_without_purchase_order",
@@ -457,11 +457,10 @@
"link_fieldname": "party"
}
],
- "modified": "2022-11-09 18:02:59.075203",
+ "modified": "2023-02-18 11:05:50.592270",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
- "name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js
index 6304a09..9db769d 100644
--- a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js
+++ b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js
@@ -22,14 +22,14 @@
fieldname:"from_date",
label: __("From Date"),
fieldtype: "Date",
- default: frappe.datetime.add_months(frappe.datetime.month_start(), -1),
+ default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
reqd: 1
},
{
fieldname:"to_date",
label: __("To Date"),
fieldtype: "Date",
- default: frappe.datetime.add_days(frappe.datetime.month_start(),-1),
+ default: frappe.datetime.get_today(),
reqd: 1
},
]
diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js
index b6739fe..7e5338f 100644
--- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js
+++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js
@@ -22,14 +22,14 @@
fieldname:"from_date",
label: __("From Date"),
fieldtype: "Date",
- default: frappe.datetime.add_months(frappe.datetime.month_start(), -1),
+ default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
reqd: 1
},
{
fieldname:"to_date",
label: __("To Date"),
fieldtype: "Date",
- default: frappe.datetime.add_days(frappe.datetime.month_start(),-1),
+ default: frappe.datetime.get_today(),
reqd: 1
},
]
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 6fa44c9..3705fcf 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -204,6 +204,12 @@
validate_einvoice_fields(self)
def on_trash(self):
+ # delete references in 'Repost Payment Ledger'
+ rpi = frappe.qb.DocType("Repost Payment Ledger Items")
+ frappe.qb.from_(rpi).delete().where(
+ (rpi.voucher_type == self.doctype) & (rpi.voucher_no == self.name)
+ ).run()
+
# delete sl and gl entries on deletion of transaction
if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"):
ple = frappe.qb.DocType("Payment Ledger Entry")
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 4f7d9ad..e15b612 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -265,7 +265,10 @@
) / qty_in_stock_uom
else:
item.valuation_rate = (
- item.base_net_amount + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
+ item.base_net_amount
+ + item.item_tax_amount
+ + flt(item.landed_cost_voucher_amount)
+ + flt(item.get("rate_difference_with_purchase_invoice"))
) / qty_in_stock_uom
else:
item.valuation_rate = 0.0
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index fc6793a..15c270e 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -131,7 +131,7 @@
)
elif ref.serial_no:
- if not d.serial_no:
+ if d.qty and not d.serial_no:
frappe.throw(_("Row # {0}: Serial No is mandatory").format(d.idx))
else:
serial_nos = get_serial_nos(d.serial_no)
@@ -252,7 +252,6 @@
child.parent = par.name and par.docstatus = 1
and par.is_return = 1 and par.return_against = %s
group by item_code
- for update
""".format(
column, doc.doctype, doc.doctype
),
@@ -401,6 +400,16 @@
if serial_nos:
target_doc.serial_no = "\n".join(serial_nos)
+ if source_doc.get("rejected_serial_no"):
+ returned_serial_nos = get_returned_serial_nos(
+ source_doc, source_parent, serial_no_field="rejected_serial_no"
+ )
+ rejected_serial_nos = list(
+ set(get_serial_nos(source_doc.rejected_serial_no)) - set(returned_serial_nos)
+ )
+ if rejected_serial_nos:
+ target_doc.rejected_serial_no = "\n".join(rejected_serial_nos)
+
if doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
returned_qty_map = get_returned_qty_map_for_row(
source_parent.name, source_parent.supplier, source_doc.name, doctype
@@ -611,7 +620,7 @@
return filters
-def get_returned_serial_nos(child_doc, parent_doc):
+def get_returned_serial_nos(child_doc, parent_doc, serial_no_field="serial_no"):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
return_ref_field = frappe.scrub(child_doc.doctype)
@@ -620,7 +629,7 @@
serial_nos = []
- fields = ["`{0}`.`serial_no`".format("tab" + child_doc.doctype)]
+ fields = [f"`{'tab' + child_doc.doctype}`.`{serial_no_field}`"]
filters = [
[parent_doc.doctype, "return_against", "=", parent_doc.name],
@@ -630,6 +639,6 @@
]
for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters):
- serial_nos.extend(get_serial_nos(row.serial_no))
+ serial_nos.extend(get_serial_nos(row.get(serial_no_field)))
return serial_nos
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 8b4d28b..3ea0216 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -84,6 +84,9 @@
)
if not self.meta.get_field("sales_team"):
party_details.pop("sales_team")
+ else:
+ self.set("sales_team", party_details.get("sales_team"))
+
self.update_if_missing(party_details)
elif lead:
@@ -136,7 +139,7 @@
self.in_words = money_in_words(amount, self.currency)
def calculate_commission(self):
- if not self.meta.get_field("commission_rate"):
+ if not self.meta.get_field("commission_rate") or self.docstatus.is_submitted():
return
self.round_floats_in(self, ("amount_eligible_for_commission", "commission_rate"))
diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py
index a9561fe..cc80f6c 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -409,7 +409,14 @@
if self.available_materials.get(key) and self.available_materials[key]["batch_no"]:
new_rm_obj = None
for batch_no, batch_qty in self.available_materials[key]["batch_no"].items():
- if batch_qty >= qty:
+ if batch_qty >= qty or (
+ rm_obj.consumed_qty == 0
+ and self.backflush_based_on == "BOM"
+ and len(self.available_materials[key]["batch_no"]) == 1
+ ):
+ if rm_obj.consumed_qty == 0:
+ self.__set_consumed_qty(rm_obj, qty)
+
self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty)
self.available_materials[key]["batch_no"][batch_no] -= qty
return
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 8c403aa..1edd7bf 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -24,11 +24,19 @@
def __init__(self, doc: Document):
self.doc = doc
frappe.flags.round_off_applicable_accounts = []
+
+ self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items")
+
get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
self.calculate()
+ def filter_rows(self):
+ """Exclude rows, that do not fulfill the filter criteria, from totals computation."""
+ items = list(filter(lambda item: not item.get("is_alternative"), self.doc.get("items")))
+ return items
+
def calculate(self):
- if not len(self.doc.get("items")):
+ if not len(self._items):
return
self.discount_amount_applied = False
@@ -70,7 +78,7 @@
if hasattr(self.doc, "tax_withholding_net_total"):
sum_net_amount = 0
sum_base_net_amount = 0
- for item in self.doc.get("items"):
+ for item in self._items:
if hasattr(item, "apply_tds") and item.apply_tds:
sum_net_amount += item.net_amount
sum_base_net_amount += item.base_net_amount
@@ -79,7 +87,7 @@
self.doc.base_tax_withholding_net_total = sum_base_net_amount
def validate_item_tax_template(self):
- for item in self.doc.get("items"):
+ for item in self._items:
if item.item_code and item.get("item_tax_template"):
item_doc = frappe.get_cached_doc("Item", item.item_code)
args = {
@@ -137,7 +145,7 @@
return
if not self.discount_amount_applied:
- for item in self.doc.get("items"):
+ for item in self._items:
self.doc.round_floats_in(item)
if item.discount_percentage == 100:
@@ -236,7 +244,7 @@
if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
return
- for item in self.doc.get("items"):
+ for item in self._items:
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
cumulated_tax_fraction = 0
total_inclusive_tax_amount_per_qty = 0
@@ -317,7 +325,7 @@
self.doc.total
) = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0
- for item in self.doc.get("items"):
+ for item in self._items:
self.doc.total += item.amount
self.doc.total_qty += item.qty
self.doc.base_total += item.base_amount
@@ -354,7 +362,7 @@
]
)
- for n, item in enumerate(self.doc.get("items")):
+ for n, item in enumerate(self._items):
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
for i, tax in enumerate(self.doc.get("taxes")):
# tax_amount represents the amount of tax for the current step
@@ -363,7 +371,7 @@
# Adjust divisional loss to the last item
if tax.charge_type == "Actual":
actual_tax_dict[tax.idx] -= current_tax_amount
- if n == len(self.doc.get("items")) - 1:
+ if n == len(self._items) - 1:
current_tax_amount += actual_tax_dict[tax.idx]
# accumulate tax amount into tax.tax_amount
@@ -391,7 +399,7 @@
)
# set precision in the last item iteration
- if n == len(self.doc.get("items")) - 1:
+ if n == len(self._items) - 1:
self.round_off_totals(tax)
self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"])
@@ -570,7 +578,7 @@
def calculate_total_net_weight(self):
if self.doc.meta.get_field("total_net_weight"):
self.doc.total_net_weight = 0.0
- for d in self.doc.items:
+ for d in self._items:
if d.total_weight:
self.doc.total_net_weight += d.total_weight
@@ -630,7 +638,7 @@
if total_for_discount_amount:
# calculate item amount after Discount Amount
- for i, item in enumerate(self.doc.get("items")):
+ for i, item in enumerate(self._items):
distributed_amount = (
flt(self.doc.discount_amount) * item.net_amount / total_for_discount_amount
)
@@ -643,7 +651,7 @@
self.doc.apply_discount_on == "Net Total"
or not taxes
or total_for_discount_amount == self.doc.net_total
- ) and i == len(self.doc.get("items")) - 1:
+ ) and i == len(self._items) - 1:
discount_amount_loss = flt(
self.doc.net_total - net_total - self.doc.discount_amount, self.doc.precision("net_total")
)
diff --git a/erpnext/crm/doctype/lead_source/lead_source.json b/erpnext/crm/doctype/lead_source/lead_source.json
index 723c6d9..c3cedcc 100644
--- a/erpnext/crm/doctype/lead_source/lead_source.json
+++ b/erpnext/crm/doctype/lead_source/lead_source.json
@@ -26,10 +26,11 @@
}
],
"links": [],
- "modified": "2021-02-08 12:51:48.971517",
+ "modified": "2023-02-10 00:51:44.973957",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead Source",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -58,5 +59,7 @@
],
"quick_entry": 1,
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": [],
+ "translated_doctype": 1
}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js
index 1f76a1a..b261795 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.js
+++ b/erpnext/crm/doctype/opportunity/opportunity.js
@@ -19,10 +19,6 @@
}
}
});
-
- if (frm.doc.opportunity_from && frm.doc.party_name){
- frm.trigger('set_contact_link');
- }
},
validate: function(frm) {
@@ -130,6 +126,10 @@
} else {
frappe.contacts.clear_address_and_contact(frm);
}
+
+ if (frm.doc.opportunity_from && frm.doc.party_name) {
+ frm.trigger('set_contact_link');
+ }
},
set_contact_link: function(frm) {
@@ -137,6 +137,8 @@
frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Customer'}
} else if(frm.doc.opportunity_from == "Lead" && frm.doc.party_name) {
frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Lead'}
+ } else if (frm.doc.opportunity_from == "Prospect" && frm.doc.party_name) {
+ frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Prospect'}
}
},
diff --git a/erpnext/crm/doctype/sales_stage/sales_stage.json b/erpnext/crm/doctype/sales_stage/sales_stage.json
index 77aa559..caf8ff5 100644
--- a/erpnext/crm/doctype/sales_stage/sales_stage.json
+++ b/erpnext/crm/doctype/sales_stage/sales_stage.json
@@ -18,10 +18,11 @@
}
],
"links": [],
- "modified": "2020-05-20 12:22:01.866472",
+ "modified": "2023-02-10 01:40:23.713390",
"modified_by": "Administrator",
"module": "CRM",
"name": "Sales Stage",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -40,5 +41,7 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
- "track_changes": 1
+ "states": [],
+ "track_changes": 1,
+ "translated_doctype": 1
}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
index 38d6993..f44fad3 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
@@ -12,7 +12,7 @@
def __init__(self, access_token=None):
self.access_token = access_token
self.settings = frappe.get_single("Plaid Settings")
- self.products = ["auth", "transactions"]
+ self.products = ["transactions"]
self.client_name = frappe.local.site
self.client = plaid.Client(
client_id=self.settings.plaid_client_id,
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
index 3740d04..3ba6bb9 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
@@ -47,7 +47,7 @@
}
async init_config() {
- this.product = ["auth", "transactions"];
+ this.product = ["transactions"];
this.plaid_env = this.frm.doc.plaid_env;
this.client_name = frappe.boot.sitename;
this.token = await this.get_link_token();
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
index 62ea85f..f3aa6a3 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
@@ -70,7 +70,8 @@
except TypeError:
pass
- bank = json.loads(bank)
+ if isinstance(bank, str):
+ bank = json.loads(bank)
result = []
default_gl_account = get_default_bank_cash_account(company, "Bank")
@@ -177,16 +178,15 @@
)
result = []
- for transaction in reversed(transactions):
- result += new_bank_transaction(transaction)
+ if transactions:
+ for transaction in reversed(transactions):
+ result += new_bank_transaction(transaction)
if result:
last_transaction_date = frappe.db.get_value("Bank Transaction", result.pop(), "date")
frappe.logger().info(
- "Plaid added {} new Bank Transactions from '{}' between {} and {}".format(
- len(result), bank_account, start_date, end_date
- )
+ f"Plaid added {len(result)} new Bank Transactions from '{bank_account}' between {start_date} and {end_date}"
)
frappe.db.set_value(
@@ -230,19 +230,20 @@
bank_account = frappe.db.get_value("Bank Account", dict(integration_id=transaction["account_id"]))
- if float(transaction["amount"]) >= 0:
- debit = 0
- credit = float(transaction["amount"])
+ amount = float(transaction["amount"])
+ if amount >= 0.0:
+ deposit = 0.0
+ withdrawal = amount
else:
- debit = abs(float(transaction["amount"]))
- credit = 0
+ deposit = abs(amount)
+ withdrawal = 0.0
status = "Pending" if transaction["pending"] == "True" else "Settled"
tags = []
try:
tags += transaction["category"]
- tags += ["Plaid Cat. {}".format(transaction["category_id"])]
+ tags += [f'Plaid Cat. {transaction["category_id"]}']
except KeyError:
pass
@@ -254,11 +255,18 @@
"date": getdate(transaction["date"]),
"status": status,
"bank_account": bank_account,
- "deposit": debit,
- "withdrawal": credit,
+ "deposit": deposit,
+ "withdrawal": withdrawal,
"currency": transaction["iso_currency_code"],
"transaction_id": transaction["transaction_id"],
- "reference_number": transaction["payment_meta"]["reference_number"],
+ "transaction_type": (
+ transaction["transaction_code"] or transaction["payment_meta"]["payment_method"]
+ ),
+ "reference_number": (
+ transaction["check_number"]
+ or transaction["payment_meta"]["reference_number"]
+ or transaction["name"]
+ ),
"description": transaction["name"],
}
)
@@ -271,7 +279,7 @@
result.append(new_transaction.name)
except Exception:
- frappe.throw(title=_("Bank transaction creation error"))
+ frappe.throw(_("Bank transaction creation error"))
return result
@@ -300,3 +308,26 @@
def get_link_token_for_update(access_token):
plaid = PlaidConnector(access_token)
return plaid.get_link_token(update_mode=True)
+
+
+def get_company(bank_account_name):
+ from frappe.defaults import get_user_default
+
+ company_names = frappe.db.get_all("Company", pluck="name")
+ if len(company_names) == 1:
+ return company_names[0]
+ if frappe.db.exists("Bank Account", bank_account_name):
+ return frappe.db.get_value("Bank Account", bank_account_name, "company")
+ company_default = get_user_default("Company")
+ if company_default:
+ return company_default
+ frappe.throw(_("Could not detect the Company for updating Bank Accounts"))
+
+
+@frappe.whitelist()
+def update_bank_account_ids(response):
+ data = json.loads(response)
+ institution_name = data["institution"]["name"]
+ bank = frappe.get_doc("Bank", institution_name).as_dict()
+ bank_account_name = f"{data['account']['name']} - {institution_name}"
+ return add_bank_accounts(response, bank, get_company(bank_account_name))
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py
index e8dc3e2..6d34a20 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py
@@ -125,6 +125,8 @@
"unofficial_currency_code": None,
"name": "INTRST PYMNT",
"transaction_type": "place",
+ "transaction_code": "direct debit",
+ "check_number": "3456789",
"amount": -4.22,
"location": {
"city": None,
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index fd19d25..dbfbcc9 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -311,15 +311,10 @@
"on_submit": [
"erpnext.regional.create_transaction_log",
"erpnext.regional.italy.utils.sales_invoice_on_submit",
- "erpnext.regional.saudi_arabia.utils.create_qr_code",
],
- "on_cancel": [
- "erpnext.regional.italy.utils.sales_invoice_on_cancel",
- "erpnext.regional.saudi_arabia.utils.delete_qr_code_file",
- ],
+ "on_cancel": ["erpnext.regional.italy.utils.sales_invoice_on_cancel"],
"on_trash": "erpnext.regional.check_deletion_permission",
},
- "POS Invoice": {"on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]},
"Purchase Invoice": {
"validate": [
"erpnext.regional.united_arab_emirates.utils.update_grand_total_for_rcm",
@@ -347,7 +342,6 @@
"Email Unsubscribe": {
"after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient"
},
- "Company": {"on_trash": ["erpnext.regional.saudi_arabia.utils.delete_vat_settings_for_company"]},
"Integration Request": {
"validate": "erpnext.accounts.doctype.payment_request.payment_request.validate_payment"
},
@@ -362,7 +356,7 @@
scheduler_events = {
"cron": {
- "0/5 * * * *": [
+ "0/15 * * * *": [
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
],
"0/30 * * * *": [
diff --git a/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json b/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json
index 158f143..ba05355 100644
--- a/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json
+++ b/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json
@@ -64,8 +64,6 @@
"fieldtype": "Section Break"
},
{
- "fetch_from": "prevdoc_detail_docname.sales_person",
- "fetch_if_empty": 1,
"fieldname": "service_person",
"fieldtype": "Link",
"in_list_view": 1,
@@ -110,13 +108,15 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-05-27 17:47:21.474282",
+ "modified": "2023-02-27 11:09:33.114458",
"modified_by": "Administrator",
"module": "Maintenance",
"name": "Maintenance Visit Purpose",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json
index c2b331f..db699b9 100644
--- a/erpnext/manufacturing/doctype/bom/bom.json
+++ b/erpnext/manufacturing/doctype/bom/bom.json
@@ -289,7 +289,7 @@
{
"fieldname": "scrap_items",
"fieldtype": "Table",
- "label": "Items",
+ "label": "Scrap Items",
"options": "BOM Scrap Item"
},
{
@@ -605,7 +605,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2023-01-10 07:47:08.652616",
+ "modified": "2023-02-13 17:31:37.504565",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
index c3f52d4..51f7b24 100644
--- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
@@ -212,7 +212,7 @@
["name", "boms_updated", "status"],
)
incomplete_level = any(row.get("status") == "Pending" for row in bom_batches)
- if not bom_batches or incomplete_level:
+ if not bom_batches or not incomplete_level:
continue
# Prep parent BOMs & updated processed BOMs for next level
@@ -252,6 +252,9 @@
current_boms = []
for row in bom_batches:
+ if not row.boms_updated:
+ continue
+
boms_updated = json.loads(row.boms_updated)
current_boms.extend(boms_updated)
boms_updated_dict = {bom: True for bom in boms_updated}
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 3133628..e82f379 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -561,7 +561,34 @@
)
def set_transferred_qty_in_job_card_item(self, ste_doc):
- from frappe.query_builder.functions import Sum
+ def _get_job_card_items_transferred_qty(ste_doc):
+ from frappe.query_builder.functions import Sum
+
+ job_card_items_transferred_qty = {}
+ job_card_items = [
+ x.get("job_card_item") for x in ste_doc.get("items") if x.get("job_card_item")
+ ]
+
+ if job_card_items:
+ se = frappe.qb.DocType("Stock Entry")
+ sed = frappe.qb.DocType("Stock Entry Detail")
+
+ query = (
+ frappe.qb.from_(sed)
+ .join(se)
+ .on(sed.parent == se.name)
+ .select(sed.job_card_item, Sum(sed.qty))
+ .where(
+ (sed.job_card_item.isin(job_card_items))
+ & (se.docstatus == 1)
+ & (se.purpose == "Material Transfer for Manufacture")
+ )
+ .groupby(sed.job_card_item)
+ )
+
+ job_card_items_transferred_qty = frappe._dict(query.run(as_list=True))
+
+ return job_card_items_transferred_qty
def _validate_over_transfer(row, transferred_qty):
"Block over transfer of items if not allowed in settings."
@@ -578,29 +605,23 @@
exc=JobCardOverTransferError,
)
- for row in ste_doc.items:
- if not row.job_card_item:
- continue
+ job_card_items_transferred_qty = _get_job_card_items_transferred_qty(ste_doc)
- sed = frappe.qb.DocType("Stock Entry Detail")
- se = frappe.qb.DocType("Stock Entry")
- transferred_qty = (
- frappe.qb.from_(sed)
- .join(se)
- .on(sed.parent == se.name)
- .select(Sum(sed.qty))
- .where(
- (sed.job_card_item == row.job_card_item)
- & (se.docstatus == 1)
- & (se.purpose == "Material Transfer for Manufacture")
- )
- ).run()[0][0]
-
+ if job_card_items_transferred_qty:
allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
- if not allow_excess:
- _validate_over_transfer(row, transferred_qty)
- frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty))
+ for row in ste_doc.items:
+ if not row.job_card_item:
+ continue
+
+ transferred_qty = flt(job_card_items_transferred_qty.get(row.job_card_item))
+
+ if not allow_excess:
+ _validate_over_transfer(row, transferred_qty)
+
+ frappe.db.set_value(
+ "Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty)
+ )
def set_transferred_qty(self, update_status=False):
"Set total FG Qty in Job Card for which RM was transferred."
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
index 7beecac..e7f67ca 100644
--- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
+++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
@@ -25,8 +25,9 @@
],
"formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
+
if (column.id == "item") {
- if (data["enough_parts_to_build"] > 0) {
+ if (data["in_stock_qty"] >= data["required_qty"]) {
value = `<a style='color:green' href="/app/item/${data['item']}" data-doctype="Item">${data['item']}</a>`;
} else {
value = `<a style='color:red' href="/app/item/${data['item']}" data-doctype="Item">${data['item']}</a>`;
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
index cdf1541..3573a3a 100644
--- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
+++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
@@ -4,7 +4,8 @@
import frappe
from frappe import _
-from frappe.query_builder.functions import Sum
+from frappe.query_builder.functions import Floor, Sum
+from frappe.utils import cint
from pypika.terms import ExistsCriterion
@@ -34,57 +35,55 @@
def get_bom_stock(filters):
- qty_to_produce = filters.get("qty_to_produce") or 1
- if int(qty_to_produce) < 0:
- frappe.throw(_("Quantity to Produce can not be less than Zero"))
+ qty_to_produce = filters.get("qty_to_produce")
+ if cint(qty_to_produce) <= 0:
+ frappe.throw(_("Quantity to Produce should be greater than zero."))
if filters.get("show_exploded_view"):
bom_item_table = "BOM Explosion Item"
else:
bom_item_table = "BOM Item"
- bin = frappe.qb.DocType("Bin")
- bom = frappe.qb.DocType("BOM")
- bom_item = frappe.qb.DocType(bom_item_table)
-
- query = (
- frappe.qb.from_(bom)
- .inner_join(bom_item)
- .on(bom.name == bom_item.parent)
- .left_join(bin)
- .on(bom_item.item_code == bin.item_code)
- .select(
- bom_item.item_code,
- bom_item.description,
- bom_item.stock_qty,
- bom_item.stock_uom,
- (bom_item.stock_qty / bom.quantity) * qty_to_produce,
- Sum(bin.actual_qty),
- Sum(bin.actual_qty) / (bom_item.stock_qty / bom.quantity),
- )
- .where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM"))
- .groupby(bom_item.item_code)
+ warehouse_details = frappe.db.get_value(
+ "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
)
- if filters.get("warehouse"):
- warehouse_details = frappe.db.get_value(
- "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
- )
+ BOM = frappe.qb.DocType("BOM")
+ BOM_ITEM = frappe.qb.DocType(bom_item_table)
+ BIN = frappe.qb.DocType("Bin")
+ WH = frappe.qb.DocType("Warehouse")
+ CONDITIONS = ()
- if warehouse_details:
- wh = frappe.qb.DocType("Warehouse")
- query = query.where(
- ExistsCriterion(
- frappe.qb.from_(wh)
- .select(wh.name)
- .where(
- (wh.lft >= warehouse_details.lft)
- & (wh.rgt <= warehouse_details.rgt)
- & (bin.warehouse == wh.name)
- )
- )
+ if warehouse_details:
+ CONDITIONS = ExistsCriterion(
+ frappe.qb.from_(WH)
+ .select(WH.name)
+ .where(
+ (WH.lft >= warehouse_details.lft)
+ & (WH.rgt <= warehouse_details.rgt)
+ & (BIN.warehouse == WH.name)
)
- else:
- query = query.where(bin.warehouse == filters.get("warehouse"))
+ )
+ else:
+ CONDITIONS = BIN.warehouse == filters.get("warehouse")
- return query.run()
+ QUERY = (
+ frappe.qb.from_(BOM)
+ .inner_join(BOM_ITEM)
+ .on(BOM.name == BOM_ITEM.parent)
+ .left_join(BIN)
+ .on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS))
+ .select(
+ BOM_ITEM.item_code,
+ BOM_ITEM.description,
+ BOM_ITEM.stock_qty,
+ BOM_ITEM.stock_uom,
+ BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity,
+ Sum(BIN.actual_qty).as_("actual_qty"),
+ Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))),
+ )
+ .where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))
+ .groupby(BOM_ITEM.item_code)
+ )
+
+ return QUERY.run()
diff --git a/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py
new file mode 100644
index 0000000..1c56ebe
--- /dev/null
+++ b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py
@@ -0,0 +1,108 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+
+import frappe
+from frappe.exceptions import ValidationError
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import floor
+
+from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+from erpnext.manufacturing.report.bom_stock_report.bom_stock_report import (
+ get_bom_stock as bom_stock_report,
+)
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
+
+class TestBomStockReport(FrappeTestCase):
+ def setUp(self):
+ self.warehouse = "_Test Warehouse - _TC"
+ self.fg_item, self.rm_items = create_items()
+ make_stock_entry(target=self.warehouse, item_code=self.rm_items[0], qty=20, basic_rate=100)
+ make_stock_entry(target=self.warehouse, item_code=self.rm_items[1], qty=40, basic_rate=200)
+ self.bom = make_bom(item=self.fg_item, quantity=1, raw_materials=self.rm_items, rm_qty=10)
+
+ def test_bom_stock_report(self):
+ # Test 1: When `qty_to_produce` is 0.
+ filters = frappe._dict(
+ {
+ "bom": self.bom.name,
+ "warehouse": "Stores - _TC",
+ "qty_to_produce": 0,
+ }
+ )
+ self.assertRaises(ValidationError, bom_stock_report, filters)
+
+ # Test 2: When stock is not available.
+ data = bom_stock_report(
+ frappe._dict(
+ {
+ "bom": self.bom.name,
+ "warehouse": "Stores - _TC",
+ "qty_to_produce": 1,
+ }
+ )
+ )
+ expected_data = get_expected_data(self.bom, "Stores - _TC", 1)
+ self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
+
+ # Test 3: When stock is available.
+ data = bom_stock_report(
+ frappe._dict(
+ {
+ "bom": self.bom.name,
+ "warehouse": self.warehouse,
+ "qty_to_produce": 1,
+ }
+ )
+ )
+ expected_data = get_expected_data(self.bom, self.warehouse, 1)
+ self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
+
+
+def create_items():
+ fg_item = make_item(properties={"is_stock_item": 1}).name
+ rm_item1 = make_item(
+ properties={
+ "is_stock_item": 1,
+ "standard_rate": 100,
+ "opening_stock": 100,
+ "last_purchase_rate": 100,
+ }
+ ).name
+ rm_item2 = make_item(
+ properties={
+ "is_stock_item": 1,
+ "standard_rate": 200,
+ "opening_stock": 200,
+ "last_purchase_rate": 200,
+ }
+ ).name
+
+ return fg_item, [rm_item1, rm_item2]
+
+
+def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False):
+ expected_data = []
+
+ for item in bom.get("exploded_items") if show_exploded_view else bom.get("items"):
+ in_stock_qty = frappe.get_cached_value(
+ "Bin", {"item_code": item.item_code, "warehouse": warehouse}, "actual_qty"
+ )
+
+ expected_data.append(
+ [
+ item.item_code,
+ item.description,
+ item.stock_qty,
+ item.stock_uom,
+ item.stock_qty * qty_to_produce / bom.quantity,
+ in_stock_qty,
+ floor(in_stock_qty / (item.stock_qty * qty_to_produce / bom.quantity))
+ if in_stock_qty
+ else None,
+ ]
+ )
+
+ return expected_data
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 52b1b05..0a0c792 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -250,18 +250,14 @@
erpnext.patches.v13_0.update_category_in_ltds_certificate
erpnext.patches.v13_0.fetch_thumbnail_in_website_items
erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit
-erpnext.patches.v13_0.create_ksa_vat_custom_fields # 07-01-2022
erpnext.patches.v14_0.migrate_crm_settings
-erpnext.patches.v13_0.rename_ksa_qr_field
erpnext.patches.v13_0.wipe_serial_no_field_for_0_qty
-erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
erpnext.patches.v13_0.agriculture_deprecation_warning
erpnext.patches.v13_0.hospitality_deprecation_warning
erpnext.patches.v13_0.update_asset_quantity_field
erpnext.patches.v13_0.delete_bank_reconciliation_detail
erpnext.patches.v13_0.enable_provisional_accounting
erpnext.patches.v13_0.non_profit_deprecation_warning
-erpnext.patches.v13_0.enable_ksa_vat_docs #1
erpnext.patches.v13_0.show_india_localisation_deprecation_warning
erpnext.patches.v13_0.show_hr_payroll_deprecation_warning
erpnext.patches.v13_0.reset_corrupt_defaults
@@ -269,6 +265,8 @@
erpnext.patches.v15_0.delete_taxjar_doctypes
erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets
erpnext.patches.v14_0.update_reference_due_date_in_journal_entry
+erpnext.patches.v15_0.saudi_depreciation_warning
+erpnext.patches.v15_0.delete_saudi_doctypes
[post_model_sync]
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
@@ -306,7 +304,6 @@
execute:frappe.delete_doc("DocType", "Naming Series")
erpnext.patches.v13_0.job_card_status_on_hold
erpnext.patches.v14_0.copy_is_subcontracted_value_to_is_old_subcontracting_flow
-erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.patches.v14_0.crm_ux_cleanup
erpnext.patches.v14_0.migrate_existing_lead_notes_as_per_the_new_format
erpnext.patches.v14_0.remove_india_localisation # 14-07-2022
@@ -315,7 +312,6 @@
erpnext.patches.v14_0.fix_crm_no_of_employees
erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes
erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries
-erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger
erpnext.patches.v13_0.update_schedule_type_in_loans
erpnext.patches.v13_0.drop_unused_sle_index_parts
erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization
@@ -329,3 +325,6 @@
erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries
erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance
erpnext.patches.v14_0.update_closing_balances
+# below 2 migration patches should always run last
+erpnext.patches.v14_0.migrate_gl_to_payment_ledger
+erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger
diff --git a/erpnext/patches/v11_0/update_sales_partner_type.py b/erpnext/patches/v11_0/update_sales_partner_type.py
index 2d37fd6..72fd424 100644
--- a/erpnext/patches/v11_0/update_sales_partner_type.py
+++ b/erpnext/patches/v11_0/update_sales_partner_type.py
@@ -1,16 +1,17 @@
import frappe
-from frappe import _
def execute():
- from erpnext.setup.setup_wizard.operations.install_fixtures import default_sales_partner_type
+ from erpnext.setup.setup_wizard.operations.install_fixtures import read_lines
frappe.reload_doc("selling", "doctype", "sales_partner_type")
frappe.local.lang = frappe.db.get_default("lang") or "en"
+ default_sales_partner_type = read_lines("sales_partner_type.txt")
+
for s in default_sales_partner_type:
- insert_sales_partner_type(_(s))
+ insert_sales_partner_type(s)
# get partner type in existing forms (customized)
# and create a document if not created
diff --git a/erpnext/patches/v13_0/create_ksa_vat_custom_fields.py b/erpnext/patches/v13_0/create_ksa_vat_custom_fields.py
deleted file mode 100644
index 093463a..0000000
--- a/erpnext/patches/v13_0/create_ksa_vat_custom_fields.py
+++ /dev/null
@@ -1,11 +0,0 @@
-import frappe
-
-from erpnext.regional.saudi_arabia.setup import make_custom_fields
-
-
-def execute():
- company = frappe.get_all("Company", filters={"country": "Saudi Arabia"})
- if not company:
- return
-
- make_custom_fields()
diff --git a/erpnext/patches/v13_0/delete_old_purchase_reports.py b/erpnext/patches/v13_0/delete_old_purchase_reports.py
index 987f53f..60621fb 100644
--- a/erpnext/patches/v13_0/delete_old_purchase_reports.py
+++ b/erpnext/patches/v13_0/delete_old_purchase_reports.py
@@ -17,10 +17,11 @@
for report in reports_to_delete:
if frappe.db.exists("Report", report):
+ delete_links_from_desktop_icons(report)
delete_auto_email_reports(report)
check_and_delete_linked_reports(report)
- frappe.delete_doc("Report", report)
+ frappe.delete_doc("Report", report, force=True)
def delete_auto_email_reports(report):
@@ -28,3 +29,10 @@
auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"])
for auto_email_report in auto_email_reports:
frappe.delete_doc("Auto Email Report", auto_email_report[0])
+
+
+def delete_links_from_desktop_icons(report):
+ """Check for one or multiple Desktop Icons and delete"""
+ desktop_icons = frappe.db.get_values("Desktop Icon", {"_report": report}, ["name"])
+ for desktop_icon in desktop_icons:
+ frappe.delete_doc("Desktop Icon", desktop_icon[0], force=True)
diff --git a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py
deleted file mode 100644
index 84b6c37..0000000
--- a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (c) 2020, Wahni Green Technologies and Contributors
-# License: GNU General Public License v3. See license.txt
-
-import frappe
-
-from erpnext.regional.saudi_arabia.setup import add_print_formats
-
-
-def execute():
- company = frappe.get_all("Company", filters={"country": "Saudi Arabia"})
- if company:
- add_print_formats()
- return
-
- if frappe.db.exists("DocType", "Print Format"):
- frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True)
- frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True)
- for d in ("KSA VAT Invoice", "KSA POS Invoice"):
- frappe.db.set_value("Print Format", d, "disabled", 1)
diff --git a/erpnext/patches/v13_0/enable_ksa_vat_docs.py b/erpnext/patches/v13_0/enable_ksa_vat_docs.py
deleted file mode 100644
index 4adf4d7..0000000
--- a/erpnext/patches/v13_0/enable_ksa_vat_docs.py
+++ /dev/null
@@ -1,12 +0,0 @@
-import frappe
-
-from erpnext.regional.saudi_arabia.setup import add_permissions, add_print_formats
-
-
-def execute():
- company = frappe.get_all("Company", filters={"country": "Saudi Arabia"})
- if not company:
- return
-
- add_print_formats()
- add_permissions()
diff --git a/erpnext/patches/v13_0/rename_ksa_qr_field.py b/erpnext/patches/v13_0/rename_ksa_qr_field.py
deleted file mode 100644
index e4b9141..0000000
--- a/erpnext/patches/v13_0/rename_ksa_qr_field.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# Copyright (c) 2020, Wahni Green Technologies and Contributors
-# License: GNU General Public License v3. See license.txt
-
-import frappe
-from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
-from frappe.model.utils.rename_field import rename_field
-
-
-def execute():
- company = frappe.get_all("Company", filters={"country": "Saudi Arabia"})
- if not company:
- return
-
- if frappe.db.exists("DocType", "Sales Invoice"):
- frappe.reload_doc("accounts", "doctype", "sales_invoice", force=True)
-
- # rename_field method assumes that the field already exists or the doc is synced
- if not frappe.db.has_column("Sales Invoice", "ksa_einv_qr"):
- create_custom_fields(
- {
- "Sales Invoice": [
- dict(
- fieldname="ksa_einv_qr",
- label="KSA E-Invoicing QR",
- fieldtype="Attach Image",
- read_only=1,
- no_copy=1,
- hidden=1,
- )
- ]
- }
- )
-
- if frappe.db.has_column("Sales Invoice", "qr_code"):
- rename_field("Sales Invoice", "qr_code", "ksa_einv_qr")
- frappe.delete_doc_if_exists("Custom Field", "Sales Invoice-qr_code")
diff --git a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
index 371ecbc..5c46bf3 100644
--- a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
+++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
@@ -27,7 +27,13 @@
records = (
frappe.qb.from_(asset)
- .select(asset.name, asset.opening_accumulated_depreciation, asset.docstatus)
+ .select(
+ asset.name,
+ asset.opening_accumulated_depreciation,
+ asset.gross_purchase_amount,
+ asset.number_of_depreciations_booked,
+ asset.docstatus,
+ )
.where(asset.calculate_depreciation == 1)
.where(asset.docstatus < 2)
).run(as_dict=True)
diff --git a/erpnext/patches/v15_0/delete_saudi_doctypes.py b/erpnext/patches/v15_0/delete_saudi_doctypes.py
new file mode 100644
index 0000000..371e335
--- /dev/null
+++ b/erpnext/patches/v15_0/delete_saudi_doctypes.py
@@ -0,0 +1,25 @@
+import click
+import frappe
+
+
+def execute():
+ if "ksa" in frappe.get_installed_apps():
+ return
+
+ doctypes = ["KSA VAT Setting", "KSA VAT Purchase Account", "KSA VAT Sales Account"]
+ for doctype in doctypes:
+ frappe.delete_doc("DocType", doctype, ignore_missing=True)
+
+ print_formats = ["KSA POS Invoice", "KSA VAT Invoice"]
+ for print_format in print_formats:
+ frappe.delete_doc("Print Format", print_format, ignore_missing=True, force=True)
+
+ reports = ["KSA VAT"]
+ for report in reports:
+ frappe.delete_doc("Report", report, ignore_missing=True, force=True)
+
+ click.secho(
+ "Region Saudi Arabia(KSA) is moved to a separate app"
+ "Please install the app to continue using the module: https://github.com/8848digital/KSA",
+ fg="yellow",
+ )
diff --git a/erpnext/patches/v15_0/saudi_depreciation_warning.py b/erpnext/patches/v15_0/saudi_depreciation_warning.py
new file mode 100644
index 0000000..6af8efe
--- /dev/null
+++ b/erpnext/patches/v15_0/saudi_depreciation_warning.py
@@ -0,0 +1,12 @@
+import click
+import frappe
+
+
+def execute():
+ if "ksa" in frappe.get_installed_apps():
+ return
+ click.secho(
+ "Region Saudi Arabia(KSA) is moved to a separate app\n"
+ "Please install the app to continue using the KSA Features: https://github.com/8848digital/KSA",
+ fg="yellow",
+ )
diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json
index 37d98ad..ba7aa85 100644
--- a/erpnext/projects/doctype/project/project.json
+++ b/erpnext/projects/doctype/project/project.json
@@ -408,7 +408,7 @@
"depends_on": "eval:(doc.frequency == \"Daily\" && doc.collect_progress == true)",
"fieldname": "daily_time_to_send",
"fieldtype": "Time",
- "label": "Time to send"
+ "label": "Daily Time to send"
},
{
"depends_on": "eval:(doc.frequency == \"Weekly\" && doc.collect_progress == true)",
@@ -421,7 +421,7 @@
"depends_on": "eval:(doc.frequency == \"Weekly\" && doc.collect_progress == true)",
"fieldname": "weekly_time_to_send",
"fieldtype": "Time",
- "label": "Time to send"
+ "label": "Weekly Time to send"
},
{
"fieldname": "column_break_45",
@@ -451,7 +451,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 4,
- "modified": "2022-06-23 16:45:06.108499",
+ "modified": "2023-02-14 04:54:25.819620",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",
@@ -497,4 +497,4 @@
"timeline_field": "customer",
"title_field": "project_name",
"track_seen": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/projects/doctype/timesheet/timesheet.json b/erpnext/projects/doctype/timesheet/timesheet.json
index 0cce129..4683006 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.json
+++ b/erpnext/projects/doctype/timesheet/timesheet.json
@@ -282,21 +282,21 @@
{
"fieldname": "base_total_costing_amount",
"fieldtype": "Currency",
- "label": "Total Costing Amount",
+ "label": "Base Total Costing Amount",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "base_total_billable_amount",
"fieldtype": "Currency",
- "label": "Total Billable Amount",
+ "label": "Base Total Billable Amount",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "base_total_billed_amount",
"fieldtype": "Currency",
- "label": "Total Billed Amount",
+ "label": "Base Total Billed Amount",
"print_hide": 1,
"read_only": 1
},
@@ -311,10 +311,11 @@
"idx": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-06-15 22:08:53.930200",
+ "modified": "2023-02-14 04:55:41.735991",
"modified_by": "Administrator",
"module": "Projects",
"name": "Timesheet",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@@ -388,5 +389,6 @@
],
"sort_field": "modified",
"sort_order": "ASC",
+ "states": [],
"title_field": "title"
}
\ No newline at end of file
diff --git a/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js b/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js
index f7c19a1..0cda938 100644
--- a/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js
+++ b/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js
@@ -182,6 +182,9 @@
);
} else {
this.transactions.splice(transaction_index, 1);
+ for (const [k, v] of Object.entries(this.transaction_dt_map)) {
+ if (v > transaction_index) this.transaction_dt_map[k] = v - 1;
+ }
}
this.datatable.refresh(this.transactions, this.columns);
diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
index 911343d..321b812 100644
--- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
+++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
@@ -20,7 +20,7 @@
doctype: "Bank Transaction",
filters: { name: this.bank_transaction_name },
fieldname: [
- "date as reference_date",
+ "date",
"deposit",
"withdrawal",
"currency",
@@ -33,6 +33,7 @@
"party",
"unallocated_amount",
"allocated_amount",
+ "transaction_type",
],
},
callback: (r) => {
@@ -41,11 +42,23 @@
r.message.payment_entry = 1;
r.message.journal_entry = 1;
this.dialog.set_values(r.message);
+ this.copy_data_to_voucher();
this.dialog.show();
}
},
});
}
+
+ copy_data_to_voucher() {
+ let copied = {
+ reference_number: this.bank_transaction.reference_number || this.bank_transaction.description,
+ posting_date: this.bank_transaction.date,
+ reference_date: this.bank_transaction.date,
+ mode_of_payment: this.bank_transaction.transaction_type,
+ };
+ this.dialog.set_values(copied);
+ }
+
get_linked_vouchers(document_types) {
frappe.call({
method:
@@ -75,10 +88,9 @@
row[1],
row[2],
reference_date,
- row[8],
format_currency(row[3], row[9]),
- row[6],
row[4],
+ row[6],
]);
});
this.get_dt_columns();
@@ -104,7 +116,7 @@
{
name: __("Document Name"),
editable: false,
- width: 150,
+ width: 1,
},
{
name: __("Reference Date"),
@@ -112,25 +124,19 @@
width: 120,
},
{
- name: "Posting Date",
- editable: false,
- width: 120,
- },
- {
- name: __("Amount"),
+ name: __("Remaining"),
editable: false,
width: 100,
},
{
- name: __("Party"),
- editable: false,
- width: 120,
- },
-
- {
name: __("Reference Number"),
editable: false,
- width: 140,
+ width: 200,
+ },
+ {
+ name: __("Party"),
+ editable: false,
+ width: 100,
},
];
}
@@ -225,6 +231,16 @@
onchange: () => this.update_options(),
},
{
+ fieldname: "column_break_5",
+ fieldtype: "Column Break",
+ },
+ {
+ fieldtype: "Check",
+ label: "Bank Transaction",
+ fieldname: "bank_transaction",
+ onchange: () => this.update_options(),
+ },
+ {
fieldtype: "Section Break",
fieldname: "section_break_1",
label: __("Select Vouchers to Match"),
@@ -289,7 +305,7 @@
fieldtype: "Column Break",
},
{
- default: "Journal Entry Type",
+ default: "Bank Entry",
fieldname: "journal_entry_type",
fieldtype: "Select",
label: "Journal Entry Type",
@@ -364,7 +380,12 @@
fieldtype: "Section Break",
fieldname: "details_section",
label: "Transaction Details",
- collapsible: 1,
+ },
+ {
+ fieldname: "date",
+ fieldtype: "Date",
+ label: "Date",
+ read_only: 1,
},
{
fieldname: "deposit",
@@ -381,14 +402,14 @@
read_only: 1,
},
{
- fieldname: "description",
- fieldtype: "Small Text",
- label: "Description",
+ fieldname: "column_break_17",
+ fieldtype: "Column Break",
read_only: 1,
},
{
- fieldname: "column_break_17",
- fieldtype: "Column Break",
+ fieldname: "description",
+ fieldtype: "Small Text",
+ label: "Description",
read_only: 1,
},
{
@@ -398,7 +419,6 @@
options: "Currency",
read_only: 1,
},
-
{
fieldname: "unallocated_amount",
fieldtype: "Currency",
@@ -593,4 +613,4 @@
}
}
-};
\ No newline at end of file
+};
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index a87c3ec..d1a55e6 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -91,6 +91,9 @@
}
_calculate_taxes_and_totals() {
+ const is_quotation = this.frm.doc.doctype == "Quotation";
+ this.frm.doc._items = is_quotation ? this.filtered_items() : this.frm.doc.items;
+
this.validate_conversion_rate();
this.calculate_item_values();
this.initialize_taxes();
@@ -122,7 +125,7 @@
calculate_item_values() {
var me = this;
if (!this.discount_amount_applied) {
- for (const item of this.frm.doc.items || []) {
+ for (const item of this.frm.doc._items || []) {
frappe.model.round_floats_in(item);
item.net_rate = item.rate;
item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty;
@@ -131,8 +134,8 @@
item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item));
}
else {
- let qty = item.qty || 1;
- qty = me.frm.doc.is_return ? -1 * qty : qty;
+ // allow for '0' qty on Credit/Debit notes
+ let qty = item.qty || -1
item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item));
}
@@ -206,7 +209,7 @@
});
if(has_inclusive_tax==false) return;
- $.each(me.frm.doc["items"] || [], function(n, item) {
+ $.each(me.frm.doc._items || [], function(n, item) {
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
var cumulated_tax_fraction = 0.0;
var total_inclusive_tax_amount_per_qty = 0;
@@ -277,7 +280,7 @@
var me = this;
this.frm.doc.total_qty = this.frm.doc.total = this.frm.doc.base_total = this.frm.doc.net_total = this.frm.doc.base_net_total = 0.0;
- $.each(this.frm.doc["items"] || [], function(i, item) {
+ $.each(this.frm.doc._items || [], function(i, item) {
me.frm.doc.total += item.amount;
me.frm.doc.total_qty += item.qty;
me.frm.doc.base_total += item.base_amount;
@@ -330,7 +333,7 @@
}
});
- $.each(this.frm.doc["items"] || [], function(n, item) {
+ $.each(this.frm.doc._items || [], function(n, item) {
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
$.each(me.frm.doc["taxes"] || [], function(i, tax) {
// tax_amount represents the amount of tax for the current step
@@ -339,7 +342,7 @@
// Adjust divisional loss to the last item
if (tax.charge_type == "Actual") {
actual_tax_dict[tax.idx] -= current_tax_amount;
- if (n == me.frm.doc["items"].length - 1) {
+ if (n == me.frm.doc._items.length - 1) {
current_tax_amount += actual_tax_dict[tax.idx];
}
}
@@ -376,7 +379,7 @@
}
// set precision in the last item iteration
- if (n == me.frm.doc["items"].length - 1) {
+ if (n == me.frm.doc._items.length - 1) {
me.round_off_totals(tax);
me.set_in_company_currency(tax,
["tax_amount", "tax_amount_after_discount_amount"]);
@@ -599,10 +602,11 @@
_cleanup() {
this.frm.doc.base_in_words = this.frm.doc.in_words = "";
+ let items = this.frm.doc._items;
- if(this.frm.doc["items"] && this.frm.doc["items"].length) {
- if(!frappe.meta.get_docfield(this.frm.doc["items"][0].doctype, "item_tax_amount", this.frm.doctype)) {
- $.each(this.frm.doc["items"] || [], function(i, item) {
+ if(items && items.length) {
+ if(!frappe.meta.get_docfield(items[0].doctype, "item_tax_amount", this.frm.doctype)) {
+ $.each(items || [], function(i, item) {
delete item["item_tax_amount"];
});
}
@@ -655,7 +659,7 @@
var net_total = 0;
// calculate item amount after Discount Amount
if (total_for_discount_amount) {
- $.each(this.frm.doc["items"] || [], function(i, item) {
+ $.each(this.frm.doc._items || [], function(i, item) {
distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount;
item.net_amount = flt(item.net_amount - distributed_amount,
precision("base_amount", item));
@@ -663,7 +667,7 @@
// discount amount rounding loss adjustment if no taxes
if ((!(me.frm.doc.taxes || []).length || total_for_discount_amount==me.frm.doc.net_total || (me.frm.doc.apply_discount_on == "Net Total"))
- && i == (me.frm.doc.items || []).length - 1) {
+ && i == (me.frm.doc._items || []).length - 1) {
var discount_amount_loss = flt(me.frm.doc.net_total - net_total
- me.frm.doc.discount_amount, precision("net_total"));
item.net_amount = flt(item.net_amount + discount_amount_loss,
@@ -892,4 +896,8 @@
}
}
+
+ filtered_items() {
+ return this.frm.doc.items.filter(item => !item["is_alternative"]);
+ }
};
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 09f2c5d..8d69ea0 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -488,7 +488,7 @@
() => {
var d = locals[cdt][cdn];
me.add_taxes_from_item_tax_template(d.item_tax_rate);
- if (d.free_item_data) {
+ if (d.free_item_data && d.free_item_data.length > 0) {
me.apply_product_discount(d);
}
},
@@ -1884,11 +1884,13 @@
get_advances() {
if(!this.frm.is_return) {
+ var me = this;
return this.frm.call({
method: "set_advances",
doc: this.frm.doc,
callback: function(r, rt) {
refresh_field("advances");
+ me.frm.dirty();
}
})
}
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 51dcd64..58aa8d7 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -221,9 +221,9 @@
callback: function(r) {
if (r.message && r.message.length) {
r.message.forEach((dimension) => {
- let found = filters.some(el => el.fieldname === dimension['fieldname']);
+ let existing_filter = filters.filter(el => el.fieldname === dimension['fieldname']);
- if (!found) {
+ if (!existing_filter.length) {
filters.splice(index, 0, {
"fieldname": dimension["fieldname"],
"label": __(dimension["doctype"]),
@@ -232,6 +232,11 @@
return frappe.db.get_link_options(dimension["doctype"], txt);
},
});
+ } else {
+ existing_filter[0]['fieldtype'] = "MultiSelectList";
+ existing_filter[0]['get_data'] = function(txt) {
+ return frappe.db.get_link_options(dimension["doctype"], txt);
+ }
}
});
}
diff --git a/erpnext/regional/doctype/ksa_vat_purchase_account/__init__.py b/erpnext/regional/doctype/ksa_vat_purchase_account/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/regional/doctype/ksa_vat_purchase_account/__init__.py
+++ /dev/null
diff --git a/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.json b/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.json
deleted file mode 100644
index 89ba3e9..0000000
--- a/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.json
+++ /dev/null
@@ -1,49 +0,0 @@
-{
- "actions": [],
- "creation": "2021-07-13 09:17:09.862163",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "title",
- "item_tax_template",
- "account"
- ],
- "fields": [
- {
- "fieldname": "account",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Account",
- "options": "Account",
- "reqd": 1
- },
- {
- "fieldname": "title",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Title",
- "reqd": 1
- },
- {
- "fieldname": "item_tax_template",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Item Tax Template",
- "options": "Item Tax Template",
- "reqd": 1
- }
- ],
- "index_web_pages_for_search": 1,
- "istable": 1,
- "links": [],
- "modified": "2021-08-04 06:42:38.205597",
- "modified_by": "Administrator",
- "module": "Regional",
- "name": "KSA VAT Purchase Account",
- "owner": "Administrator",
- "permissions": [],
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.py b/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.py
deleted file mode 100644
index 3920bc5..0000000
--- a/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2021, Havenir Solutions and contributors
-# For license information, please see license.txt
-
-# import frappe
-from frappe.model.document import Document
-
-
-class KSAVATPurchaseAccount(Document):
- pass
diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/__init__.py b/erpnext/regional/doctype/ksa_vat_sales_account/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/regional/doctype/ksa_vat_sales_account/__init__.py
+++ /dev/null
diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.js b/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.js
deleted file mode 100644
index 72613f4..0000000
--- a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2021, Havenir Solutions and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('KSA VAT Sales Account', {
- // refresh: function(frm) {
-
- // }
-});
diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.json b/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.json
deleted file mode 100644
index df27478..0000000
--- a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.json
+++ /dev/null
@@ -1,49 +0,0 @@
-{
- "actions": [],
- "creation": "2021-07-13 08:46:33.820968",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "title",
- "item_tax_template",
- "account"
- ],
- "fields": [
- {
- "fieldname": "account",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Account",
- "options": "Account",
- "reqd": 1
- },
- {
- "fieldname": "title",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Title",
- "reqd": 1
- },
- {
- "fieldname": "item_tax_template",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Item Tax Template",
- "options": "Item Tax Template",
- "reqd": 1
- }
- ],
- "index_web_pages_for_search": 1,
- "istable": 1,
- "links": [],
- "modified": "2021-08-04 06:42:00.081407",
- "modified_by": "Administrator",
- "module": "Regional",
- "name": "KSA VAT Sales Account",
- "owner": "Administrator",
- "permissions": [],
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.py b/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.py
deleted file mode 100644
index 7c2689f..0000000
--- a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2021, Havenir Solutions and contributors
-# For license information, please see license.txt
-
-# import frappe
-from frappe.model.document import Document
-
-
-class KSAVATSalesAccount(Document):
- pass
diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/test_ksa_vat_sales_account.py b/erpnext/regional/doctype/ksa_vat_sales_account/test_ksa_vat_sales_account.py
deleted file mode 100644
index 1d6a6a7..0000000
--- a/erpnext/regional/doctype/ksa_vat_sales_account/test_ksa_vat_sales_account.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2021, Havenir Solutions and Contributors
-# See license.txt
-
-# import frappe
-import unittest
-
-
-class TestKSAVATSalesAccount(unittest.TestCase):
- pass
diff --git a/erpnext/regional/doctype/ksa_vat_setting/__init__.py b/erpnext/regional/doctype/ksa_vat_setting/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/regional/doctype/ksa_vat_setting/__init__.py
+++ /dev/null
diff --git a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.js b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.js
deleted file mode 100644
index 00b62b9..0000000
--- a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2021, Havenir Solutions and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('KSA VAT Setting', {
- onload: function () {
- frappe.breadcrumbs.add('Accounts', 'KSA VAT Setting');
- }
-});
diff --git a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.json b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.json
deleted file mode 100644
index 3361946..0000000
--- a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.json
+++ /dev/null
@@ -1,49 +0,0 @@
-{
- "actions": [],
- "autoname": "field:company",
- "creation": "2021-07-13 08:49:01.100356",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "company",
- "ksa_vat_sales_accounts",
- "ksa_vat_purchase_accounts"
- ],
- "fields": [
- {
- "fieldname": "company",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Company",
- "options": "Company",
- "reqd": 1,
- "unique": 1
- },
- {
- "fieldname": "ksa_vat_sales_accounts",
- "fieldtype": "Table",
- "label": "KSA VAT Sales Accounts",
- "options": "KSA VAT Sales Account",
- "reqd": 1
- },
- {
- "fieldname": "ksa_vat_purchase_accounts",
- "fieldtype": "Table",
- "label": "KSA VAT Purchase Accounts",
- "options": "KSA VAT Purchase Account",
- "reqd": 1
- }
- ],
- "links": [],
- "modified": "2021-08-26 04:29:06.499378",
- "modified_by": "Administrator",
- "module": "Regional",
- "name": "KSA VAT Setting",
- "owner": "Administrator",
- "permissions": [],
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "company",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.py b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.py
deleted file mode 100644
index bdae116..0000000
--- a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2021, Havenir Solutions and contributors
-# For license information, please see license.txt
-
-# import frappe
-from frappe.model.document import Document
-
-
-class KSAVATSetting(Document):
- pass
diff --git a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting_list.js b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting_list.js
deleted file mode 100644
index 269cbec..0000000
--- a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting_list.js
+++ /dev/null
@@ -1,5 +0,0 @@
-frappe.listview_settings['KSA VAT Setting'] = {
- onload () {
- frappe.breadcrumbs.add('Accounts');
- }
-}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/ksa_vat_setting/test_ksa_vat_setting.py b/erpnext/regional/doctype/ksa_vat_setting/test_ksa_vat_setting.py
deleted file mode 100644
index 7207901..0000000
--- a/erpnext/regional/doctype/ksa_vat_setting/test_ksa_vat_setting.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2021, Havenir Solutions and Contributors
-# See license.txt
-
-# import frappe
-import unittest
-
-
-class TestKSAVATSetting(unittest.TestCase):
- pass
diff --git a/erpnext/regional/print_format/ksa_pos_invoice/__init__.py b/erpnext/regional/print_format/ksa_pos_invoice/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/regional/print_format/ksa_pos_invoice/__init__.py
+++ /dev/null
diff --git a/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json b/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json
deleted file mode 100644
index c2a3092..0000000
--- a/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- "absolute_value": 0,
- "align_labels_right": 0,
- "creation": "2021-12-07 13:25:05.424827",
- "css": "",
- "custom_format": 1,
- "default_print_language": "en",
- "disabled": 1,
- "doc_type": "POS Invoice",
- "docstatus": 0,
- "doctype": "Print Format",
- "font_size": 0,
- "html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n<p class=\"text-center\" style=\"margin-bottom: 1rem\">\n\t{{ doc.company }}<br>\n\t<b>{{ doc.select_print_heading or _(\"Invoice\") }}</b><br>\n\t<img src={{doc.ksa_einv_qr}}>\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Cashier\") }}:</b> {{ doc.owner }}<br>\n\t<b>{{ _(\"Customer\") }}:</b> {{ doc.customer_name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t<b>{{ _(\"Time\") }}:</b> {{ doc.get_formatted(\"posting_time\") }}<br>\n</p>\n\n<hr>\n<table class=\"table table-condensed\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"40%\">{{ _(\"Item\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"35%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"SR.No\") }}:</b><br>\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"net_amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.change_amount -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t{%- endif -%}\n\t</tbody>\n</table>\n<hr>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
- "idx": 0,
- "line_breaks": 0,
- "margin_bottom": 0.0,
- "margin_left": 0.0,
- "margin_right": 0.0,
- "margin_top": 0.0,
- "modified": "2021-12-08 10:25:01.930885",
- "modified_by": "Administrator",
- "module": "Regional",
- "name": "KSA POS Invoice",
- "owner": "Administrator",
- "page_number": "Hide",
- "print_format_builder": 0,
- "print_format_builder_beta": 0,
- "print_format_type": "Jinja",
- "raw_printing": 0,
- "show_section_headings": 0,
- "standard": "Yes"
-}
\ No newline at end of file
diff --git a/erpnext/regional/print_format/ksa_vat_invoice/__init__.py b/erpnext/regional/print_format/ksa_vat_invoice/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/regional/print_format/ksa_vat_invoice/__init__.py
+++ /dev/null
diff --git a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json
deleted file mode 100644
index 6b64d47..0000000
--- a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- "absolute_value": 0,
- "align_labels_right": 0,
- "creation": "2021-10-29 22:46:26.039023",
- "css": ".qr-code{\n float:right;\n}\n\n.invoice-heading {\n margin: 0;\n}\n\n.ksa-invoice-table {\n border: 1px solid #888a8e;\n border-collapse: collapse;\n width: 100%;\n margin: 20px 0;\n font-size: 16px;\n}\n\n.ksa-invoice-table.two-columns td:nth-child(2) {\n direction: rtl;\n}\n\n.ksa-invoice-table th {\n border: 1px solid #888a8e;\n max-width: 50%;\n padding: 8px;\n}\n\n.ksa-invoice-table td {\n padding: 5px;\n border: 1px solid #888a8e;\n max-width: 50%;\n}\n\n.ksa-invoice-table thead,\n.ksa-invoice-table tfoot {\n text-transform: uppercase;\n}\n\n.qr-rtl {\n direction: rtl;\n}\n\n.qr-flex{\n display: flex;\n justify-content: space-between;\n}",
- "custom_format": 1,
- "default_print_language": "en",
- "disabled": 1,
- "doc_type": "Sales Invoice",
- "docstatus": 0,
- "doctype": "Print Format",
- "font_size": 14,
- "html": "<div class=\"ksa-vat-format\">\n <div class=\"qr-flex\">\n <div style=\"qr-flex: 1\">\n <h2 class=\"invoice-heading\">TAX INVOICE</h2>\n <h2 class=\"invoice-heading\">\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629</h2>\n </div>\n \n <img class=\"qr-code\" src={{doc.ksa_einv_qr}}>\n </div>\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n <table class=\"ksa-invoice-table two-columns\">\n <thead>\n <tr>\n <th>{{ company.name }}</th>\n <th style=\"text-align: right;\">{{ company.company_name_in_arabic }}</th>\n </tr>\n </thead>\n\n <tbody>\n <!-- Invoice Info -->\n <tr>\n <td>Invoice#: {{doc.name}}</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}</td>\n </tr>\n <tr>\n <td>Invoice Date: {{doc.posting_date}}</td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}</td>\n </tr>\n <tr>\n <td>Date of Supply:{{doc.posting_date}}</td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}</td>\n </tr>\n \n <!--Supplier Info -->\n <tr>\n <td>Supplier:</td>\n <td>\u0627\u0644\u0645\u0648\u0631\u062f:</td>\n </tr>\n\t\t{% if (company.tax_id) %}\n <tr>\n <td>Supplier Tax Identification Number:</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:</td>\n </tr>\n <tr>\n <td>{{ company.tax_id }}</td>\n <td>{{ company.tax_id }}</td>\n </tr>\n {% endif %}\n <tr>\n <td>{{ company.name }}</td>\n <td>{{ company.company_name_in_arabic }} </td>\n </tr>\n \n \n {% if(supplier_address_doc) %}\n <tr>\n <td>{{ supplier_address_doc.address_line1}} </td>\n <td>{{ supplier_address_doc.address_in_arabic}} </td>\n </tr>\n <tr>\n <td>Phone: {{ supplier_address_doc.phone }}</td>\n <td>\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}</td>\n </tr>\n <tr>\n <td>Email: {{ supplier_address_doc.email_id }}</td>\n <td>\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}</td>\n </tr>\n {% endif %}\n \n <!-- Customer Info -->\n <tr>\n <td>CUSTOMER:</td>\n <td>\u0639\u0645\u064a\u0644:</td>\n </tr>\n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n <tr>\n <td>Customer Tax Identification Number:</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:</td>\n </tr>\n <tr>\n <td>{{ customer_tax_id }}</td>\n <td>{{ customer_tax_id }}</td>\n </tr>\n {% endif %}\n <tr>\n <td> {{ doc.customer }}</td>\n <td> {{ doc.customer_name_in_arabic }} </td>\n </tr>\n \n {% if(customer_address) %}\n <tr>\n <td>{{ customer_address.address_line1}} </td>\n <td>{{ customer_address.address_in_arabic}} </td>\n </tr>\n {% endif %}\n \n {% if(customer_shipping_address) %}\n <tr>\n <td>SHIPPING ADDRESS:</td>\n <td>\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:</td>\n </tr>\n \n <tr>\n <td>{{ customer_shipping_address.address_line1}} </td>\n <td>{{ customer_shipping_address.address_in_arabic}} </td>\n </tr>\n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n <tr>\n <td>OTHER INFORMATION</td>\n <td>\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649</td>\n </tr>\n \n <tr>\n <td>Purchase Order Number: {{ doc.po_no }}</td>\n <td>\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}</td>\n </tr>\n {% endif %}\n \n <tr>\n <td>Payment Due Date: {{ doc.due_date}} </td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}</td>\n </tr>\n </tbody>\n </table>\n\n <!--Dynamic Colspan for total row columns-->\n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n <!-- Items -->\n {% set total = namespace(amount = 0) %}\n <table class=\"ksa-invoice-table\">\n <thead>\n <tr>\n <th>Nature of goods or services <br />\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a</th>\n <th>\n Unit price <br />\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n </th>\n <th>\n Quantity <br />\n \u0627\u0644\u0643\u0645\u064a\u0629\n </th>\n <th>\n Taxable Amount <br />\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n </th>\n \n {% for row in doc.taxes %}\n <th style=\"min-width: 130px\">{{row.description}}</th>\n {% endfor %}\n \n <th>\n Total <br />\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n </th>\n </tr>\n </thead>\n <tbody>\n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n <tr>\n <td>{{ item.item_code or item.item_name }}</td>\n <td>{{ item.get_formatted(\"rate\") }}</td>\n <td>{{ item.qty }}</td>\n <td>{{ item.get_formatted(\"amount\") }}</td>\n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set key = item.item_code or item.item_name %}\n {% set tax_amount = frappe.utils.flt(data_object[key][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n <td>\n <div class=\"qr-flex\">\n {%- if(data_object[key][0])-%}\n <span>{{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}</span>\n {%- endif -%}\n <span>\n {%- if(data_object[key][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}</span>\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n </div>\n </td>\n {% endfor %}\n <td>{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}</td>\n </tr>\n {%- endfor -%}\n </tbody>\n <tfoot>\n <tr>\n <td>\n {{ doc.get_formatted(\"total\") }} <br />\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n </td>\n \n <td colspan={{ col.one }} class=\"qr-rtl\">\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n <br />\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n </td>\n <td colspan={{ col.two }}>\n Total (Excluding VAT)\n <br />\n Total VAT\n </td>\n <td>\n {{ doc.get_formatted(\"total\") }} <br />\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n </td>\n </tr>\n <tr>\n <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n <td colspan={{ col.one }} class=\"qr-rtl\">\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642</td>\n <td colspan={{ col.two }}>Total Amount Due</td>\n <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n </tr>\n </tfoot>\n </table>\n\n\t{%- if doc.terms -%}\n <p>\n {{doc.terms}}\n </p>\n\t{%- endif -%}\n</div>\n",
- "idx": 0,
- "line_breaks": 0,
- "margin_bottom": 15.0,
- "margin_left": 15.0,
- "margin_right": 15.0,
- "margin_top": 15.0,
- "modified": "2021-12-07 13:43:38.018593",
- "modified_by": "Administrator",
- "module": "Regional",
- "name": "KSA VAT Invoice",
- "owner": "Administrator",
- "page_number": "Hide",
- "print_format_builder": 0,
- "print_format_builder_beta": 0,
- "print_format_type": "Jinja",
- "raw_printing": 0,
- "show_section_headings": 0,
- "standard": "Yes"
-}
\ No newline at end of file
diff --git a/erpnext/regional/report/ksa_vat/__init__.py b/erpnext/regional/report/ksa_vat/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/regional/report/ksa_vat/__init__.py
+++ /dev/null
diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.js b/erpnext/regional/report/ksa_vat/ksa_vat.js
deleted file mode 100644
index 59e72c3..0000000
--- a/erpnext/regional/report/ksa_vat/ksa_vat.js
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (c) 2016, Havenir Solutions and contributors
-// For license information, please see license.txt
-/* eslint-disable */
-
-frappe.query_reports["KSA VAT"] = {
- onload() {
- frappe.breadcrumbs.add('Accounts');
- },
- "filters": [
- {
- "fieldname": "company",
- "label": __("Company"),
- "fieldtype": "Link",
- "options": "Company",
- "reqd": 1,
- "default": frappe.defaults.get_user_default("Company")
- },
- {
- "fieldname": "from_date",
- "label": __("From Date"),
- "fieldtype": "Date",
- "reqd": 1,
- "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1),
- },
- {
- "fieldname": "to_date",
- "label": __("To Date"),
- "fieldtype": "Date",
- "reqd": 1,
- "default": frappe.datetime.get_today()
- }
- ],
- "formatter": function(value, row, column, data, default_formatter) {
- if (data
- && (data.title=='VAT on Sales' || data.title=='VAT on Purchases')
- && data.title==value) {
- value = $(`<span>${value}</span>`);
- var $value = $(value).css("font-weight", "bold");
- value = $value.wrap("<p></p>").parent().html();
- return value
- }else if (data.title=='Grand Total'){
- if (data.title==value) {
- value = $(`<span>${value}</span>`);
- var $value = $(value).css("font-weight", "bold");
- value = $value.wrap("<p></p>").parent().html();
- return value
- }else{
- value = default_formatter(value, row, column, data);
- value = $(`<span>${value}</span>`);
- var $value = $(value).css("font-weight", "bold");
- value = $value.wrap("<p></p>").parent().html();
- return value
- }
- }else{
- value = default_formatter(value, row, column, data);
- return value;
- }
- },
-};
diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.json b/erpnext/regional/report/ksa_vat/ksa_vat.json
deleted file mode 100644
index 036e260..0000000
--- a/erpnext/regional/report/ksa_vat/ksa_vat.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- "add_total_row": 0,
- "columns": [],
- "creation": "2021-07-13 08:54:38.000949",
- "disable_prepared_report": 1,
- "disabled": 1,
- "docstatus": 0,
- "doctype": "Report",
- "filters": [],
- "idx": 0,
- "is_standard": "Yes",
- "modified": "2021-08-26 04:14:37.202594",
- "modified_by": "Administrator",
- "module": "Regional",
- "name": "KSA VAT",
- "owner": "Administrator",
- "prepared_report": 1,
- "ref_doctype": "GL Entry",
- "report_name": "KSA VAT",
- "report_type": "Script Report",
- "roles": [
- {
- "role": "System Manager"
- },
- {
- "role": "Accounts Manager"
- },
- {
- "role": "Accounts User"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.py b/erpnext/regional/report/ksa_vat/ksa_vat.py
deleted file mode 100644
index 15996d2..0000000
--- a/erpnext/regional/report/ksa_vat/ksa_vat.py
+++ /dev/null
@@ -1,231 +0,0 @@
-# Copyright (c) 2013, Havenir Solutions and contributors
-# For license information, please see license.txt
-
-
-import json
-
-import frappe
-from frappe import _
-from frappe.utils import get_url_to_list
-
-
-def execute(filters=None):
- columns = columns = get_columns()
- data = get_data(filters)
- return columns, data
-
-
-def get_columns():
- return [
- {
- "fieldname": "title",
- "label": _("Title"),
- "fieldtype": "Data",
- "width": 300,
- },
- {
- "fieldname": "amount",
- "label": _("Amount (SAR)"),
- "fieldtype": "Currency",
- "options": "currency",
- "width": 150,
- },
- {
- "fieldname": "adjustment_amount",
- "label": _("Adjustment (SAR)"),
- "fieldtype": "Currency",
- "options": "currency",
- "width": 150,
- },
- {
- "fieldname": "vat_amount",
- "label": _("VAT Amount (SAR)"),
- "fieldtype": "Currency",
- "options": "currency",
- "width": 150,
- },
- {
- "fieldname": "currency",
- "label": _("Currency"),
- "fieldtype": "Currency",
- "width": 150,
- "hidden": 1,
- },
- ]
-
-
-def get_data(filters):
- data = []
-
- # Validate if vat settings exist
- company = filters.get("company")
- company_currency = frappe.get_cached_value("Company", company, "default_currency")
-
- if frappe.db.exists("KSA VAT Setting", company) is None:
- url = get_url_to_list("KSA VAT Setting")
- frappe.msgprint(_('Create <a href="{}">KSA VAT Setting</a> for this company').format(url))
- return data
-
- ksa_vat_setting = frappe.get_doc("KSA VAT Setting", company)
-
- # Sales Heading
- append_data(data, "VAT on Sales", "", "", "", company_currency)
-
- grand_total_taxable_amount = 0
- grand_total_taxable_adjustment_amount = 0
- grand_total_tax = 0
-
- for vat_setting in ksa_vat_setting.ksa_vat_sales_accounts:
- (
- total_taxable_amount,
- total_taxable_adjustment_amount,
- total_tax,
- ) = get_tax_data_for_each_vat_setting(vat_setting, filters, "Sales Invoice")
-
- # Adding results to data
- append_data(
- data,
- vat_setting.title,
- total_taxable_amount,
- total_taxable_adjustment_amount,
- total_tax,
- company_currency,
- )
-
- grand_total_taxable_amount += total_taxable_amount
- grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
- grand_total_tax += total_tax
-
- # Sales Grand Total
- append_data(
- data,
- "Grand Total",
- grand_total_taxable_amount,
- grand_total_taxable_adjustment_amount,
- grand_total_tax,
- company_currency,
- )
-
- # Blank Line
- append_data(data, "", "", "", "", company_currency)
-
- # Purchase Heading
- append_data(data, "VAT on Purchases", "", "", "", company_currency)
-
- grand_total_taxable_amount = 0
- grand_total_taxable_adjustment_amount = 0
- grand_total_tax = 0
-
- for vat_setting in ksa_vat_setting.ksa_vat_purchase_accounts:
- (
- total_taxable_amount,
- total_taxable_adjustment_amount,
- total_tax,
- ) = get_tax_data_for_each_vat_setting(vat_setting, filters, "Purchase Invoice")
-
- # Adding results to data
- append_data(
- data,
- vat_setting.title,
- total_taxable_amount,
- total_taxable_adjustment_amount,
- total_tax,
- company_currency,
- )
-
- grand_total_taxable_amount += total_taxable_amount
- grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
- grand_total_tax += total_tax
-
- # Purchase Grand Total
- append_data(
- data,
- "Grand Total",
- grand_total_taxable_amount,
- grand_total_taxable_adjustment_amount,
- grand_total_tax,
- company_currency,
- )
-
- return data
-
-
-def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype):
- """
- (KSA, {filters}, 'Sales Invoice') => 500, 153, 10 \n
- calculates and returns \n
- total_taxable_amount, total_taxable_adjustment_amount, total_tax"""
- from_date = filters.get("from_date")
- to_date = filters.get("to_date")
-
- # Initiate variables
- total_taxable_amount = 0
- total_taxable_adjustment_amount = 0
- total_tax = 0
- # Fetch All Invoices
- invoices = frappe.get_all(
- doctype,
- filters={"docstatus": 1, "posting_date": ["between", [from_date, to_date]]},
- fields=["name", "is_return"],
- )
-
- for invoice in invoices:
- invoice_items = frappe.get_all(
- f"{doctype} Item",
- filters={
- "docstatus": 1,
- "parent": invoice.name,
- "item_tax_template": vat_setting.item_tax_template,
- },
- fields=["item_code", "net_amount"],
- )
-
- for item in invoice_items:
- # Summing up total taxable amount
- if invoice.is_return == 0:
- total_taxable_amount += item.net_amount
-
- if invoice.is_return == 1:
- total_taxable_adjustment_amount += item.net_amount
-
- # Summing up total tax
- total_tax += get_tax_amount(item.item_code, vat_setting.account, doctype, invoice.name)
-
- return total_taxable_amount, total_taxable_adjustment_amount, total_tax
-
-
-def append_data(data, title, amount, adjustment_amount, vat_amount, company_currency):
- """Returns data with appended value."""
- data.append(
- {
- "title": _(title),
- "amount": amount,
- "adjustment_amount": adjustment_amount,
- "vat_amount": vat_amount,
- "currency": company_currency,
- }
- )
-
-
-def get_tax_amount(item_code, account_head, doctype, parent):
- if doctype == "Sales Invoice":
- tax_doctype = "Sales Taxes and Charges"
-
- elif doctype == "Purchase Invoice":
- tax_doctype = "Purchase Taxes and Charges"
-
- item_wise_tax_detail = frappe.get_value(
- tax_doctype,
- {"docstatus": 1, "parent": parent, "account_head": account_head},
- "item_wise_tax_detail",
- )
-
- tax_amount = 0
- if item_wise_tax_detail and len(item_wise_tax_detail) > 0:
- item_wise_tax_detail = json.loads(item_wise_tax_detail)
- for key, value in item_wise_tax_detail.items():
- if key == item_code:
- tax_amount = value[1]
- break
-
- return tax_amount
diff --git a/erpnext/regional/saudi_arabia/__init__.py b/erpnext/regional/saudi_arabia/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/regional/saudi_arabia/__init__.py
+++ /dev/null
diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py
deleted file mode 100644
index 7f41c46..0000000
--- a/erpnext/regional/saudi_arabia/setup.py
+++ /dev/null
@@ -1,173 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-import frappe
-from frappe.permissions import add_permission, update_permission_property
-from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import (
- create_ksa_vat_setting,
-)
-from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
-
-
-def setup(company=None, patch=True):
- add_print_formats()
- add_permissions()
- make_custom_fields()
-
-
-def add_print_formats():
- frappe.reload_doc("regional", "print_format", "detailed_tax_invoice", force=True)
- frappe.reload_doc("regional", "print_format", "simplified_tax_invoice", force=True)
- frappe.reload_doc("regional", "print_format", "tax_invoice", force=True)
- frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True)
- frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True)
-
- for d in (
- "Simplified Tax Invoice",
- "Detailed Tax Invoice",
- "Tax Invoice",
- "KSA VAT Invoice",
- "KSA POS Invoice",
- ):
- frappe.db.set_value("Print Format", d, "disabled", 0)
-
-
-def add_permissions():
- """Add Permissions for KSA VAT Setting."""
- add_permission("KSA VAT Setting", "All", 0)
- for role in ("Accounts Manager", "Accounts User", "System Manager"):
- add_permission("KSA VAT Setting", role, 0)
- update_permission_property("KSA VAT Setting", role, 0, "write", 1)
- update_permission_property("KSA VAT Setting", role, 0, "create", 1)
-
- """Enable KSA VAT Report"""
- frappe.db.set_value("Report", "KSA VAT", "disabled", 0)
-
-
-def make_custom_fields():
- """Create Custom fields
- - QR code Image file
- - Company Name in Arabic
- - Address in Arabic
- """
- is_zero_rated = dict(
- fieldname="is_zero_rated",
- label="Is Zero Rated",
- fieldtype="Check",
- fetch_from="item_code.is_zero_rated",
- insert_after="description",
- print_hide=1,
- )
-
- is_exempt = dict(
- fieldname="is_exempt",
- label="Is Exempt",
- fieldtype="Check",
- fetch_from="item_code.is_exempt",
- insert_after="is_zero_rated",
- print_hide=1,
- )
-
- purchase_invoice_fields = [
- dict(
- fieldname="company_trn",
- label="Company TRN",
- fieldtype="Read Only",
- insert_after="shipping_address",
- fetch_from="company.tax_id",
- print_hide=1,
- ),
- dict(
- fieldname="supplier_name_in_arabic",
- label="Supplier Name in Arabic",
- fieldtype="Read Only",
- insert_after="supplier_name",
- fetch_from="supplier.supplier_name_in_arabic",
- print_hide=1,
- ),
- ]
-
- sales_invoice_fields = [
- dict(
- fieldname="company_trn",
- label="Company TRN",
- fieldtype="Read Only",
- insert_after="company_address",
- fetch_from="company.tax_id",
- print_hide=1,
- ),
- dict(
- fieldname="customer_name_in_arabic",
- label="Customer Name in Arabic",
- fieldtype="Read Only",
- insert_after="customer_name",
- fetch_from="customer.customer_name_in_arabic",
- print_hide=1,
- ),
- dict(
- fieldname="ksa_einv_qr",
- label="KSA E-Invoicing QR",
- fieldtype="Attach Image",
- read_only=1,
- no_copy=1,
- hidden=1,
- ),
- ]
-
- custom_fields = {
- "Item": [is_zero_rated, is_exempt],
- "Customer": [
- dict(
- fieldname="customer_name_in_arabic",
- label="Customer Name in Arabic",
- fieldtype="Data",
- insert_after="customer_name",
- ),
- ],
- "Supplier": [
- dict(
- fieldname="supplier_name_in_arabic",
- label="Supplier Name in Arabic",
- fieldtype="Data",
- insert_after="supplier_name",
- ),
- ],
- "Purchase Invoice": purchase_invoice_fields,
- "Purchase Order": purchase_invoice_fields,
- "Purchase Receipt": purchase_invoice_fields,
- "Sales Invoice": sales_invoice_fields,
- "POS Invoice": sales_invoice_fields,
- "Sales Order": sales_invoice_fields,
- "Delivery Note": sales_invoice_fields,
- "Sales Invoice Item": [is_zero_rated, is_exempt],
- "POS Invoice Item": [is_zero_rated, is_exempt],
- "Purchase Invoice Item": [is_zero_rated, is_exempt],
- "Sales Order Item": [is_zero_rated, is_exempt],
- "Delivery Note Item": [is_zero_rated, is_exempt],
- "Quotation Item": [is_zero_rated, is_exempt],
- "Purchase Order Item": [is_zero_rated, is_exempt],
- "Purchase Receipt Item": [is_zero_rated, is_exempt],
- "Supplier Quotation Item": [is_zero_rated, is_exempt],
- "Address": [
- dict(
- fieldname="address_in_arabic",
- label="Address in Arabic",
- fieldtype="Data",
- insert_after="address_line2",
- )
- ],
- "Company": [
- dict(
- fieldname="company_name_in_arabic",
- label="Company Name In Arabic",
- fieldtype="Data",
- insert_after="company_name",
- )
- ],
- }
-
- create_custom_fields(custom_fields, ignore_validate=True, update=True)
-
-
-def update_regional_tax_settings(country, company):
- create_ksa_vat_setting(company)
diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py
deleted file mode 100644
index cac5ec1..0000000
--- a/erpnext/regional/saudi_arabia/utils.py
+++ /dev/null
@@ -1,169 +0,0 @@
-import io
-import os
-from base64 import b64encode
-
-import frappe
-from frappe import _
-from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
-from frappe.utils.data import add_to_date, get_time, getdate
-from pyqrcode import create as qr_create
-
-from erpnext import get_region
-
-
-def create_qr_code(doc, method=None):
- region = get_region(doc.company)
- if region not in ["Saudi Arabia"]:
- return
-
- # if QR Code field not present, create it. Invoices without QR are invalid as per law.
- if not hasattr(doc, "ksa_einv_qr"):
- create_custom_fields(
- {
- doc.doctype: [
- dict(
- fieldname="ksa_einv_qr",
- label="KSA E-Invoicing QR",
- fieldtype="Attach Image",
- read_only=1,
- no_copy=1,
- hidden=1,
- )
- ]
- }
- )
-
- # Don't create QR Code if it already exists
- qr_code = doc.get("ksa_einv_qr")
- if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}):
- return
-
- meta = frappe.get_meta(doc.doctype)
-
- if "ksa_einv_qr" in [d.fieldname for d in meta.get_image_fields()]:
- """TLV conversion for
- 1. Seller's Name
- 2. VAT Number
- 3. Time Stamp
- 4. Invoice Amount
- 5. VAT Amount
- """
- tlv_array = []
- # Sellers Name
-
- seller_name = frappe.db.get_value("Company", doc.company, "company_name_in_arabic")
-
- if not seller_name:
- frappe.throw(_("Arabic name missing for {} in the company document").format(doc.company))
-
- tag = bytes([1]).hex()
- length = bytes([len(seller_name.encode("utf-8"))]).hex()
- value = seller_name.encode("utf-8").hex()
- tlv_array.append("".join([tag, length, value]))
-
- # VAT Number
- tax_id = frappe.db.get_value("Company", doc.company, "tax_id")
- if not tax_id:
- frappe.throw(_("Tax ID missing for {} in the company document").format(doc.company))
-
- tag = bytes([2]).hex()
- length = bytes([len(tax_id)]).hex()
- value = tax_id.encode("utf-8").hex()
- tlv_array.append("".join([tag, length, value]))
-
- # Time Stamp
- posting_date = getdate(doc.posting_date)
- time = get_time(doc.posting_time)
- seconds = time.hour * 60 * 60 + time.minute * 60 + time.second
- time_stamp = add_to_date(posting_date, seconds=seconds)
- time_stamp = time_stamp.strftime("%Y-%m-%dT%H:%M:%SZ")
-
- tag = bytes([3]).hex()
- length = bytes([len(time_stamp)]).hex()
- value = time_stamp.encode("utf-8").hex()
- tlv_array.append("".join([tag, length, value]))
-
- # Invoice Amount
- invoice_amount = str(doc.base_grand_total)
- tag = bytes([4]).hex()
- length = bytes([len(invoice_amount)]).hex()
- value = invoice_amount.encode("utf-8").hex()
- tlv_array.append("".join([tag, length, value]))
-
- # VAT Amount
- vat_amount = str(get_vat_amount(doc))
-
- tag = bytes([5]).hex()
- length = bytes([len(vat_amount)]).hex()
- value = vat_amount.encode("utf-8").hex()
- tlv_array.append("".join([tag, length, value]))
-
- # Joining bytes into one
- tlv_buff = "".join(tlv_array)
-
- # base64 conversion for QR Code
- base64_string = b64encode(bytes.fromhex(tlv_buff)).decode()
-
- qr_image = io.BytesIO()
- url = qr_create(base64_string, error="L")
- url.png(qr_image, scale=2, quiet_zone=1)
-
- name = frappe.generate_hash(doc.name, 5)
-
- # making file
- filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
- _file = frappe.get_doc(
- {
- "doctype": "File",
- "file_name": filename,
- "is_private": 0,
- "content": qr_image.getvalue(),
- "attached_to_doctype": doc.get("doctype"),
- "attached_to_name": doc.get("name"),
- "attached_to_field": "ksa_einv_qr",
- }
- )
-
- _file.save()
-
- # assigning to document
- doc.db_set("ksa_einv_qr", _file.file_url)
- doc.notify_update()
-
-
-def get_vat_amount(doc):
- vat_settings = frappe.db.get_value("KSA VAT Setting", {"company": doc.company})
- vat_accounts = []
- vat_amount = 0
-
- if vat_settings:
- vat_settings_doc = frappe.get_cached_doc("KSA VAT Setting", vat_settings)
-
- for row in vat_settings_doc.get("ksa_vat_sales_accounts"):
- vat_accounts.append(row.account)
-
- for tax in doc.get("taxes"):
- if tax.account_head in vat_accounts:
- vat_amount += tax.base_tax_amount
-
- return vat_amount
-
-
-def delete_qr_code_file(doc, method=None):
- region = get_region(doc.company)
- if region not in ["Saudi Arabia"]:
- return
-
- if hasattr(doc, "ksa_einv_qr"):
- if doc.get("ksa_einv_qr"):
- file_doc = frappe.get_list("File", {"file_url": doc.get("ksa_einv_qr")})
- if len(file_doc):
- frappe.delete_doc("File", file_doc[0].name)
-
-
-def delete_vat_settings_for_company(doc, method=None):
- if doc.country != "Saudi Arabia":
- return
-
- if frappe.db.exists("KSA VAT Setting", doc.name):
- frappe.delete_doc("KSA VAT Setting", doc.name)
diff --git a/erpnext/regional/saudi_arabia/wizard/__init__.py b/erpnext/regional/saudi_arabia/wizard/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/regional/saudi_arabia/wizard/__init__.py
+++ /dev/null
diff --git a/erpnext/regional/saudi_arabia/wizard/data/__init__.py b/erpnext/regional/saudi_arabia/wizard/data/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/regional/saudi_arabia/wizard/data/__init__.py
+++ /dev/null
diff --git a/erpnext/regional/saudi_arabia/wizard/data/ksa_vat_settings.json b/erpnext/regional/saudi_arabia/wizard/data/ksa_vat_settings.json
deleted file mode 100644
index 60951a9..0000000
--- a/erpnext/regional/saudi_arabia/wizard/data/ksa_vat_settings.json
+++ /dev/null
@@ -1,47 +0,0 @@
-[
- {
- "type": "Sales Account",
- "accounts": [
- {
- "title": "Standard rated Sales",
- "item_tax_template": "KSA VAT 5%",
- "account": "VAT 5%"
- },
- {
- "title": "Zero rated domestic sales",
- "item_tax_template": "KSA VAT Zero",
- "account": "VAT Zero"
- },
- {
- "title": "Exempted sales",
- "item_tax_template": "KSA VAT Exempted",
- "account": "VAT Exempted"
- }
- ]
- },
- {
- "type": "Purchase Account",
- "accounts": [
- {
- "title": "Standard rated domestic purchases",
- "item_tax_template": "KSA VAT 5%",
- "account": "VAT 5%"
- },
- {
- "title": "Imports subject to VAT paid at customs",
- "item_tax_template": "KSA Excise 50%",
- "account": "Excise 50%"
- },
- {
- "title": "Zero rated purchases",
- "item_tax_template": "KSA VAT Zero",
- "account": "VAT Zero"
- },
- {
- "title": "Exempted purchases",
- "item_tax_template": "KSA VAT Exempted",
- "account": "VAT Exempted"
- }
- ]
- }
-]
\ No newline at end of file
diff --git a/erpnext/regional/saudi_arabia/wizard/operations/__init__.py b/erpnext/regional/saudi_arabia/wizard/operations/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/regional/saudi_arabia/wizard/operations/__init__.py
+++ /dev/null
diff --git a/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py b/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py
deleted file mode 100644
index 66d9df2..0000000
--- a/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py
+++ /dev/null
@@ -1,46 +0,0 @@
-import json
-import os
-
-import frappe
-
-
-def create_ksa_vat_setting(company):
- """On creation of first company. Creates KSA VAT Setting"""
-
- company = frappe.get_doc("Company", company)
-
- file_path = os.path.join(os.path.dirname(__file__), "..", "data", "ksa_vat_settings.json")
- with open(file_path, "r") as json_file:
- account_data = json.load(json_file)
-
- # Creating KSA VAT Setting
- ksa_vat_setting = frappe.get_doc({"doctype": "KSA VAT Setting", "company": company.name})
-
- for data in account_data:
- if data["type"] == "Sales Account":
- for row in data["accounts"]:
- item_tax_template = row["item_tax_template"]
- account = row["account"]
- ksa_vat_setting.append(
- "ksa_vat_sales_accounts",
- {
- "title": row["title"],
- "item_tax_template": f"{item_tax_template} - {company.abbr}",
- "account": f"{account} - {company.abbr}",
- },
- )
-
- elif data["type"] == "Purchase Account":
- for row in data["accounts"]:
- item_tax_template = row["item_tax_template"]
- account = row["account"]
- ksa_vat_setting.append(
- "ksa_vat_purchase_accounts",
- {
- "title": row["title"],
- "item_tax_template": f"{item_tax_template} - {company.abbr}",
- "account": f"{account} - {company.abbr}",
- },
- )
-
- ksa_vat_setting.save()
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index 7482a33..c133cd3 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -24,10 +24,10 @@
"account_manager",
"image",
"defaults_tab",
- "default_price_list",
+ "default_currency",
"default_bank_account",
"column_break_14",
- "default_currency",
+ "default_price_list",
"internal_customer_section",
"is_internal_customer",
"represents_company",
@@ -568,11 +568,10 @@
"link_fieldname": "party"
}
],
- "modified": "2022-11-08 15:52:34.462657",
+ "modified": "2023-02-18 11:04:46.343527",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
- "name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
diff --git a/erpnext/selling/doctype/industry_type/industry_type.json b/erpnext/selling/doctype/industry_type/industry_type.json
index 6c49f0f..3c8ab8e 100644
--- a/erpnext/selling/doctype/industry_type/industry_type.json
+++ b/erpnext/selling/doctype/industry_type/industry_type.json
@@ -1,123 +1,68 @@
{
- "allow_copy": 0,
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "field:industry",
- "beta": 0,
- "creation": "2012-03-27 14:36:09",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 0,
+ "actions": [],
+ "allow_import": 1,
+ "allow_rename": 1,
+ "autoname": "field:industry",
+ "creation": "2012-03-27 14:36:09",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "industry"
+ ],
"fields": [
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "industry",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Industry",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "industry",
- "oldfieldtype": "Data",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "industry",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Industry",
+ "oldfieldname": "industry",
+ "oldfieldtype": "Data",
+ "reqd": 1,
+ "unique": 1
}
- ],
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-flag",
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
-
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2020-09-18 17:26:09.703215",
- "modified_by": "Administrator",
- "module": "Selling",
- "name": "Industry Type",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-flag",
+ "idx": 1,
+ "links": [],
+ "modified": "2023-02-10 03:14:40.735763",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "Industry Type",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 0,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Sales Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Sales User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
- },
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales User"
+ },
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 0,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Sales Master Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales Master Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "track_seen": 0
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "translated_doctype": 1
}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/party_specific_item/party_specific_item.json b/erpnext/selling/doctype/party_specific_item/party_specific_item.json
index 32b5d47..a1f9902 100644
--- a/erpnext/selling/doctype/party_specific_item/party_specific_item.json
+++ b/erpnext/selling/doctype/party_specific_item/party_specific_item.json
@@ -1,5 +1,6 @@
{
"actions": [],
+ "allow_import": 1,
"creation": "2021-08-27 19:28:07.559978",
"doctype": "DocType",
"editable_grid": 1,
@@ -51,7 +52,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-09-14 13:27:58.612334",
+ "modified": "2023-02-15 13:00:50.379713",
"modified_by": "Administrator",
"module": "Selling",
"name": "Party Specific Item",
@@ -72,6 +73,7 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "party",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js
index b348bd3..81ef44d 100644
--- a/erpnext/selling/doctype/quotation/quotation.js
+++ b/erpnext/selling/doctype/quotation/quotation.js
@@ -90,7 +90,7 @@
|| frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) {
this.frm.add_custom_button(
__("Sales Order"),
- this.frm.cscript["Make Sales Order"],
+ () => this.make_sales_order(),
__("Create")
);
}
@@ -145,6 +145,20 @@
}
+ make_sales_order() {
+ var me = this;
+
+ let has_alternative_item = this.frm.doc.items.some((item) => item.is_alternative);
+ if (has_alternative_item) {
+ this.show_alternative_items_dialog();
+ } else {
+ frappe.model.open_mapped_doc({
+ method: "erpnext.selling.doctype.quotation.quotation.make_sales_order",
+ frm: me.frm
+ });
+ }
+ }
+
set_dynamic_field_label(){
if (this.frm.doc.quotation_to == "Customer")
{
@@ -220,17 +234,111 @@
}
})
}
+
+ show_alternative_items_dialog() {
+ let me = this;
+
+ const table_fields = [
+ {
+ fieldtype:"Data",
+ fieldname:"name",
+ label: __("Name"),
+ read_only: 1,
+ },
+ {
+ fieldtype:"Link",
+ fieldname:"item_code",
+ options: "Item",
+ label: __("Item Code"),
+ read_only: 1,
+ in_list_view: 1,
+ columns: 2,
+ formatter: (value, df, options, doc) => {
+ return doc.is_alternative ? `<span class="indicator yellow">${value}</span>` : value;
+ }
+ },
+ {
+ fieldtype:"Data",
+ fieldname:"description",
+ label: __("Description"),
+ in_list_view: 1,
+ read_only: 1,
+ },
+ {
+ fieldtype:"Currency",
+ fieldname:"amount",
+ label: __("Amount"),
+ options: "currency",
+ in_list_view: 1,
+ read_only: 1,
+ },
+ {
+ fieldtype:"Check",
+ fieldname:"is_alternative",
+ label: __("Is Alternative"),
+ read_only: 1,
+ }];
+
+
+ this.data = this.frm.doc.items.filter(
+ (item) => item.is_alternative || item.has_alternative_item
+ ).map((item) => {
+ return {
+ "name": item.name,
+ "item_code": item.item_code,
+ "description": item.description,
+ "amount": item.amount,
+ "is_alternative": item.is_alternative,
+ }
+ });
+
+ const dialog = new frappe.ui.Dialog({
+ title: __("Select Alternative Items for Sales Order"),
+ fields: [
+ {
+ fieldname: "info",
+ fieldtype: "HTML",
+ read_only: 1
+ },
+ {
+ fieldname: "alternative_items",
+ fieldtype: "Table",
+ cannot_add_rows: true,
+ in_place_edit: true,
+ reqd: 1,
+ data: this.data,
+ description: __("Select an item from each set to be used in the Sales Order."),
+ get_data: () => {
+ return this.data;
+ },
+ fields: table_fields
+ },
+ ],
+ primary_action: function() {
+ frappe.model.open_mapped_doc({
+ method: "erpnext.selling.doctype.quotation.quotation.make_sales_order",
+ frm: me.frm,
+ args: {
+ selected_items: dialog.fields_dict.alternative_items.grid.get_selected_children()
+ }
+ });
+ dialog.hide();
+ },
+ primary_action_label: __('Continue')
+ });
+
+ dialog.fields_dict.info.$wrapper.html(
+ `<p class="small text-muted">
+ <span class="indicator yellow"></span>
+ Alternative Items
+ </p>`
+ )
+ dialog.show();
+ }
};
cur_frm.script_manager.make(erpnext.selling.QuotationController);
-cur_frm.cscript['Make Sales Order'] = function() {
- frappe.model.open_mapped_doc({
- method: "erpnext.selling.doctype.quotation.quotation.make_sales_order",
- frm: cur_frm
- })
-}
-
frappe.ui.form.on("Quotation Item", "items_on_form_rendered", "packed_items_on_form_rendered", function(frm, cdt, cdn) {
// enable tax_amount field if Actual
})
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index 063813b..fc66db2 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -35,6 +35,9 @@
make_packing_list(self)
+ def before_submit(self):
+ self.set_has_alternative_item()
+
def validate_valid_till(self):
if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date):
frappe.throw(_("Valid till date cannot be before transaction date"))
@@ -59,7 +62,18 @@
title=_("Unpublished Item"),
)
+ def set_has_alternative_item(self):
+ """Mark 'Has Alternative Item' for rows."""
+ if not any(row.is_alternative for row in self.get("items")):
+ return
+
+ items_with_alternatives = self.get_rows_with_alternatives()
+ for row in self.get("items"):
+ if not row.is_alternative and row.name in items_with_alternatives:
+ row.has_alternative_item = 1
+
def get_ordered_status(self):
+ status = "Open"
ordered_items = frappe._dict(
frappe.db.get_all(
"Sales Order Item",
@@ -70,16 +84,40 @@
)
)
- status = "Open"
- if ordered_items:
+ if not ordered_items:
+ return status
+
+ has_alternatives = any(row.is_alternative for row in self.get("items"))
+ self._items = self.get_valid_items() if has_alternatives else self.get("items")
+
+ if any(row.qty > ordered_items.get(row.item_code, 0.0) for row in self._items):
+ status = "Partially Ordered"
+ else:
status = "Ordered"
- for item in self.get("items"):
- if item.qty > ordered_items.get(item.item_code, 0.0):
- status = "Partially Ordered"
-
return status
+ def get_valid_items(self):
+ """
+ Filters out items in an alternatives set that were not ordered.
+ """
+
+ def is_in_sales_order(row):
+ in_sales_order = bool(
+ frappe.db.exists(
+ "Sales Order Item", {"quotation_item": row.name, "item_code": row.item_code, "docstatus": 1}
+ )
+ )
+ return in_sales_order
+
+ def can_map(row) -> bool:
+ if row.is_alternative or row.has_alternative_item:
+ return is_in_sales_order(row)
+
+ return True
+
+ return list(filter(can_map, self.get("items")))
+
def is_fully_ordered(self):
return self.get_ordered_status() == "Ordered"
@@ -176,6 +214,22 @@
def on_recurring(self, reference_doc, auto_repeat_doc):
self.valid_till = None
+ def get_rows_with_alternatives(self):
+ rows_with_alternatives = []
+ table_length = len(self.get("items"))
+
+ for idx, row in enumerate(self.get("items")):
+ if row.is_alternative:
+ continue
+
+ if idx == (table_length - 1):
+ break
+
+ if self.get("items")[idx + 1].is_alternative:
+ rows_with_alternatives.append(row.name)
+
+ return rows_with_alternatives
+
def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context
@@ -221,6 +275,8 @@
)
)
+ selected_rows = [x.get("name") for x in frappe.flags.get("args", {}).get("selected_items", [])]
+
def set_missing_values(source, target):
if customer:
target.customer = customer.name
@@ -244,6 +300,24 @@
target.blanket_order = obj.blanket_order
target.blanket_order_rate = obj.blanket_order_rate
+ def can_map_row(item) -> bool:
+ """
+ Row mapping from Quotation to Sales order:
+ 1. If no selections, map all non-alternative rows (that sum up to the grand total)
+ 2. If selections: Is Alternative Item/Has Alternative Item: Map if selected and adequate qty
+ 3. If selections: Simple row: Map if adequate qty
+ """
+ has_qty = item.qty > 0
+
+ if not selected_rows:
+ return not item.is_alternative
+
+ if selected_rows and (item.is_alternative or item.has_alternative_item):
+ return (item.name in selected_rows) and has_qty
+
+ # Simple row
+ return has_qty
+
doclist = get_mapped_doc(
"Quotation",
source_name,
@@ -253,7 +327,7 @@
"doctype": "Sales Order Item",
"field_map": {"parent": "prevdoc_docname", "name": "quotation_item"},
"postprocess": update_item,
- "condition": lambda doc: doc.qty > 0,
+ "condition": can_map_row,
},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
@@ -322,7 +396,11 @@
source_name,
{
"Quotation": {"doctype": "Sales Invoice", "validation": {"docstatus": ["=", 1]}},
- "Quotation Item": {"doctype": "Sales Invoice Item", "postprocess": update_item},
+ "Quotation Item": {
+ "doctype": "Sales Invoice Item",
+ "postprocess": update_item,
+ "condition": lambda row: not row.is_alternative,
+ },
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
},
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index cdf5f5d..67f6518 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -457,6 +457,139 @@
expected_index = id + 1
self.assertEqual(item.idx, expected_index)
+ def test_alternative_items_with_stock_items(self):
+ """
+ Check if taxes & totals considers only non-alternative items with:
+ - One set of non-alternative & alternative items [first 3 rows]
+ - One simple stock item
+ """
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ item_list = []
+ stock_items = {
+ "_Test Simple Item 1": 100,
+ "_Test Alt 1": 120,
+ "_Test Alt 2": 110,
+ "_Test Simple Item 2": 200,
+ }
+
+ for item, rate in stock_items.items():
+ make_item(item, {"is_stock_item": 1})
+ item_list.append(
+ {
+ "item_code": item,
+ "qty": 1,
+ "rate": rate,
+ "is_alternative": bool("Alt" in item),
+ }
+ )
+
+ quotation = make_quotation(item_list=item_list, do_not_submit=1)
+ quotation.append(
+ "taxes",
+ {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 10,
+ },
+ )
+ quotation.submit()
+
+ self.assertEqual(quotation.net_total, 300)
+ self.assertEqual(quotation.grand_total, 330)
+
+ def test_alternative_items_with_service_items(self):
+ """
+ Check if taxes & totals considers only non-alternative items with:
+ - One set of non-alternative & alternative service items [first 3 rows]
+ - One simple non-alternative service item
+ All having the same item code and unique item name/description due to
+ dynamic services
+ """
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ item_list = []
+ service_items = {
+ "Tiling with Standard Tiles": 100,
+ "Alt Tiling with Durable Tiles": 150,
+ "Alt Tiling with Premium Tiles": 180,
+ "False Ceiling with Material #234": 190,
+ }
+
+ make_item("_Test Dynamic Service Item", {"is_stock_item": 0})
+
+ for name, rate in service_items.items():
+ item_list.append(
+ {
+ "item_code": "_Test Dynamic Service Item",
+ "item_name": name,
+ "description": name,
+ "qty": 1,
+ "rate": rate,
+ "is_alternative": bool("Alt" in name),
+ }
+ )
+
+ quotation = make_quotation(item_list=item_list, do_not_submit=1)
+ quotation.append(
+ "taxes",
+ {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 10,
+ },
+ )
+ quotation.submit()
+
+ self.assertEqual(quotation.net_total, 290)
+ self.assertEqual(quotation.grand_total, 319)
+
+ def test_alternative_items_sales_order_mapping_with_stock_items(self):
+ from erpnext.selling.doctype.quotation.quotation import make_sales_order
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ frappe.flags.args = frappe._dict()
+ item_list = []
+ stock_items = {
+ "_Test Simple Item 1": 100,
+ "_Test Alt 1": 120,
+ "_Test Alt 2": 110,
+ "_Test Simple Item 2": 200,
+ }
+
+ for item, rate in stock_items.items():
+ make_item(item, {"is_stock_item": 1})
+ item_list.append(
+ {
+ "item_code": item,
+ "qty": 1,
+ "rate": rate,
+ "is_alternative": bool("Alt" in item),
+ "warehouse": "_Test Warehouse - _TC",
+ }
+ )
+
+ quotation = make_quotation(item_list=item_list)
+
+ frappe.flags.args.selected_items = [quotation.items[2]]
+ sales_order = make_sales_order(quotation.name)
+ sales_order.delivery_date = add_days(sales_order.transaction_date, 10)
+ sales_order.save()
+
+ self.assertEqual(sales_order.items[0].item_code, "_Test Alt 2")
+ self.assertEqual(sales_order.items[1].item_code, "_Test Simple Item 2")
+ self.assertEqual(sales_order.net_total, 310)
+
+ sales_order.submit()
+ quotation.reload()
+ self.assertEqual(quotation.status, "Ordered")
+
test_records = frappe.get_test_records("Quotation")
diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json
index ca7dfd2..f2aabc5 100644
--- a/erpnext/selling/doctype/quotation_item/quotation_item.json
+++ b/erpnext/selling/doctype/quotation_item/quotation_item.json
@@ -49,6 +49,8 @@
"pricing_rules",
"stock_uom_rate",
"is_free_item",
+ "is_alternative",
+ "has_alternative_item",
"section_break_43",
"valuation_rate",
"column_break_45",
@@ -643,12 +645,28 @@
"no_copy": 1,
"options": "currency",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "is_alternative",
+ "fieldtype": "Check",
+ "label": "Is Alternative",
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "has_alternative_item",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Has Alternative Item",
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2022-12-25 02:49:53.926625",
+ "modified": "2023-02-06 11:00:07.042364",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation Item",
@@ -656,5 +674,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index fb64772..449d461 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -275,7 +275,7 @@
if (this.frm.doc.docstatus===0) {
this.frm.add_custom_button(__('Quotation'),
function() {
- erpnext.utils.map_current_doc({
+ let d = erpnext.utils.map_current_doc({
method: "erpnext.selling.doctype.quotation.quotation.make_sales_order",
source_doctype: "Quotation",
target: me.frm,
@@ -293,7 +293,16 @@
docstatus: 1,
status: ["!=", "Lost"]
}
- })
+ });
+
+ setTimeout(() => {
+ d.$parent.append(`
+ <span class='small text-muted'>
+ ${__("Note: Please create Sales Orders from individual Quotations to select from among Alternative Items.")}
+ </span>
+ `);
+ }, 200);
+
}, __("Get Items From"));
}
@@ -309,9 +318,12 @@
make_work_order() {
var me = this;
- this.frm.call({
- doc: this.frm.doc,
- method: 'get_work_order_items',
+ me.frm.call({
+ method: "erpnext.selling.doctype.sales_order.sales_order.get_work_order_items",
+ args: {
+ sales_order: this.frm.docname,
+ },
+ freeze: true,
callback: function(r) {
if(!r.message) {
frappe.msgprint({
@@ -321,14 +333,7 @@
});
return;
}
- else if(!r.message) {
- frappe.msgprint({
- title: __('Work Order not created'),
- message: __('Work Order already created for all items with BOM'),
- indicator: 'orange'
- });
- return;
- } else {
+ else {
const fields = [{
label: 'Items',
fieldtype: 'Table',
@@ -429,9 +434,9 @@
make_raw_material_request() {
var me = this;
this.frm.call({
- doc: this.frm.doc,
- method: 'get_work_order_items',
+ method: "erpnext.selling.doctype.sales_order.sales_order.get_work_order_items",
args: {
+ sales_order: this.frm.docname,
for_raw_material_request: 1
},
callback: function(r) {
@@ -450,6 +455,7 @@
}
make_raw_material_request_dialog(r) {
+ var me = this;
var fields = [
{fieldtype:'Check', fieldname:'include_exploded_items',
label: __('Include Exploded Items')},
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index ca6a51a..385d0f3 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -6,11 +6,12 @@
import frappe
import frappe.utils
-from frappe import _
+from frappe import _, qb
from frappe.contacts.doctype.address.address import get_company_address
from frappe.desk.notifications import clear_doctype_notifications
from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values
+from frappe.query_builder.functions import Sum
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, nowdate, strip_html
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
@@ -414,51 +415,6 @@
self.indicator_color = "green"
self.indicator_title = _("Paid")
- @frappe.whitelist()
- def get_work_order_items(self, for_raw_material_request=0):
- """Returns items with BOM that already do not have a linked work order"""
- items = []
- item_codes = [i.item_code for i in self.items]
- product_bundle_parents = [
- pb.new_item_code
- for pb in frappe.get_all(
- "Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"]
- )
- ]
-
- for table in [self.items, self.packed_items]:
- for i in table:
- bom = get_default_bom(i.item_code)
- stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty
-
- if not for_raw_material_request:
- total_work_order_qty = flt(
- frappe.db.sql(
- """select sum(qty) from `tabWork Order`
- where production_item=%s and sales_order=%s and sales_order_item = %s and docstatus<2""",
- (i.item_code, self.name, i.name),
- )[0][0]
- )
- pending_qty = stock_qty - total_work_order_qty
- else:
- pending_qty = stock_qty
-
- if pending_qty and i.item_code not in product_bundle_parents:
- items.append(
- dict(
- name=i.name,
- item_code=i.item_code,
- description=i.description,
- bom=bom or "",
- warehouse=i.warehouse,
- pending_qty=pending_qty,
- required_qty=pending_qty if for_raw_material_request else 0,
- sales_order_item=i.name,
- )
- )
-
- return items
-
def on_recurring(self, reference_doc, auto_repeat_doc):
def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date):
delivery_date = auto_repeat_doc.get_next_schedule_date(schedule_date=ref_doc_delivery_date)
@@ -1350,3 +1306,57 @@
return
frappe.db.set_value("Sales Order Item", sales_order_item, "produced_qty", total_produced_qty)
+
+
+@frappe.whitelist()
+def get_work_order_items(sales_order, for_raw_material_request=0):
+ """Returns items with BOM that already do not have a linked work order"""
+ if sales_order:
+ so = frappe.get_doc("Sales Order", sales_order)
+
+ wo = qb.DocType("Work Order")
+
+ items = []
+ item_codes = [i.item_code for i in so.items]
+ product_bundle_parents = [
+ pb.new_item_code
+ for pb in frappe.get_all(
+ "Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"]
+ )
+ ]
+
+ for table in [so.items, so.packed_items]:
+ for i in table:
+ bom = get_default_bom(i.item_code)
+ stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty
+
+ if not for_raw_material_request:
+ total_work_order_qty = flt(
+ qb.from_(wo)
+ .select(Sum(wo.qty))
+ .where(
+ (wo.production_item == i.item_code)
+ & (wo.sales_order == so.name) * (wo.sales_order_item == i.name)
+ & (wo.docstatus.lte(2))
+ )
+ .run()[0][0]
+ )
+ pending_qty = stock_qty - total_work_order_qty
+ else:
+ pending_qty = stock_qty
+
+ if pending_qty and i.item_code not in product_bundle_parents:
+ items.append(
+ dict(
+ name=i.name,
+ item_code=i.item_code,
+ description=i.description,
+ bom=bom or "",
+ warehouse=i.warehouse,
+ pending_qty=pending_qty,
+ required_qty=pending_qty if for_raw_material_request else 0,
+ sales_order_item=i.name,
+ )
+ )
+
+ return items
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index d4d7c58..627914f 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -1217,6 +1217,8 @@
self.assertTrue(si.get("payment_schedule"))
def test_make_work_order(self):
+ from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
+
# Make a new Sales Order
so = make_sales_order(
**{
@@ -1230,7 +1232,7 @@
# Raise Work Orders
po_items = []
so_item_name = {}
- for item in so.get_work_order_items():
+ for item in get_work_order_items(so.name):
po_items.append(
{
"warehouse": item.get("warehouse"),
@@ -1448,6 +1450,7 @@
from erpnext.controllers.item_variant import create_variant
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+ from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
make_item( # template item
"Test-WO-Tshirt",
@@ -1487,7 +1490,7 @@
]
}
)
- wo_items = so.get_work_order_items()
+ wo_items = get_work_order_items(so.name)
self.assertEqual(wo_items[0].get("item_code"), "Test-WO-Tshirt-R")
self.assertEqual(wo_items[0].get("bom"), red_var_bom.name)
@@ -1497,6 +1500,8 @@
self.assertEqual(wo_items[1].get("bom"), template_bom.name)
def test_request_for_raw_materials(self):
+ from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
+
item = make_item(
"_Test Finished Item",
{
@@ -1529,7 +1534,7 @@
so = make_sales_order(**{"item_list": [{"item_code": item.item_code, "qty": 1, "rate": 1000}]})
so.submit()
mr_dict = frappe._dict()
- items = so.get_work_order_items(1)
+ items = get_work_order_items(so.name, 1)
mr_dict["items"] = items
mr_dict["include_exploded_items"] = 0
mr_dict["ignore_existing_ordered_qty"] = 1
diff --git a/erpnext/selling/doctype/sales_partner_type/sales_partner_type.json b/erpnext/selling/doctype/sales_partner_type/sales_partner_type.json
index e7dd0d8..a9b500a 100644
--- a/erpnext/selling/doctype/sales_partner_type/sales_partner_type.json
+++ b/erpnext/selling/doctype/sales_partner_type/sales_partner_type.json
@@ -1,94 +1,47 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:sales_partner_type",
- "beta": 0,
- "creation": "2018-06-11 13:15:57.404716",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "autoname": "field:sales_partner_type",
+ "creation": "2018-06-11 13:15:57.404716",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "sales_partner_type"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "sales_partner_type",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Sales Partner Type",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "sales_partner_type",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Sales Partner Type",
+ "reqd": 1,
+ "unique": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-06-11 13:45:13.554307",
- "modified_by": "Administrator",
- "module": "Selling",
- "name": "Sales Partner Type",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "links": [],
+ "modified": "2023-02-10 01:00:20.110800",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "Sales Partner Type",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "translated_doctype": 1
}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index c442774..46320e5 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -522,7 +522,7 @@
const from_selector = field === 'qty' && value === "+1";
if (from_selector)
- value = flt(item_row.qty) + flt(value);
+ value = flt(item_row.stock_qty) + flt(value);
if (item_row_exists) {
if (field === 'qty')
diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
index 44c4d54..2624db3 100644
--- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
+++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
@@ -216,7 +216,7 @@
)
if filters.get("item_group"):
- query = query.where(db_so_item.item_group == frappe.db.escape(filters.item_group))
+ query = query.where(db_so_item.item_group == filters.item_group)
if filters.get("from_date"):
query = query.where(db_so.transaction_date >= filters.from_date)
@@ -225,7 +225,7 @@
query = query.where(db_so.transaction_date <= filters.to_date)
if filters.get("item_code"):
- query = query.where(db_so_item.item_group == frappe.db.escape(filters.item_code))
+ query = query.where(db_so_item.item_code == filters.item_code)
if filters.get("customer"):
query = query.where(db_so.customer == filters.customer)
diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
index 63d339a..2969123 100644
--- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
+++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
@@ -175,7 +175,9 @@
# update existing entry
so_row = sales_order_map[so_name]
so_row["required_date"] = max(getdate(so_row["delivery_date"]), getdate(row["delivery_date"]))
- so_row["delay"] = min(so_row["delay"], row["delay"])
+ so_row["delay"] = (
+ min(so_row["delay"], row["delay"]) if row["delay"] and so_row["delay"] else so_row["delay"]
+ )
# sum numeric columns
fields = [
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 8ff01f5..f1df3a1 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -253,7 +253,7 @@
}
calculate_commission() {
- if(!this.frm.fields_dict.commission_rate) return;
+ if(!this.frm.fields_dict.commission_rate || this.frm.doc.docstatus === 1) return;
if(this.frm.doc.commission_rate > 100) {
this.frm.set_value("commission_rate", 100);
@@ -418,8 +418,6 @@
callback: function(r) {
if(r.message) {
frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message);
- } else {
- frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message);
}
}
});
diff --git a/erpnext/setup/doctype/designation/designation.json b/erpnext/setup/doctype/designation/designation.json
index 2cbbb04..a5b2ac9 100644
--- a/erpnext/setup/doctype/designation/designation.json
+++ b/erpnext/setup/doctype/designation/designation.json
@@ -31,7 +31,7 @@
"icon": "fa fa-bookmark",
"idx": 1,
"links": [],
- "modified": "2022-06-28 17:10:26.853753",
+ "modified": "2023-02-10 01:53:41.319386",
"modified_by": "Administrator",
"module": "Setup",
"name": "Designation",
@@ -58,5 +58,6 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "ASC",
- "states": []
+ "states": [],
+ "translated_doctype": 1
}
\ No newline at end of file
diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py
index facefa3..ece5a7d 100755
--- a/erpnext/setup/doctype/employee/employee.py
+++ b/erpnext/setup/doctype/employee/employee.py
@@ -8,7 +8,6 @@
get_doc_permissions,
has_permission,
remove_user_permission,
- set_user_permission_if_allowed,
)
from frappe.utils import cstr, getdate, today, validate_email_address
from frappe.utils.nestedset import NestedSet
@@ -96,7 +95,7 @@
return
add_user_permission("Employee", self.name, self.user_id)
- set_user_permission_if_allowed("Company", self.company, self.user_id)
+ add_user_permission("Company", self.company, self.user_id)
def update_user(self):
# add employee role if missing
diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
index 4256a7d..481a3a5 100644
--- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
+++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
@@ -3,13 +3,17 @@
import frappe
-from frappe import _
+from frappe import _, qb
from frappe.desk.notifications import clear_notifications
from frappe.model.document import Document
-from frappe.utils import cint
+from frappe.utils import cint, create_batch
class TransactionDeletionRecord(Document):
+ def __init__(self, *args, **kwargs):
+ super(TransactionDeletionRecord, self).__init__(*args, **kwargs)
+ self.batch_size = 5000
+
def validate(self):
frappe.only_for("System Manager")
self.validate_doctypes_to_be_ignored()
@@ -155,8 +159,9 @@
"DocField", filters={"fieldtype": "Table", "parent": doctype}, pluck="options"
)
- for table in child_tables:
- frappe.db.delete(table, {"parent": ["in", parent_docs_to_be_deleted]})
+ for batch in create_batch(parent_docs_to_be_deleted, self.batch_size):
+ for table in child_tables:
+ frappe.db.delete(table, {"parent": ["in", batch]})
def delete_docs_linked_with_specified_company(self, doctype, company_fieldname):
frappe.db.delete(doctype, {company_fieldname: self.company})
@@ -181,13 +186,16 @@
frappe.db.sql("""update `tabSeries` set current = %s where name=%s""", (last, prefix))
def delete_version_log(self, doctype, company_fieldname):
- frappe.db.sql(
- """delete from `tabVersion` where ref_doctype=%s and docname in
- (select name from `tab{0}` where `{1}`=%s)""".format(
- doctype, company_fieldname
- ),
- (doctype, self.company),
- )
+ dt = qb.DocType(doctype)
+ names = qb.from_(dt).select(dt.name).where(dt[company_fieldname] == self.company).run(as_list=1)
+ names = [x[0] for x in names]
+
+ if names:
+ versions = qb.DocType("Version")
+ for batch in create_batch(names, self.batch_size):
+ qb.from_(versions).delete().where(
+ (versions.ref_doctype == doctype) & (versions.docname.isin(batch))
+ ).run()
def delete_communications(self, doctype, company_fieldname):
reference_docs = frappe.get_all(doctype, filters={company_fieldname: self.company})
@@ -199,7 +207,8 @@
)
communication_names = [c.name for c in communications]
- frappe.delete_doc("Communication", communication_names, ignore_permissions=True)
+ for batch in create_batch(communication_names, self.batch_size):
+ frappe.delete_doc("Communication", batch, ignore_permissions=True)
@frappe.whitelist()
diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json
index 45e39c5..5750914 100644
--- a/erpnext/setup/setup_wizard/data/country_wise_tax.json
+++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json
@@ -4015,34 +4015,6 @@
"tax_rate": 18.00
}
},
-
- "Saudi Arabia": {
- "KSA VAT 15%": {
- "account_name": "VAT 15%",
- "tax_rate": 15.00
- },
- "KSA VAT 5%": {
- "account_name": "VAT 5%",
- "tax_rate": 5.00
- },
- "KSA VAT Zero": {
- "account_name": "VAT Zero",
- "tax_rate": 0.00
- },
- "KSA VAT Exempted": {
- "account_name": "VAT Exempted",
- "tax_rate": 0.00
- },
- "KSA Excise 50%": {
- "account_name": "Excise 50%",
- "tax_rate": 50.00
- },
- "KSA Excise 100%": {
- "account_name": "Excise 100%",
- "tax_rate": 100.00
- }
- },
-
"Serbia": {
"Serbia Tax": {
"account_name": "VAT",
diff --git a/erpnext/setup/setup_wizard/data/designation.txt b/erpnext/setup/setup_wizard/data/designation.txt
new file mode 100644
index 0000000..4c6d7bd
--- /dev/null
+++ b/erpnext/setup/setup_wizard/data/designation.txt
@@ -0,0 +1,31 @@
+Accountant
+Administrative Assistant
+Administrative Officer
+Analyst
+Associate
+Business Analyst
+Business Development Manager
+Consultant
+Chief Executive Officer
+Chief Financial Officer
+Chief Operating Officer
+Chief Technology Officer
+Customer Service Representative
+Designer
+Engineer
+Executive Assistant
+Finance Manager
+HR Manager
+Head of Marketing and Sales
+Manager
+Managing Director
+Marketing Manager
+Marketing Specialist
+President
+Product Manager
+Project Manager
+Researcher
+Sales Representative
+Secretary
+Software Developer
+Vice President
diff --git a/erpnext/setup/setup_wizard/data/industry_type.py b/erpnext/setup/setup_wizard/data/industry_type.py
deleted file mode 100644
index 0bc3f32..0000000
--- a/erpnext/setup/setup_wizard/data/industry_type.py
+++ /dev/null
@@ -1,57 +0,0 @@
-from frappe import _
-
-
-def get_industry_types():
- return [
- _("Accounting"),
- _("Advertising"),
- _("Aerospace"),
- _("Agriculture"),
- _("Airline"),
- _("Apparel & Accessories"),
- _("Automotive"),
- _("Banking"),
- _("Biotechnology"),
- _("Broadcasting"),
- _("Brokerage"),
- _("Chemical"),
- _("Computer"),
- _("Consulting"),
- _("Consumer Products"),
- _("Cosmetics"),
- _("Defense"),
- _("Department Stores"),
- _("Education"),
- _("Electronics"),
- _("Energy"),
- _("Entertainment & Leisure"),
- _("Executive Search"),
- _("Financial Services"),
- _("Food, Beverage & Tobacco"),
- _("Grocery"),
- _("Health Care"),
- _("Internet Publishing"),
- _("Investment Banking"),
- _("Legal"),
- _("Manufacturing"),
- _("Motion Picture & Video"),
- _("Music"),
- _("Newspaper Publishers"),
- _("Online Auctions"),
- _("Pension Funds"),
- _("Pharmaceuticals"),
- _("Private Equity"),
- _("Publishing"),
- _("Real Estate"),
- _("Retail & Wholesale"),
- _("Securities & Commodity Exchanges"),
- _("Service"),
- _("Soap & Detergent"),
- _("Software"),
- _("Sports"),
- _("Technology"),
- _("Telecommunications"),
- _("Television"),
- _("Transportation"),
- _("Venture Capital"),
- ]
diff --git a/erpnext/setup/setup_wizard/data/industry_type.txt b/erpnext/setup/setup_wizard/data/industry_type.txt
new file mode 100644
index 0000000..eadc689
--- /dev/null
+++ b/erpnext/setup/setup_wizard/data/industry_type.txt
@@ -0,0 +1,51 @@
+Accounting
+Advertising
+Aerospace
+Agriculture
+Airline
+Apparel & Accessories
+Automotive
+Banking
+Biotechnology
+Broadcasting
+Brokerage
+Chemical
+Computer
+Consulting
+Consumer Products
+Cosmetics
+Defense
+Department Stores
+Education
+Electronics
+Energy
+Entertainment & Leisure
+Executive Search
+Financial Services
+Food, Beverage & Tobacco
+Grocery
+Health Care
+Internet Publishing
+Investment Banking
+Legal
+Manufacturing
+Motion Picture & Video
+Music
+Newspaper Publishers
+Online Auctions
+Pension Funds
+Pharmaceuticals
+Private Equity
+Publishing
+Real Estate
+Retail & Wholesale
+Securities & Commodity Exchanges
+Service
+Soap & Detergent
+Software
+Sports
+Technology
+Telecommunications
+Television
+Transportation
+Venture Capital
diff --git a/erpnext/setup/setup_wizard/data/lead_source.txt b/erpnext/setup/setup_wizard/data/lead_source.txt
new file mode 100644
index 0000000..00ca180
--- /dev/null
+++ b/erpnext/setup/setup_wizard/data/lead_source.txt
@@ -0,0 +1,10 @@
+Existing Customer
+Reference
+Advertisement
+Cold Calling
+Exhibition
+Supplier Reference
+Mass Mailing
+Customer's Vendor
+Campaign
+Walk In
diff --git a/erpnext/setup/setup_wizard/data/sales_partner_type.txt b/erpnext/setup/setup_wizard/data/sales_partner_type.txt
new file mode 100644
index 0000000..68e9b9a
--- /dev/null
+++ b/erpnext/setup/setup_wizard/data/sales_partner_type.txt
@@ -0,0 +1,7 @@
+Channel Partner
+Distributor
+Dealer
+Agent
+Retailer
+Implementation Partner
+Reseller
diff --git a/erpnext/setup/setup_wizard/data/sales_stage.txt b/erpnext/setup/setup_wizard/data/sales_stage.txt
new file mode 100644
index 0000000..2808ce7
--- /dev/null
+++ b/erpnext/setup/setup_wizard/data/sales_stage.txt
@@ -0,0 +1,8 @@
+Prospecting
+Qualification
+Needs Analysis
+Value Proposition
+Identifying Decision Makers
+Perception Analysis
+Proposal/Price Quote
+Negotiation/Review
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 1f8c0d6..6bc1771 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -4,6 +4,7 @@
import json
import os
+from pathlib import Path
import frappe
from frappe import _
@@ -16,28 +17,10 @@
from erpnext.accounts.doctype.account.account import RootNotEditable
from erpnext.regional.address_template.setup import set_up_address_templates
-default_lead_sources = [
- "Existing Customer",
- "Reference",
- "Advertisement",
- "Cold Calling",
- "Exhibition",
- "Supplier Reference",
- "Mass Mailing",
- "Customer's Vendor",
- "Campaign",
- "Walk In",
-]
-default_sales_partner_type = [
- "Channel Partner",
- "Distributor",
- "Dealer",
- "Agent",
- "Retailer",
- "Implementation Partner",
- "Reseller",
-]
+def read_lines(filename: str) -> list[str]:
+ """Return a list of lines from a file in the data directory."""
+ return (Path(__file__).parent.parent / "data" / filename).read_text().splitlines()
def install(country=None):
@@ -85,7 +68,11 @@
# Stock Entry Type
{"doctype": "Stock Entry Type", "name": "Material Issue", "purpose": "Material Issue"},
{"doctype": "Stock Entry Type", "name": "Material Receipt", "purpose": "Material Receipt"},
- {"doctype": "Stock Entry Type", "name": "Material Transfer", "purpose": "Material Transfer"},
+ {
+ "doctype": "Stock Entry Type",
+ "name": "Material Transfer",
+ "purpose": "Material Transfer",
+ },
{"doctype": "Stock Entry Type", "name": "Manufacture", "purpose": "Manufacture"},
{"doctype": "Stock Entry Type", "name": "Repack", "purpose": "Repack"},
{
@@ -103,22 +90,6 @@
"name": "Material Consumption for Manufacture",
"purpose": "Material Consumption for Manufacture",
},
- # Designation
- {"doctype": "Designation", "designation_name": _("CEO")},
- {"doctype": "Designation", "designation_name": _("Manager")},
- {"doctype": "Designation", "designation_name": _("Analyst")},
- {"doctype": "Designation", "designation_name": _("Engineer")},
- {"doctype": "Designation", "designation_name": _("Accountant")},
- {"doctype": "Designation", "designation_name": _("Secretary")},
- {"doctype": "Designation", "designation_name": _("Associate")},
- {"doctype": "Designation", "designation_name": _("Administrative Officer")},
- {"doctype": "Designation", "designation_name": _("Business Development Manager")},
- {"doctype": "Designation", "designation_name": _("HR Manager")},
- {"doctype": "Designation", "designation_name": _("Project Manager")},
- {"doctype": "Designation", "designation_name": _("Head of Marketing and Sales")},
- {"doctype": "Designation", "designation_name": _("Software Developer")},
- {"doctype": "Designation", "designation_name": _("Designer")},
- {"doctype": "Designation", "designation_name": _("Researcher")},
# territory: with two default territories, one for home country and one named Rest of the World
{
"doctype": "Territory",
@@ -291,28 +262,18 @@
{"doctype": "Market Segment", "market_segment": _("Lower Income")},
{"doctype": "Market Segment", "market_segment": _("Middle Income")},
{"doctype": "Market Segment", "market_segment": _("Upper Income")},
- # Sales Stages
- {"doctype": "Sales Stage", "stage_name": _("Prospecting")},
- {"doctype": "Sales Stage", "stage_name": _("Qualification")},
- {"doctype": "Sales Stage", "stage_name": _("Needs Analysis")},
- {"doctype": "Sales Stage", "stage_name": _("Value Proposition")},
- {"doctype": "Sales Stage", "stage_name": _("Identifying Decision Makers")},
- {"doctype": "Sales Stage", "stage_name": _("Perception Analysis")},
- {"doctype": "Sales Stage", "stage_name": _("Proposal/Price Quote")},
- {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")},
# Warehouse Type
{"doctype": "Warehouse Type", "name": "Transit"},
]
- from erpnext.setup.setup_wizard.data.industry_type import get_industry_types
-
- records += [{"doctype": "Industry Type", "industry": d} for d in get_industry_types()]
- # records += [{"doctype":"Operation", "operation": d} for d in get_operations()]
- records += [{"doctype": "Lead Source", "source_name": _(d)} for d in default_lead_sources]
-
- records += [
- {"doctype": "Sales Partner Type", "sales_partner_type": _(d)} for d in default_sales_partner_type
- ]
+ for doctype, title_field, filename in (
+ ("Designation", "designation_name", "designation.txt"),
+ ("Sales Stage", "stage_name", "sales_stage.txt"),
+ ("Industry Type", "industry", "industry_type.txt"),
+ ("Lead Source", "source_name", "lead_source.txt"),
+ ("Sales Partner Type", "sales_partner_type", "sales_partner_type.txt"),
+ ):
+ records += [{"doctype": doctype, title_field: title} for title in read_lines(filename)]
base_path = frappe.get_app_path("erpnext", "stock", "doctype")
response = frappe.read_file(
@@ -397,7 +358,8 @@
frappe.get_doc({"doctype": "UOM Category", "category_name": _(d.get("category"))}).db_insert()
if not frappe.db.exists(
- "UOM Conversion Factor", {"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))}
+ "UOM Conversion Factor",
+ {"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))},
):
frappe.get_doc(
{
@@ -535,7 +497,8 @@
company_name = args.get("company_name")
bank_account_group = frappe.db.get_value(
- "Account", {"account_type": "Bank", "is_group": 1, "root_type": "Asset", "company": company_name}
+ "Account",
+ {"account_type": "Bank", "is_group": 1, "root_type": "Asset", "company": company_name},
)
if bank_account_group:
bank_account = frappe.get_doc(
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js
index ea3cf19..ae56645 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -97,12 +97,12 @@
}
if (frm.doc.docstatus == 1 && !frm.doc.inter_company_reference) {
- let internal = me.frm.doc.is_internal_customer;
+ let internal = frm.doc.is_internal_customer;
if (internal) {
- let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Receipt" :
+ let button_label = (frm.doc.company === frm.doc.represents_company) ? "Internal Purchase Receipt" :
"Inter Company Purchase Receipt";
- me.frm.add_custom_button(button_label, function() {
+ frm.add_custom_button(__(button_label), function() {
frappe.model.open_mapped_doc({
method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_inter_company_purchase_receipt',
frm: frm,
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index 165a56b..0c1f820 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -521,6 +521,7 @@
"allow_bulk_edit": 1,
"fieldname": "items",
"fieldtype": "Table",
+ "label": "Delivery Note Item",
"oldfieldname": "delivery_note_details",
"oldfieldtype": "Table",
"options": "Delivery Note Item",
@@ -666,6 +667,7 @@
{
"fieldname": "taxes",
"fieldtype": "Table",
+ "label": "Sales Taxes and Charges",
"oldfieldname": "other_charges",
"oldfieldtype": "Table",
"options": "Sales Taxes and Charges"
@@ -1401,7 +1403,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2022-12-12 18:38:53.067799",
+ "modified": "2023-02-14 04:45:44.179670",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js
index 9e6f3bc..6ff3ed3 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js
@@ -14,7 +14,7 @@
return [__("Completed"), "green", "per_billed,=,100"];
}
},
- onload: function (listview) {
+ onload: function (doclist) {
const action = () => {
const selected_docs = doclist.get_checked_items();
const docnames = doclist.get_checked_items(true);
@@ -56,14 +56,14 @@
// doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false);
- listview.page.add_action_item(__('Create Delivery Trip'), action);
+ doclist.page.add_action_item(__('Create Delivery Trip'), action);
- listview.page.add_action_item(__("Sales Invoice"), ()=>{
- erpnext.bulk_transaction_processing.create(listview, "Delivery Note", "Sales Invoice");
+ doclist.page.add_action_item(__("Sales Invoice"), ()=>{
+ erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Sales Invoice");
});
- listview.page.add_action_item(__("Packaging Slip From Delivery Note"), ()=>{
- erpnext.bulk_transaction_processing.create(listview, "Delivery Note", "Packing Slip");
+ doclist.page.add_action_item(__("Packaging Slip From Delivery Note"), ()=>{
+ erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Packing Slip");
});
}
};
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 5bcb05a..9a9ddf4 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -33,6 +33,9 @@
'Material Request': () => {
open_form(frm, "Material Request", "Material Request Item", "items");
},
+ 'Stock Entry': () => {
+ open_form(frm, "Stock Entry", "Stock Entry Detail", "items");
+ },
};
},
@@ -893,6 +896,9 @@
new_child_doc.item_name = frm.doc.item_name;
new_child_doc.uom = frm.doc.stock_uom;
new_child_doc.description = frm.doc.description;
+ if (!new_child_doc.qty) {
+ new_child_doc.qty = 1.0;
+ }
frappe.run_serially([
() => frappe.ui.form.make_quick_entry(doctype, null, null, new_doc),
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index 629e50e..34adbeb 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -706,7 +706,7 @@
"depends_on": "enable_deferred_expense",
"fieldname": "no_of_months_exp",
"fieldtype": "Int",
- "label": "No of Months"
+ "label": "No of Months (Expense)"
},
{
"collapsible": 1,
@@ -911,7 +911,7 @@
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
- "modified": "2023-01-07 22:45:00.341745",
+ "modified": "2023-02-14 04:48:26.343620",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 686e6cb..c06700a 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -358,7 +358,7 @@
check_list.append(d.item_tax_template)
def validate_barcode(self):
- from stdnum import ean
+ import barcodenumber
if len(self.barcodes) > 0:
for item_barcode in self.barcodes:
@@ -376,19 +376,16 @@
item_barcode.barcode_type = (
"" if item_barcode.barcode_type not in options else item_barcode.barcode_type
)
- if item_barcode.barcode_type and item_barcode.barcode_type.upper() in (
- "EAN",
- "UPC-A",
- "EAN-13",
- "EAN-8",
- ):
- if not ean.is_valid(item_barcode.barcode):
- frappe.throw(
- _("Barcode {0} is not a valid {1} code").format(
- item_barcode.barcode, item_barcode.barcode_type
- ),
- InvalidBarcode,
- )
+ if item_barcode.barcode_type:
+ barcode_type = convert_erpnext_to_barcodenumber(item_barcode.barcode_type.upper())
+ if barcode_type in barcodenumber.barcodes():
+ if not barcodenumber.check_code(barcode_type, item_barcode.barcode):
+ frappe.throw(
+ _("Barcode {0} is not a valid {1} code").format(
+ item_barcode.barcode, item_barcode.barcode_type
+ ),
+ InvalidBarcode,
+ )
def validate_warehouse_for_reorder(self):
"""Validate Reorder level table for duplicate and conditional mandatory"""
@@ -985,6 +982,22 @@
)
+def convert_erpnext_to_barcodenumber(erpnext_number):
+ convert = {
+ "UPC-A": "UPCA",
+ "CODE-39": "CODE39",
+ "EAN": "EAN13",
+ "EAN-12": "EAN",
+ "EAN-8": "EAN8",
+ "ISBN-10": "ISBN10",
+ "ISBN-13": "ISBN13",
+ }
+ if erpnext_number in convert:
+ return convert[erpnext_number]
+ else:
+ return erpnext_number
+
+
def make_item_price(item, price_list_name, item_price):
frappe.get_doc(
{
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 53f6b7f..67ed90d 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -579,6 +579,19 @@
{
"barcode": "ARBITRARY_TEXT",
},
+ {"barcode": "72527273070", "barcode_type": "UPC-A"},
+ {"barcode": "123456", "barcode_type": "CODE-39"},
+ {"barcode": "401268452363", "barcode_type": "EAN-12"},
+ {"barcode": "90311017", "barcode_type": "EAN-8"},
+ {"barcode": "0123456789012", "barcode_type": "GS1"},
+ {"barcode": "2211564566668", "barcode_type": "GTIN"},
+ {"barcode": "0256480249", "barcode_type": "ISBN"},
+ {"barcode": "0192552570", "barcode_type": "ISBN-10"},
+ {"barcode": "9781234567897", "barcode_type": "ISBN-13"},
+ {"barcode": "9771234567898", "barcode_type": "ISSN"},
+ {"barcode": "4581171967072", "barcode_type": "JAN"},
+ {"barcode": "12345678", "barcode_type": "PZN"},
+ {"barcode": "725272730706", "barcode_type": "UPC"},
]
create_item(item_code)
for barcode_properties in barcode_properties_list:
diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.json b/erpnext/stock/doctype/item_barcode/item_barcode.json
index bda1218..d9a8347 100644
--- a/erpnext/stock/doctype/item_barcode/item_barcode.json
+++ b/erpnext/stock/doctype/item_barcode/item_barcode.json
@@ -25,7 +25,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Barcode Type",
- "options": "\nEAN\nUPC-A"
+ "options": "\nEAN\nUPC-A\nCODE-39\nEAN-12\nEAN-8\nGS1\nGTIN\nISBN\nISBN-10\nISBN-13\nISSN\nJAN\nPZN\nUPC"
},
{
"fieldname": "uom",
diff --git a/erpnext/stock/doctype/item_price/item_price.js b/erpnext/stock/doctype/item_price/item_price.js
index 12cf6cf..ce489ff 100644
--- a/erpnext/stock/doctype/item_price/item_price.js
+++ b/erpnext/stock/doctype/item_price/item_price.js
@@ -2,7 +2,18 @@
// License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Item Price", {
- onload: function (frm) {
+ setup(frm) {
+ frm.set_query("item_code", function() {
+ return {
+ filters: {
+ "disabled": 0,
+ "has_variants": 0
+ }
+ };
+ });
+ },
+
+ onload(frm) {
// Fetch price list details
frm.add_fetch("price_list", "buying", "buying");
frm.add_fetch("price_list", "selling", "selling");
diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py
index bcd31ad..54d1ae6 100644
--- a/erpnext/stock/doctype/item_price/item_price.py
+++ b/erpnext/stock/doctype/item_price/item_price.py
@@ -3,7 +3,7 @@
import frappe
-from frappe import _
+from frappe import _, bold
from frappe.model.document import Document
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Cast_
@@ -21,6 +21,7 @@
self.update_price_list_details()
self.update_item_details()
self.check_duplicates()
+ self.validate_item_template()
def validate_item(self):
if not frappe.db.exists("Item", self.item_code):
@@ -49,6 +50,12 @@
"Item", self.item_code, ["item_name", "description"]
)
+ def validate_item_template(self):
+ if frappe.get_cached_value("Item", self.item_code, "has_variants"):
+ msg = f"Item Price cannot be created for the template item {bold(self.item_code)}"
+
+ frappe.throw(_(msg))
+
def check_duplicates(self):
item_price = frappe.qb.DocType("Item Price")
diff --git a/erpnext/stock/doctype/item_price/test_item_price.py b/erpnext/stock/doctype/item_price/test_item_price.py
index 30d933e..8fd4938 100644
--- a/erpnext/stock/doctype/item_price/test_item_price.py
+++ b/erpnext/stock/doctype/item_price/test_item_price.py
@@ -16,6 +16,28 @@
frappe.db.sql("delete from `tabItem Price`")
make_test_records_for_doctype("Item Price", force=True)
+ def test_template_item_price(self):
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ item = make_item(
+ "Test Template Item 1",
+ {
+ "has_variants": 1,
+ "variant_based_on": "Manufacturer",
+ },
+ )
+
+ doc = frappe.get_doc(
+ {
+ "doctype": "Item Price",
+ "price_list": "_Test Price List",
+ "item_code": item.name,
+ "price_list_rate": 100,
+ }
+ )
+
+ self.assertRaises(frappe.ValidationError, doc.save)
+
def test_duplicate_item(self):
doc = frappe.copy_doc(test_records[0])
self.assertRaises(ItemPriceDuplicateItem, doc.save)
diff --git a/erpnext/stock/doctype/item_price/test_records.json b/erpnext/stock/doctype/item_price/test_records.json
index 0a3d7e8..afe5ad6 100644
--- a/erpnext/stock/doctype/item_price/test_records.json
+++ b/erpnext/stock/doctype/item_price/test_records.json
@@ -38,5 +38,19 @@
"price_list_rate": 1000,
"valid_from": "2017-04-10",
"valid_upto": "2017-04-17"
+ },
+ {
+ "doctype": "Item Price",
+ "item_code": "_Test Item",
+ "price_list": "_Test Buying Price List",
+ "price_list_rate": 100,
+ "supplier": "_Test Supplier"
+ },
+ {
+ "doctype": "Item Price",
+ "item_code": "_Test Item",
+ "price_list": "_Test Selling Price List",
+ "price_list_rate": 200,
+ "customer": "_Test Customer"
}
]
diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
index b3af309..111a0861 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
@@ -55,7 +55,6 @@
self.get_items_from_purchase_receipts()
self.set_applicable_charges_on_item()
- self.validate_applicable_charges_for_item()
def check_mandatory(self):
if not self.get("purchase_receipts"):
@@ -115,6 +114,13 @@
total_item_cost += item.get(based_on_field)
for item in self.get("items"):
+ if not total_item_cost and not item.get(based_on_field):
+ frappe.throw(
+ _(
+ "It's not possible to distribute charges equally when total amount is zero, please set 'Distribute Charges Based On' as 'Quantity'"
+ )
+ )
+
item.applicable_charges = flt(
flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)),
item.precision("applicable_charges"),
@@ -162,6 +168,7 @@
)
def on_submit(self):
+ self.validate_applicable_charges_for_item()
self.update_landed_cost()
def on_cancel(self):
diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
index 979b5c4..00fa168 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
@@ -175,6 +175,59 @@
)
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0)
+ def test_landed_cost_voucher_for_zero_purchase_rate(self):
+ "Test impact of LCV on future stock balances."
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ item = make_item("LCV Stock Item", {"is_stock_item": 1})
+ warehouse = "Stores - _TC"
+
+ pr = make_purchase_receipt(
+ item_code=item.name,
+ warehouse=warehouse,
+ qty=10,
+ rate=0,
+ posting_date=add_days(frappe.utils.nowdate(), -2),
+ )
+
+ self.assertEqual(
+ frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
+ "stock_value_difference",
+ ),
+ 0,
+ )
+
+ lcv = make_landed_cost_voucher(
+ company=pr.company,
+ receipt_document_type="Purchase Receipt",
+ receipt_document=pr.name,
+ charges=100,
+ distribute_charges_based_on="Distribute Manually",
+ do_not_save=True,
+ )
+
+ lcv.get_items_from_purchase_receipts()
+ lcv.items[0].applicable_charges = 100
+ lcv.save()
+ lcv.submit()
+
+ self.assertTrue(
+ frappe.db.exists(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
+ )
+ )
+ self.assertEqual(
+ frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
+ "stock_value_difference",
+ ),
+ 100,
+ )
+
def test_landed_cost_voucher_against_purchase_invoice(self):
pi = make_purchase_invoice(
@@ -516,7 +569,7 @@
lcv = frappe.new_doc("Landed Cost Voucher")
lcv.company = args.company or "_Test Company"
- lcv.distribute_charges_based_on = "Amount"
+ lcv.distribute_charges_based_on = args.distribute_charges_based_on or "Amount"
lcv.set(
"purchase_receipts",
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index 156e591..c1f1b0d 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -110,8 +110,11 @@
if (frm.doc.material_request_type === "Material Transfer") {
add_create_pick_list_button();
- frm.add_custom_button(__("Transfer Material"),
+ frm.add_custom_button(__("Material Transfer"),
() => frm.events.make_stock_entry(frm), __('Create'));
+
+ frm.add_custom_button(__("Material Transfer (In Transit)"),
+ () => frm.events.make_in_transit_stock_entry(frm), __('Create'));
}
if (frm.doc.material_request_type === "Material Issue") {
@@ -333,6 +336,46 @@
});
},
+ make_in_transit_stock_entry(frm) {
+ frappe.prompt(
+ [
+ {
+ label: __('In Transit Warehouse'),
+ fieldname: 'in_transit_warehouse',
+ fieldtype: 'Link',
+ options: 'Warehouse',
+ reqd: 1,
+ get_query: () => {
+ return{
+ filters: {
+ 'company': frm.doc.company,
+ 'is_group': 0,
+ 'warehouse_type': 'Transit'
+ }
+ }
+ }
+ }
+ ],
+ (values) => {
+ frappe.call({
+ method: "erpnext.stock.doctype.material_request.material_request.make_in_transit_stock_entry",
+ args: {
+ source_name: frm.doc.name,
+ in_transit_warehouse: values.in_transit_warehouse
+ },
+ callback: function(r) {
+ if (r.message) {
+ let doc = frappe.model.sync(r.message);
+ frappe.set_route('Form', doc[0].doctype, doc[0].name);
+ }
+ }
+ })
+ },
+ __('In Transit Transfer'),
+ __("Create Stock Entry")
+ )
+ },
+
create_pick_list: (frm) => {
frappe.model.open_mapped_doc({
method: "erpnext.stock.doctype.material_request.material_request.create_pick_list",
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 94f63a5..8aeb751 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -10,6 +10,7 @@
import frappe
from frappe import _, msgprint
from frappe.model.mapper import get_mapped_doc
+from frappe.query_builder.functions import Sum
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
@@ -180,6 +181,34 @@
self.update_requested_qty()
self.update_requested_qty_in_production_plan()
+ def get_mr_items_ordered_qty(self, mr_items):
+ mr_items_ordered_qty = {}
+ mr_items = [d.name for d in self.get("items") if d.name in mr_items]
+
+ doctype = qty_field = None
+ if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"):
+ doctype = frappe.qb.DocType("Stock Entry Detail")
+ qty_field = doctype.transfer_qty
+ elif self.material_request_type == "Manufacture":
+ doctype = frappe.qb.DocType("Work Order")
+ qty_field = doctype.qty
+
+ if doctype and qty_field:
+ query = (
+ frappe.qb.from_(doctype)
+ .select(doctype.material_request_item, Sum(qty_field))
+ .where(
+ (doctype.material_request == self.name)
+ & (doctype.material_request_item.isin(mr_items))
+ & (doctype.docstatus == 1)
+ )
+ .groupby(doctype.material_request_item)
+ )
+
+ mr_items_ordered_qty = frappe._dict(query.run())
+
+ return mr_items_ordered_qty
+
def update_completed_qty(self, mr_items=None, update_modified=True):
if self.material_request_type == "Purchase":
return
@@ -187,18 +216,13 @@
if not mr_items:
mr_items = [d.name for d in self.get("items")]
+ mr_items_ordered_qty = self.get_mr_items_ordered_qty(mr_items)
+ mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance")
+
for d in self.get("items"):
if d.name in mr_items:
if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"):
- d.ordered_qty = flt(
- frappe.db.sql(
- """select sum(transfer_qty)
- from `tabStock Entry Detail` where material_request = %s
- and material_request_item = %s and docstatus = 1""",
- (self.name, d.name),
- )[0][0]
- )
- mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance")
+ d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
if mr_qty_allowance:
allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100))
@@ -217,14 +241,7 @@
)
elif self.material_request_type == "Manufacture":
- d.ordered_qty = flt(
- frappe.db.sql(
- """select sum(qty)
- from `tabWork Order` where material_request = %s
- and material_request_item = %s and docstatus = 1""",
- (self.name, d.name),
- )[0][0]
- )
+ d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty)
@@ -587,6 +604,9 @@
def set_missing_values(source, target):
target.purpose = source.material_request_type
+ target.from_warehouse = source.set_from_warehouse
+ target.to_warehouse = source.set_warehouse
+
if source.job_card:
target.purpose = "Material Transfer for Manufacture"
@@ -716,3 +736,15 @@
doc.set_item_locations()
return doc
+
+
+@frappe.whitelist()
+def make_in_transit_stock_entry(source_name, in_transit_warehouse):
+ ste_doc = make_stock_entry(source_name)
+ ste_doc.add_to_transit = 1
+ ste_doc.to_warehouse = in_transit_warehouse
+
+ for row in ste_doc.items:
+ row.t_warehouse = in_transit_warehouse
+
+ return ste_doc
diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py
index f0a9499..a707c74 100644
--- a/erpnext/stock/doctype/material_request/test_material_request.py
+++ b/erpnext/stock/doctype/material_request/test_material_request.py
@@ -11,6 +11,7 @@
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.material_request.material_request import (
+ make_in_transit_stock_entry,
make_purchase_order,
make_stock_entry,
make_supplier_quotation,
@@ -56,6 +57,22 @@
self.assertEqual(se.doctype, "Stock Entry")
self.assertEqual(len(se.get("items")), len(mr.get("items")))
+ def test_in_transit_make_stock_entry(self):
+ mr = frappe.copy_doc(test_records[0]).insert()
+
+ self.assertRaises(frappe.ValidationError, make_stock_entry, mr.name)
+
+ mr = frappe.get_doc("Material Request", mr.name)
+ mr.material_request_type = "Material Transfer"
+ mr.submit()
+
+ in_transit_warehouse = get_in_transit_warehouse(mr.company)
+ se = make_in_transit_stock_entry(mr.name, in_transit_warehouse)
+
+ self.assertEqual(se.doctype, "Stock Entry")
+ for row in se.get("items"):
+ self.assertEqual(row.t_warehouse, in_transit_warehouse)
+
def _insert_stock_entry(self, qty1, qty2, warehouse=None):
se = frappe.get_doc(
{
@@ -742,6 +759,36 @@
self.assertEqual(existing_requested_qty, current_requested_qty)
+def get_in_transit_warehouse(company):
+ if not frappe.db.exists("Warehouse Type", "Transit"):
+ frappe.get_doc(
+ {
+ "doctype": "Warehouse Type",
+ "name": "Transit",
+ }
+ ).insert()
+
+ in_transit_warehouse = frappe.db.exists(
+ "Warehouse", {"warehouse_type": "Transit", "company": company}
+ )
+
+ if not in_transit_warehouse:
+ in_transit_warehouse = (
+ frappe.get_doc(
+ {
+ "doctype": "Warehouse",
+ "warehouse_name": "Transit",
+ "warehouse_type": "Transit",
+ "company": company,
+ }
+ )
+ .insert()
+ .name
+ )
+
+ return in_transit_warehouse
+
+
def make_material_request(**args):
args = frappe._dict(args)
mr = frappe.new_doc("Material Request")
diff --git a/erpnext/stock/doctype/price_list/test_records.json b/erpnext/stock/doctype/price_list/test_records.json
index 7ca949c..e02a7ad 100644
--- a/erpnext/stock/doctype/price_list/test_records.json
+++ b/erpnext/stock/doctype/price_list/test_records.json
@@ -31,5 +31,21 @@
"enabled": 1,
"price_list_name": "_Test Price List Rest of the World",
"selling": 1
+ },
+ {
+ "buying": 0,
+ "currency": "USD",
+ "doctype": "Price List",
+ "enabled": 1,
+ "price_list_name": "_Test Selling Price List",
+ "selling": 1
+ },
+ {
+ "buying": 1,
+ "currency": "USD",
+ "doctype": "Price List",
+ "enabled": 1,
+ "price_list_name": "_Test Buying Price List",
+ "selling": 0
}
]
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index af0d148..c1abd31 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -293,6 +293,7 @@
get_purchase_document_details,
)
+ stock_rbnb = None
if erpnext.is_perpetual_inventory_enabled(self.company):
stock_rbnb = self.get_company_default("stock_received_but_not_billed")
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
@@ -450,6 +451,21 @@
item=d,
)
+ if d.rate_difference_with_purchase_invoice and stock_rbnb:
+ account_currency = get_account_currency(stock_rbnb)
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=stock_rbnb,
+ cost_center=d.cost_center,
+ debit=0.0,
+ credit=flt(d.rate_difference_with_purchase_invoice),
+ remarks=_("Adjustment based on Purchase Invoice rate"),
+ against_account=warehouse_account_name,
+ account_currency=account_currency,
+ project=d.project,
+ item=d,
+ )
+
# sub-contracting warehouse
if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse):
self.add_gl_entry(
@@ -470,10 +486,11 @@
+ flt(d.landed_cost_voucher_amount)
+ flt(d.rm_supp_cost)
+ flt(d.item_tax_amount)
+ + flt(d.rate_difference_with_purchase_invoice)
)
divisional_loss = flt(
- valuation_amount_as_per_doc - stock_value_diff, d.precision("base_net_amount")
+ valuation_amount_as_per_doc - flt(stock_value_diff), d.precision("base_net_amount")
)
if divisional_loss:
@@ -765,7 +782,7 @@
updated_pr += update_billed_amount_based_on_po(po_details, update_modified)
for pr in set(updated_pr):
- pr_doc = self if (pr == self.name) else frappe.get_cached_doc("Purchase Receipt", pr)
+ pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr)
update_billing_percentage(pr_doc, update_modified=update_modified)
self.load_from_db()
@@ -881,30 +898,28 @@
return {d.po_detail: flt(d.billed_amt) for d in query}
-def update_billing_percentage(pr_doc, update_modified=True):
+def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False):
# Reload as billed amount was set in db directly
pr_doc.load_from_db()
# Update Billing % based on pending accepted qty
total_amount, total_billed_amount = 0, 0
- for item in pr_doc.items:
- return_data = frappe.get_all(
- "Purchase Receipt",
- fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"],
- filters=[
- ["Purchase Receipt", "docstatus", "=", 1],
- ["Purchase Receipt", "is_return", "=", 1],
- ["Purchase Receipt Item", "purchase_receipt_item", "=", item.name],
- ],
- )
+ item_wise_returned_qty = get_item_wise_returned_qty(pr_doc)
- returned_qty = return_data[0].qty if return_data else 0
+ for item in pr_doc.items:
+ returned_qty = flt(item_wise_returned_qty.get(item.name))
returned_amount = flt(returned_qty) * flt(item.rate)
pending_amount = flt(item.amount) - returned_amount
total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt
total_amount += total_billable_amount
total_billed_amount += flt(item.billed_amt)
+ if adjust_incoming_rate:
+ adjusted_amt = 0.0
+ if item.billed_amt and item.amount:
+ adjusted_amt = flt(item.billed_amt) - flt(item.amount)
+
+ item.db_set("rate_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6)
pr_doc.db_set("per_billed", percent_billed)
@@ -914,6 +929,47 @@
pr_doc.set_status(update=True)
pr_doc.notify_update()
+ if adjust_incoming_rate:
+ adjust_incoming_rate_for_pr(pr_doc)
+
+
+def adjust_incoming_rate_for_pr(doc):
+ doc.update_valuation_rate(reset_outgoing_rate=False)
+
+ for item in doc.get("items"):
+ item.db_update()
+
+ doc.docstatus = 2
+ doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True)
+ doc.make_gl_entries_on_cancel()
+
+ # update stock & gl entries for submit state of PR
+ doc.docstatus = 1
+ doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True)
+ doc.make_gl_entries()
+ doc.repost_future_sle_and_gle()
+
+
+def get_item_wise_returned_qty(pr_doc):
+ items = [d.name for d in pr_doc.items]
+
+ return frappe._dict(
+ frappe.get_all(
+ "Purchase Receipt",
+ fields=[
+ "`tabPurchase Receipt Item`.purchase_receipt_item",
+ "sum(abs(`tabPurchase Receipt Item`.qty)) as qty",
+ ],
+ filters=[
+ ["Purchase Receipt", "docstatus", "=", 1],
+ ["Purchase Receipt", "is_return", "=", 1],
+ ["Purchase Receipt Item", "purchase_receipt_item", "in", items],
+ ],
+ group_by="`tabPurchase Receipt Item`.purchase_receipt_item",
+ as_list=1,
+ )
+ )
+
@frappe.whitelist()
def make_purchase_invoice(source_name, target_doc=None):
@@ -1121,13 +1177,25 @@
account.expense_account, {"amount": 0.0, "base_amount": 0.0}
)
- item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][
- "amount"
- ] += (account.amount * item.get(based_on_field) / total_item_cost)
+ if total_item_cost > 0:
+ item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
+ account.expense_account
+ ]["amount"] += (
+ account.amount * item.get(based_on_field) / total_item_cost
+ )
- item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][
- "base_amount"
- ] += (account.base_amount * item.get(based_on_field) / total_item_cost)
+ item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
+ account.expense_account
+ ]["base_amount"] += (
+ account.base_amount * item.get(based_on_field) / total_item_cost
+ )
+ else:
+ item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
+ account.expense_account
+ ]["amount"] += item.applicable_charges
+ item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
+ account.expense_account
+ ]["base_amount"] += item.applicable_charges
return item_account_wise_cost
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index 557bb59..cd320fd 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -69,6 +69,7 @@
"item_tax_amount",
"rm_supp_cost",
"landed_cost_voucher_amount",
+ "rate_difference_with_purchase_invoice",
"billed_amt",
"warehouse_and_reference",
"warehouse",
@@ -859,7 +860,8 @@
"label": "Purchase Receipt Item",
"no_copy": 1,
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"collapsible": 1,
@@ -974,7 +976,8 @@
"label": "Purchase Invoice Item",
"no_copy": 1,
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"fieldname": "product_bundle",
@@ -1005,12 +1008,20 @@
"fieldtype": "Check",
"label": "Has Item Scanned",
"read_only": 1
+ },
+ {
+ "fieldname": "rate_difference_with_purchase_invoice",
+ "fieldtype": "Currency",
+ "label": "Rate Difference with Purchase Invoice",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2022-11-02 12:49:28.746701",
+ "modified": "2023-02-28 15:43:04.470104",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
index 2a9f091..9673c81 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
@@ -6,7 +6,7 @@
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
-from frappe.utils import cint, cstr, flt
+from frappe.utils import cint, cstr, flt, get_number_format_info
from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template import (
get_template_details,
@@ -156,7 +156,9 @@
for i in range(1, 11):
reading_value = reading.get("reading_" + str(i))
if reading_value is not None and reading_value.strip():
- result = flt(reading.get("min_value")) <= flt(reading_value) <= flt(reading.get("max_value"))
+ result = (
+ flt(reading.get("min_value")) <= parse_float(reading_value) <= flt(reading.get("max_value"))
+ )
if not result:
return False
return True
@@ -196,7 +198,7 @@
# numeric readings
for i in range(1, 11):
field = "reading_" + str(i)
- data[field] = flt(reading.get(field))
+ data[field] = parse_float(reading.get(field))
data["mean"] = self.calculate_mean(reading)
return data
@@ -210,7 +212,7 @@
for i in range(1, 11):
reading_value = reading.get("reading_" + str(i))
if reading_value is not None and reading_value.strip():
- readings_list.append(flt(reading_value))
+ readings_list.append(parse_float(reading_value))
actual_mean = mean(readings_list) if readings_list else 0
return actual_mean
@@ -324,3 +326,19 @@
)
return doc
+
+
+def parse_float(num: str) -> float:
+ """Since reading_# fields are `Data` field they might contain number which
+ is representation in user's prefered number format instead of machine
+ readable format. This function converts them to machine readable format."""
+
+ number_format = frappe.db.get_default("number_format") or "#,###.##"
+ decimal_str, comma_str, _number_format_precision = get_number_format_info(number_format)
+
+ if decimal_str == "," and comma_str == ".":
+ num = num.replace(",", "#$")
+ num = num.replace(".", ",")
+ num = num.replace("#$", ".")
+
+ return flt(num)
diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
index 4f19643..9d2e139 100644
--- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
@@ -2,7 +2,7 @@
# See license.txt
import frappe
-from frappe.tests.utils import FrappeTestCase
+from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import nowdate
from erpnext.controllers.stock_controller import (
@@ -216,6 +216,40 @@
qa.save()
self.assertEqual(qa.status, "Accepted")
+ @change_settings("System Settings", {"number_format": "#.###,##"})
+ def test_diff_number_format(self):
+ self.assertEqual(frappe.db.get_default("number_format"), "#.###,##") # sanity check
+
+ # Test QI based on acceptance values (Non formula)
+ dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
+ readings = [
+ {
+ "specification": "Iron Content", # numeric reading
+ "min_value": 60,
+ "max_value": 100,
+ "reading_1": "70,000",
+ },
+ {
+ "specification": "Iron Content", # numeric reading
+ "min_value": 60,
+ "max_value": 100,
+ "reading_1": "1.100,00",
+ },
+ ]
+
+ qa = create_quality_inspection(
+ reference_type="Delivery Note", reference_name=dn.name, readings=readings, do_not_save=True
+ )
+
+ qa.save()
+
+ # status must be auto set as per formula
+ self.assertEqual(qa.readings[0].status, "Accepted")
+ self.assertEqual(qa.readings[1].status, "Rejected")
+
+ qa.delete()
+ dn.delete()
+
def create_quality_inspection(**args):
args = frappe._dict(args)
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 398b3c9..3f6a2c8 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -397,6 +397,7 @@
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": row.name,
+ "actual_qty": 0,
"company": self.company,
"stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"),
"is_cancelled": 1 if self.docstatus == 2 else 0,
@@ -423,6 +424,8 @@
data.valuation_rate = flt(row.valuation_rate)
data.stock_value_difference = -1 * flt(row.amount_difference)
+ self.update_inventory_dimensions(row, data)
+
return data
def make_sle_on_cancel(self):
diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js
index 42d0723..5f81679 100644
--- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js
+++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js
@@ -2,7 +2,22 @@
// For license information, please see license.txt
frappe.ui.form.on('Stock Reposting Settings', {
- // refresh: function(frm) {
+ refresh: function(frm) {
+ frm.trigger('convert_to_item_based_reposting');
+ },
- // }
+ convert_to_item_based_reposting: function(frm) {
+ frm.add_custom_button(__('Convert to Item Based Reposting'), function() {
+ frm.call({
+ method: 'convert_to_item_wh_reposting',
+ frezz: true,
+ doc: frm.doc,
+ callback: function(r) {
+ if (!r.exc) {
+ frm.reload_doc();
+ }
+ }
+ })
+ })
+ }
});
diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py
index e0c8ed1..51fb5ac 100644
--- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py
+++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py
@@ -1,6 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
+import frappe
+from frappe import _
from frappe.model.document import Document
from frappe.utils import add_to_date, get_datetime, get_time_str, time_diff_in_hours
@@ -24,3 +26,62 @@
if diff < 10:
self.end_time = get_time_str(add_to_date(self.start_time, hours=10, as_datetime=True))
+
+ @frappe.whitelist()
+ def convert_to_item_wh_reposting(self):
+ """Convert Transaction reposting to Item Warehouse based reposting if Item Based Reposting has enabled."""
+
+ reposting_data = get_reposting_entries()
+
+ vouchers = [d.voucher_no for d in reposting_data]
+
+ item_warehouses = {}
+
+ for ledger in get_stock_ledgers(vouchers):
+ key = (ledger.item_code, ledger.warehouse)
+ if key not in item_warehouses:
+ item_warehouses[key] = ledger.posting_date
+ elif frappe.utils.getdate(item_warehouses.get(key)) > frappe.utils.getdate(ledger.posting_date):
+ item_warehouses[key] = ledger.posting_date
+
+ for key, posting_date in item_warehouses.items():
+ item_code, warehouse = key
+ create_repost_item_valuation(item_code, warehouse, posting_date)
+
+ for row in reposting_data:
+ frappe.db.set_value("Repost Item Valuation", row.name, "status", "Skipped")
+
+ self.db_set("item_based_reposting", 1)
+ frappe.msgprint(_("Item Warehouse based reposting has been enabled."))
+
+
+def get_reposting_entries():
+ return frappe.get_all(
+ "Repost Item Valuation",
+ fields=["voucher_no", "name"],
+ filters={"status": ("in", ["Queued", "In Progress"]), "docstatus": 1, "based_on": "Transaction"},
+ )
+
+
+def get_stock_ledgers(vouchers):
+ return frappe.get_all(
+ "Stock Ledger Entry",
+ fields=["item_code", "warehouse", "posting_date"],
+ filters={"voucher_no": ("in", vouchers)},
+ )
+
+
+def create_repost_item_valuation(item_code, warehouse, posting_date):
+ frappe.get_doc(
+ {
+ "doctype": "Repost Item Valuation",
+ "company": frappe.get_cached_value("Warehouse", warehouse, "company"),
+ "posting_date": posting_date,
+ "based_on": "Item and Warehouse",
+ "posting_time": "00:00:01",
+ "item_code": item_code,
+ "warehouse": warehouse,
+ "allow_negative_stock": True,
+ "status": "Queued",
+ }
+ ).submit()
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 5af1441..489ec6e 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -8,6 +8,7 @@
from frappe import _, throw
from frappe.model import child_table_fields, default_fields
from frappe.model.meta import get_field_precision
+from frappe.query_builder.functions import CombineDatetime, IfNull, Sum
from frappe.utils import add_days, add_months, cint, cstr, flt, getdate
from erpnext import get_company_currency
@@ -88,8 +89,15 @@
update_party_blanket_order(args, out)
+ # Never try to find a customer price if customer is set in these Doctype
+ current_customer = args.customer
+ if args.get("doctype") in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]:
+ args.customer = None
+
out.update(get_price_list_rate(args, item))
+ args.customer = current_customer
+
if args.customer and cint(args.is_pos):
out.update(get_pos_profile_item_details(args.company, args, update_data=True))
@@ -519,12 +527,8 @@
itemwise_barcode = {}
for item in items_list:
- barcodes = frappe.db.sql(
- """
- select barcode from `tabItem Barcode` where parent = %s
- """,
- item.item_code,
- as_dict=1,
+ barcodes = frappe.db.get_all(
+ "Item Barcode", filters={"parent": item.item_code}, fields="barcode"
)
for barcode in barcodes:
@@ -884,34 +888,36 @@
:param item_code: str, Item Doctype field item_code
"""
- args["item_code"] = item_code
-
- conditions = """where item_code=%(item_code)s
- and price_list=%(price_list)s
- and ifnull(uom, '') in ('', %(uom)s)"""
-
- conditions += "and ifnull(batch_no, '') in ('', %(batch_no)s)"
+ ip = frappe.qb.DocType("Item Price")
+ query = (
+ frappe.qb.from_(ip)
+ .select(ip.name, ip.price_list_rate, ip.uom)
+ .where(
+ (ip.item_code == item_code)
+ & (ip.price_list == args.get("price_list"))
+ & (IfNull(ip.uom, "").isin(["", args.get("uom")]))
+ & (IfNull(ip.batch_no, "").isin(["", args.get("batch_no")]))
+ )
+ .orderby(ip.valid_from, order=frappe.qb.desc)
+ .orderby(IfNull(ip.batch_no, ""), order=frappe.qb.desc)
+ .orderby(ip.uom, order=frappe.qb.desc)
+ )
if not ignore_party:
if args.get("customer"):
- conditions += " and customer=%(customer)s"
+ query = query.where(ip.customer == args.get("customer"))
elif args.get("supplier"):
- conditions += " and supplier=%(supplier)s"
+ query = query.where(ip.supplier == args.get("supplier"))
else:
- conditions += "and (customer is null or customer = '') and (supplier is null or supplier = '')"
+ query = query.where((IfNull(ip.customer, "") == "") & (IfNull(ip.supplier, "") == ""))
if args.get("transaction_date"):
- conditions += """ and %(transaction_date)s between
- ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')"""
+ query = query.where(
+ (IfNull(ip.valid_from, "2000-01-01") <= args["transaction_date"])
+ & (IfNull(ip.valid_upto, "2500-12-31") >= args["transaction_date"])
+ )
- return frappe.db.sql(
- """ select name, price_list_rate, uom
- from `tabItem Price` {conditions}
- order by valid_from desc, ifnull(batch_no, '') desc, uom desc """.format(
- conditions=conditions
- ),
- args,
- )
+ return query.run()
def get_price_list_rate_for(args, item_code):
@@ -1084,91 +1090,68 @@
if not user:
user = frappe.session["user"]
- condition = "pfu.user = %(user)s AND pfu.default=1"
- if user and company:
- condition = "pfu.user = %(user)s AND pf.company = %(company)s AND pfu.default=1"
+ pf = frappe.qb.DocType("POS Profile")
+ pfu = frappe.qb.DocType("POS Profile User")
- pos_profile = frappe.db.sql(
- """SELECT pf.*
- FROM
- `tabPOS Profile` pf LEFT JOIN `tabPOS Profile User` pfu
- ON
- pf.name = pfu.parent
- WHERE
- {cond} AND pf.disabled = 0
- """.format(
- cond=condition
- ),
- {"user": user, "company": company},
- as_dict=1,
+ query = (
+ frappe.qb.from_(pf)
+ .left_join(pfu)
+ .on(pf.name == pfu.parent)
+ .select(pf.star)
+ .where((pfu.user == user) & (pfu.default == 1))
)
+ if company:
+ query = query.where(pf.company == company)
+
+ pos_profile = query.run(as_dict=True)
+
if not pos_profile and company:
- pos_profile = frappe.db.sql(
- """SELECT pf.*
- FROM
- `tabPOS Profile` pf LEFT JOIN `tabPOS Profile User` pfu
- ON
- pf.name = pfu.parent
- WHERE
- pf.company = %(company)s AND pf.disabled = 0
- """,
- {"company": company},
- as_dict=1,
- )
+ pos_profile = (
+ frappe.qb.from_(pf)
+ .left_join(pfu)
+ .on(pf.name == pfu.parent)
+ .select(pf.star)
+ .where((pf.company == company) & (pf.disabled == 0))
+ ).run(as_dict=True)
return pos_profile and pos_profile[0] or None
def get_serial_nos_by_fifo(args, sales_order=None):
if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"):
- return "\n".join(
- frappe.db.sql_list(
- """select name from `tabSerial No`
- where item_code=%(item_code)s and warehouse=%(warehouse)s and
- sales_order=IF(%(sales_order)s IS NULL, sales_order, %(sales_order)s)
- order by timestamp(purchase_date, purchase_time)
- asc limit %(qty)s""",
- {
- "item_code": args.item_code,
- "warehouse": args.warehouse,
- "qty": abs(cint(args.stock_qty)),
- "sales_order": sales_order,
- },
- )
+ sn = frappe.qb.DocType("Serial No")
+ query = (
+ frappe.qb.from_(sn)
+ .select(sn.name)
+ .where((sn.item_code == args.item_code) & (sn.warehouse == args.warehouse))
+ .orderby(CombineDatetime(sn.purchase_date, sn.purchase_time))
+ .limit(abs(cint(args.stock_qty)))
)
+ if sales_order:
+ query = query.where(sn.sales_order == sales_order)
+ if args.batch_no:
+ query = query.where(sn.batch_no == args.batch_no)
-def get_serial_no_batchwise(args, sales_order=None):
- if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"):
- return "\n".join(
- frappe.db.sql_list(
- """select name from `tabSerial No`
- where item_code=%(item_code)s and warehouse=%(warehouse)s and
- sales_order=IF(%(sales_order)s IS NULL, sales_order, %(sales_order)s)
- and batch_no=IF(%(batch_no)s IS NULL, batch_no, %(batch_no)s) order
- by timestamp(purchase_date, purchase_time) asc limit %(qty)s""",
- {
- "item_code": args.item_code,
- "warehouse": args.warehouse,
- "batch_no": args.batch_no,
- "qty": abs(cint(args.stock_qty)),
- "sales_order": sales_order,
- },
- )
- )
+ serial_nos = query.run(as_list=True)
+ serial_nos = [s[0] for s in serial_nos]
+
+ return "\n".join(serial_nos)
@frappe.whitelist()
def get_conversion_factor(item_code, uom):
variant_of = frappe.db.get_value("Item", item_code, "variant_of", cache=True)
filters = {"parent": item_code, "uom": uom}
+
if variant_of:
filters["parent"] = ("in", (item_code, variant_of))
conversion_factor = frappe.db.get_value("UOM Conversion Detail", filters, "conversion_factor")
if not conversion_factor:
stock_uom = frappe.db.get_value("Item", item_code, "stock_uom")
conversion_factor = get_uom_conv_factor(uom, stock_uom)
+
return {"conversion_factor": conversion_factor or 1.0}
@@ -1210,12 +1193,16 @@
def get_company_total_stock(item_code, company):
- return frappe.db.sql(
- """SELECT sum(actual_qty) from
- (`tabBin` INNER JOIN `tabWarehouse` ON `tabBin`.warehouse = `tabWarehouse`.name)
- WHERE `tabWarehouse`.company = %s and `tabBin`.item_code = %s""",
- (company, item_code),
- )[0][0]
+ bin = frappe.qb.DocType("Bin")
+ wh = frappe.qb.DocType("Warehouse")
+
+ return (
+ frappe.qb.from_(bin)
+ .inner_join(wh)
+ .on(bin.warehouse == wh.name)
+ .select(Sum(bin.actual_qty))
+ .where((wh.company == company) & (bin.item_code == item_code))
+ ).run()[0][0]
@frappe.whitelist()
@@ -1224,6 +1211,7 @@
{"item_code": item_code, "warehouse": warehouse, "stock_qty": stock_qty, "serial_no": serial_no}
)
serial_no = get_serial_no(args)
+
return {"serial_no": serial_no}
@@ -1243,6 +1231,7 @@
bin_details_and_serial_nos.update(
get_serial_no_details(item_code, warehouse, stock_qty, serial_no)
)
+
return bin_details_and_serial_nos
@@ -1257,6 +1246,7 @@
)
serial_no = get_serial_no(args)
batch_qty_and_serial_no.update({"serial_no": serial_no})
+
return batch_qty_and_serial_no
@@ -1329,7 +1319,6 @@
def apply_price_list_on_item(args):
item_doc = frappe.db.get_value("Item", args.item_code, ["name", "variant_of"], as_dict=1)
item_details = get_price_list_rate(args, item_doc)
-
item_details.update(get_pricing_rule_for_item(args))
return item_details
@@ -1413,12 +1402,12 @@
) or {"valuation_rate": 0}
elif not item.get("is_stock_item"):
- valuation_rate = frappe.db.sql(
- """select sum(base_net_amount) / sum(qty*conversion_factor)
- from `tabPurchase Invoice Item`
- where item_code = %s and docstatus=1""",
- item_code,
- )
+ pi_item = frappe.qb.DocType("Purchase Invoice Item")
+ valuation_rate = (
+ frappe.qb.from_(pi_item)
+ .select((Sum(pi_item.base_net_amount) / Sum(pi_item.qty * pi_item.conversion_factor)))
+ .where((pi_item.docstatus == 1) & (pi_item.item_code == item_code))
+ ).run()
if valuation_rate:
return {"valuation_rate": valuation_rate[0][0] or 0.0}
@@ -1444,7 +1433,7 @@
if args.get("warehouse") and args.get("stock_qty") and args.get("item_code"):
has_serial_no = frappe.get_value("Item", {"item_code": args.item_code}, "has_serial_no")
if args.get("batch_no") and has_serial_no == 1:
- return get_serial_no_batchwise(args, sales_order)
+ return get_serial_nos_by_fifo(args, sales_order)
elif has_serial_no == 1:
args = json.dumps(
{
@@ -1476,31 +1465,35 @@
args = frappe._dict(json.loads(args))
blanket_order_details = None
- condition = ""
- if args.item_code:
- if args.customer and args.doctype == "Sales Order":
- condition = " and bo.customer=%(customer)s"
- elif args.supplier and args.doctype == "Purchase Order":
- condition = " and bo.supplier=%(supplier)s"
- if args.blanket_order:
- condition += " and bo.name =%(blanket_order)s"
- if args.transaction_date:
- condition += " and bo.to_date>=%(transaction_date)s"
- blanket_order_details = frappe.db.sql(
- """
- select boi.rate as blanket_order_rate, bo.name as blanket_order
- from `tabBlanket Order` bo, `tabBlanket Order Item` boi
- where bo.company=%(company)s and boi.item_code=%(item_code)s
- and bo.docstatus=1 and bo.name = boi.parent {0}
- """.format(
- condition
- ),
- args,
- as_dict=True,
+ if args.item_code:
+ bo = frappe.qb.DocType("Blanket Order")
+ bo_item = frappe.qb.DocType("Blanket Order Item")
+
+ query = (
+ frappe.qb.from_(bo)
+ .from_(bo_item)
+ .select(bo_item.rate.as_("blanket_order_rate"), bo.name.as_("blanket_order"))
+ .where(
+ (bo.company == args.company)
+ & (bo_item.item_code == args.item_code)
+ & (bo.docstatus == 1)
+ & (bo.name == bo_item.parent)
+ )
)
+ if args.customer and args.doctype == "Sales Order":
+ query = query.where(bo.customer == args.customer)
+ elif args.supplier and args.doctype == "Purchase Order":
+ query = query.where(bo.supplier == args.supplier)
+ if args.blanket_order:
+ query = query.where(bo.name == args.blanket_order)
+ if args.transaction_date:
+ query = query.where(bo.to_date >= args.transaction_date)
+
+ blanket_order_details = query.run(as_dict=True)
blanket_order_details = blanket_order_details[0] if blanket_order_details else ""
+
return blanket_order_details
@@ -1510,10 +1503,10 @@
if get_reserved_qty_for_so(args.get("against_sales_order"), args.get("item_code")):
reserved_so = args.get("against_sales_order")
elif args.get("against_sales_invoice"):
- sales_order = frappe.db.sql(
- """select sales_order from `tabSales Invoice Item` where
- parent=%s and item_code=%s""",
- (args.get("against_sales_invoice"), args.get("item_code")),
+ sales_order = frappe.db.get_all(
+ "Sales Invoice Item",
+ filters={"parent": args.get("against_sales_invoice"), "item_code": args.get("item_code")},
+ fields="sales_order",
)
if sales_order and sales_order[0]:
if get_reserved_qty_for_so(sales_order[0][0], args.get("item_code")):
@@ -1525,13 +1518,14 @@
def get_reserved_qty_for_so(sales_order, item_code):
- reserved_qty = frappe.db.sql(
- """select sum(qty) from `tabSales Order Item`
- where parent=%s and item_code=%s and ensure_delivery_based_on_produced_serial_no=1
- """,
- (sales_order, item_code),
+ reserved_qty = frappe.db.get_value(
+ "Sales Order Item",
+ filters={
+ "parent": sales_order,
+ "item_code": item_code,
+ "ensure_delivery_based_on_produced_serial_no": 1,
+ },
+ fieldname="sum(qty)",
)
- if reserved_qty and reserved_qty[0][0]:
- return reserved_qty[0][0]
- else:
- return 0
+
+ return reserved_qty or 0
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index 8b63c0f..da17cde 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -306,7 +306,7 @@
query = query.where(sle.item_code.isin(items))
for field in ["voucher_no", "batch_no", "project", "company"]:
- if filters.get(field):
+ if filters.get(field) and field not in inventory_dimension_fields:
query = query.where(sle[field] == filters.get(field))
query = apply_warehouse_filter(query, sle, filters)
diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py
index 14cedd2..439ed7a 100644
--- a/erpnext/stock/stock_balance.py
+++ b/erpnext/stock/stock_balance.py
@@ -121,7 +121,7 @@
and parenttype='Sales Order'
and item_code != parent_item
and exists (select * from `tabSales Order` so
- where name = dnpi_in.parent and docstatus = 1 and status != 'Closed')
+ where name = dnpi_in.parent and docstatus = 1 and status not in ('On Hold', 'Closed'))
) dnpi)
union
(select stock_qty as dnpi_qty, qty as so_item_qty,
@@ -131,7 +131,7 @@
and (so_item.delivered_by_supplier is null or so_item.delivered_by_supplier = 0)
and exists(select * from `tabSales Order` so
where so.name = so_item.parent and so.docstatus = 1
- and so.status != 'Closed'))
+ and so.status not in ('On Hold', 'Closed')))
) tab
where
so_item_qty >= so_item_delivered_qty
diff --git a/erpnext/stock/tests/test_get_item_details.py b/erpnext/stock/tests/test_get_item_details.py
new file mode 100644
index 0000000..b53e29e
--- /dev/null
+++ b/erpnext/stock/tests/test_get_item_details.py
@@ -0,0 +1,40 @@
+import json
+
+import frappe
+from frappe.test_runner import make_test_records
+from frappe.tests.utils import FrappeTestCase
+
+from erpnext.stock.get_item_details import get_item_details
+
+test_ignore = ["BOM"]
+test_dependencies = ["Customer", "Supplier", "Item", "Price List", "Item Price"]
+
+
+class TestGetItemDetail(FrappeTestCase):
+ def setUp(self):
+ make_test_records("Price List")
+ super().setUp()
+
+ def test_get_item_detail_purchase_order(self):
+
+ args = frappe._dict(
+ {
+ "item_code": "_Test Item",
+ "company": "_Test Company",
+ "customer": "_Test Customer",
+ "conversion_rate": 1.0,
+ "price_list_currency": "USD",
+ "plc_conversion_rate": 1.0,
+ "doctype": "Purchase Order",
+ "name": None,
+ "supplier": "_Test Supplier",
+ "transaction_date": None,
+ "conversion_rate": 1.0,
+ "price_list": "_Test Buying Price List",
+ "is_subcontracted": 0,
+ "ignore_pricing_rule": 1,
+ "qty": 1,
+ }
+ )
+ details = get_item_details(args)
+ self.assertEqual(details.get("price_list_rate"), 100)
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
index d054ce0..6a2983f 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
@@ -2,6 +2,7 @@
# See license.txt
import copy
+from collections import defaultdict
import frappe
from frappe.tests.utils import FrappeTestCase
@@ -186,6 +187,40 @@
)
self.assertEqual(len(ste.items), len(rm_items))
+ def test_make_rm_stock_entry_for_batch_items_with_less_transfer(self):
+ set_backflush_based_on("BOM")
+
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 4",
+ "qty": 5,
+ "rate": 100,
+ "fg_item": "Subcontracted Item SA4",
+ "fg_item_qty": 5,
+ }
+ ]
+
+ sco = get_subcontracting_order(service_items=service_items)
+ rm_items = get_rm_items(sco.supplied_items)
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+
+ itemwise_transfer_qty = defaultdict(int)
+ for item in rm_items:
+ item["qty"] -= 1
+ itemwise_transfer_qty[item["item_code"]] += item["qty"]
+
+ ste = make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+
+ scr = make_subcontracting_receipt(sco.name)
+
+ for row in scr.supplied_items:
+ self.assertEqual(row.consumed_qty, itemwise_transfer_qty.get(row.rm_item_code) + 1)
+
def test_update_reserved_qty_for_subcontracting(self):
# Create RM Material Receipt
make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=10, basic_rate=100)
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
index b6bef8c..3a2c53f 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
@@ -51,13 +51,31 @@
}
}));
- frm.set_query("expense_account", "items", function () {
+ frm.set_query('expense_account', 'items', function () {
return {
- query: "erpnext.controllers.queries.get_expense_account",
+ query: 'erpnext.controllers.queries.get_expense_account',
filters: { 'company': frm.doc.company }
};
});
+ frm.set_query('batch_no', 'items', function(doc, cdt, cdn) {
+ var row = locals[cdt][cdn];
+ return {
+ filters: {
+ item: row.item_code
+ }
+ }
+ });
+
+ let batch_no_field = frm.get_docfield("items", "batch_no");
+ if (batch_no_field) {
+ batch_no_field.get_route_options_for_new_doc = function(row) {
+ return {
+ "item": row.doc.item_code
+ }
+ };
+ }
+
frappe.db.get_single_value('Buying Settings', 'backflush_raw_materials_of_subcontract_based_on').then(val => {
if (val == 'Material Transferred for Subcontract') {
frm.fields_dict['supplied_items'].grid.grid_rows.forEach((grid_row) => {
@@ -73,7 +91,7 @@
refresh: (frm) => {
if (frm.doc.docstatus > 0) {
- frm.add_custom_button(__("Stock Ledger"), function () {
+ frm.add_custom_button(__('Stock Ledger'), function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.posting_date,
@@ -81,8 +99,8 @@
company: frm.doc.company,
show_cancelled_entries: frm.doc.docstatus === 2
};
- frappe.set_route("query-report", "Stock Ledger");
- }, __("View"));
+ frappe.set_route('query-report', 'Stock Ledger');
+ }, __('View'));
frm.add_custom_button(__('Accounting Ledger'), function () {
frappe.route_options = {
@@ -90,11 +108,11 @@
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format('YYYY-MM-DD'),
company: frm.doc.company,
- group_by: "Group by Voucher (Consolidated)",
+ group_by: 'Group by Voucher (Consolidated)',
show_cancelled_entries: frm.doc.docstatus === 2
};
- frappe.set_route("query-report", "General Ledger");
- }, __("View"));
+ frappe.set_route('query-report', 'General Ledger');
+ }, __('View'));
}
if (!frm.doc.is_return && frm.doc.docstatus == 1 && frm.doc.per_returned < 100) {
@@ -111,25 +129,25 @@
frm.add_custom_button(__('Subcontracting Order'), function () {
if (!frm.doc.supplier) {
frappe.throw({
- title: __("Mandatory"),
- message: __("Please Select a Supplier")
+ title: __('Mandatory'),
+ message: __('Please Select a Supplier')
});
}
erpnext.utils.map_current_doc({
method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt',
- source_doctype: "Subcontracting Order",
+ source_doctype: 'Subcontracting Order',
target: frm,
setters: {
supplier: frm.doc.supplier,
},
get_query_filters: {
docstatus: 1,
- per_received: ["<", 100],
+ per_received: ['<', 100],
company: frm.doc.company
}
});
- }, __("Get Items From"));
+ }, __('Get Items From'));
}
},
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
index f4fd4de..95dbc83 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -191,14 +191,17 @@
def validate_available_qty_for_consumption(self):
for item in self.get("supplied_items"):
+ precision = item.precision("consumed_qty")
if (
- item.available_qty_for_consumption and item.available_qty_for_consumption < item.consumed_qty
+ item.available_qty_for_consumption
+ and flt(item.available_qty_for_consumption, precision) - flt(item.consumed_qty, precision) < 0
):
- frappe.throw(
- _(
- "Row {0}: Consumed Qty must be less than or equal to Available Qty For Consumption in Consumed Items Table."
- ).format(item.idx)
- )
+ msg = f"""Row {item.idx}: Consumed Qty {flt(item.consumed_qty, precision)}
+ must be less than or equal to Available Qty For Consumption
+ {flt(item.available_qty_for_consumption, precision)}
+ in Consumed Items Table."""
+
+ frappe.throw(_(msg))
def validate_items_qty(self):
for item in self.items:
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
index 7f4e9ef..2a078c4 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
@@ -13,8 +13,8 @@
get_datetime,
get_datetime_str,
get_link_to_form,
+ get_system_timezone,
get_time,
- get_time_zone,
get_weekdays,
getdate,
nowdate,
@@ -981,7 +981,7 @@
def get_tz(user):
- return frappe.db.get_value("User", user, "time_zone") or get_time_zone()
+ return frappe.db.get_value("User", user, "time_zone") or get_system_timezone()
@frappe.whitelist()
diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html
index 6b354b2..bc34ad5 100644
--- a/erpnext/templates/pages/order.html
+++ b/erpnext/templates/pages/order.html
@@ -34,16 +34,18 @@
</a>
</ul>
</div>
- <div class="form-column col-sm-6">
- <div class="page-header-actions-block" data-html-block="header-actions">
- <p>
- <a href="/api/method/erpnext.accounts.doctype.payment_request.payment_request.make_payment_request?dn={{ doc.name }}&dt={{ doc.doctype }}&submit_doc=1&order_type=Shopping Cart"
- class="btn btn-primary btn-sm" id="pay-for-order">
- {{ _("Pay") }} {{doc.get_formatted("grand_total") }}
- </a>
- </p>
+ {% if show_pay_button %}
+ <div class="form-column col-sm-6">
+ <div class="page-header-actions-block" data-html-block="header-actions">
+ <p>
+ <a href="/api/method/erpnext.accounts.doctype.payment_request.payment_request.make_payment_request?dn={{ doc.name }}&dt={{ doc.doctype }}&submit_doc=1&order_type=Shopping Cart"
+ class="btn btn-primary btn-sm" id="pay-for-order">
+ {{ _("Pay") }} {{doc.get_formatted("grand_total") }}
+ </a>
+ </p>
+ </div>
</div>
- </div>
+ {% endif %}
</div>
{% endblock %}
diff --git a/erpnext/templates/pages/order.py b/erpnext/templates/pages/order.py
index 185ec66..13772d3 100644
--- a/erpnext/templates/pages/order.py
+++ b/erpnext/templates/pages/order.py
@@ -55,6 +55,7 @@
)
context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points"))
+ context.show_pay_button = frappe.db.get_single_value("Buying Settings", "show_pay_button")
context.show_make_pi_button = False
if context.doc.get("supplier"):
# show Make Purchase Invoice button based on permission
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index 5a0a863..bec3ce2 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -9916,3 +9916,5 @@
Delivered at Place,Geliefert benannter Ort,
Delivered at Place Unloaded,Geliefert benannter Ort entladen,
Delivered Duty Paid,Geliefert verzollt,
+Discount Validity,Frist für den Rabatt,
+Discount Validity Based On,Frist für den Rabatt berechnet sich nach,
diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py
index 13b7877..62033a5 100644
--- a/erpnext/utilities/doctype/video/video.py
+++ b/erpnext/utilities/doctype/video/video.py
@@ -10,6 +10,7 @@
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint
+from frappe.utils.data import get_system_timezone
from pyyoutube import Api
@@ -64,7 +65,7 @@
frequency = get_frequency(frequency)
time = datetime.now()
- timezone = pytz.timezone(frappe.utils.get_time_zone())
+ timezone = pytz.timezone(get_system_timezone())
site_time = time.astimezone(timezone)
if frequency == 30:
diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py
index dfca946..f50c207 100644
--- a/erpnext/www/book_appointment/index.py
+++ b/erpnext/www/book_appointment/index.py
@@ -4,6 +4,7 @@
import frappe
import pytz
from frappe import _
+from frappe.utils.data import get_system_timezone
WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
@@ -125,7 +126,7 @@
def convert_to_guest_timezone(guest_tz, datetimeobject):
guest_tz = pytz.timezone(guest_tz)
- local_timezone = pytz.timezone(frappe.utils.get_time_zone())
+ local_timezone = pytz.timezone(get_system_timezone())
datetimeobject = local_timezone.localize(datetimeobject)
datetimeobject = datetimeobject.astimezone(guest_tz)
return datetimeobject
@@ -134,7 +135,7 @@
def convert_to_system_timezone(guest_tz, datetimeobject):
guest_tz = pytz.timezone(guest_tz)
datetimeobject = guest_tz.localize(datetimeobject)
- system_tz = pytz.timezone(frappe.utils.get_time_zone())
+ system_tz = pytz.timezone(get_system_timezone())
datetimeobject = datetimeobject.astimezone(system_tz)
return datetimeobject
diff --git a/pyproject.toml b/pyproject.toml
index 1b342a5..0718e5b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,8 +10,8 @@
dependencies = [
# Core dependencies
"pycountry~=20.7.3",
- "python-stdnum~=1.16",
"Unidecode~=1.2.0",
+ "barcodenumber~=0.5.0",
# integration dependencies
"gocardless-pro~=1.22.0",
@@ -28,9 +28,6 @@
requires = ["flit_core >=3.4,<4"]
build-backend = "flit_core.buildapi"
-[tool.bench.dev-dependencies]
-hypothesis = "~=6.31.0"
-
[tool.black]
line-length = 99