Merge branch 'develop' of https://github.com/frappe/erpnext into opening_entry
This commit is contained in:
commit
719ac5c8f3
@ -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/maintenance/ @rohitwaghchaure @s-aga-r
|
||||
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
|
||||
|
@ -65,7 +65,7 @@ New passwords will be created for the ERPNext "Administrator" user, the MariaDB
|
||||
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
|
||||
|
@ -29,6 +29,7 @@ def create_charts(
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
"account_currency",
|
||||
]:
|
||||
|
||||
account_number = cstr(child.get("account_number")).strip()
|
||||
@ -95,7 +96,17 @@ def identify_is_group(child):
|
||||
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 @@ def get_account_tree_from_existing_company(existing_company):
|
||||
"root_type",
|
||||
"tax_rate",
|
||||
"account_number",
|
||||
"account_currency",
|
||||
],
|
||||
order_by="lft, rgt",
|
||||
)
|
||||
@ -267,6 +279,7 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
"account_currency",
|
||||
]:
|
||||
continue
|
||||
|
||||
|
@ -118,6 +118,10 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
|
||||
}
|
||||
|
||||
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' });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -155,7 +155,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
}
|
||||
},
|
||||
|
||||
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 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
currency: frm.currency,
|
||||
}
|
||||
);
|
||||
}, 500),
|
||||
},
|
||||
|
||||
render(frm) {
|
||||
if (frm.doc.bank_account) {
|
||||
|
@ -10,7 +10,7 @@ from frappe.model.document import Document
|
||||
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 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
|
||||
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 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
|
||||
@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 @@ def get_account_balance(bank_account, till_date):
|
||||
|
||||
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
|
||||
|
||||
total_debit, total_credit = 0, 0
|
||||
total_debit, total_credit = 0.0, 0.0
|
||||
for d in data:
|
||||
total_debit += flt(d.debit)
|
||||
total_credit += flt(d.credit)
|
||||
@ -131,10 +131,8 @@ def create_journal_entry_bts(
|
||||
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 @@ def create_journal_entry_bts(
|
||||
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 @@ def create_journal_entry_bts(
|
||||
{
|
||||
"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 @@ def create_journal_entry_bts(
|
||||
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 @@ def create_payment_entry_bts(
|
||||
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 @@ def create_payment_entry_bts(
|
||||
|
||||
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 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
|
||||
# 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 @@ def get_linked_payments(
|
||||
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 @@ def get_linked_payments(
|
||||
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 @@ def check_matching(
|
||||
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 @@ def check_matching(
|
||||
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 @@ def check_matching(
|
||||
|
||||
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 @@ def get_queries(
|
||||
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 @@ def get_queries(
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
amount_condition,
|
||||
exact_match,
|
||||
account_from_to,
|
||||
from_date,
|
||||
to_date,
|
||||
@ -520,7 +496,7 @@ def get_matching_queries(
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
amount_condition,
|
||||
exact_match,
|
||||
account_from_to,
|
||||
from_date,
|
||||
to_date,
|
||||
@ -530,8 +506,8 @@ def get_matching_queries(
|
||||
):
|
||||
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 @@ def get_matching_queries(
|
||||
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 @@ def get_matching_queries(
|
||||
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 @@ def get_ld_matching_query(bank_account, amount_condition, filters):
|
||||
.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 @@ def get_lr_matching_query(bank_account, amount_condition, filters):
|
||||
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_lr_matching_query(bank_account, amount_condition, filters):
|
||||
|
||||
|
||||
def get_pe_matching_query(
|
||||
amount_condition,
|
||||
exact_match,
|
||||
account_from_to,
|
||||
transaction,
|
||||
from_date,
|
||||
@ -669,7 +681,7 @@ def get_pe_matching_query(
|
||||
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 @@ def get_pe_matching_query(
|
||||
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 @@ def get_pe_matching_query(
|
||||
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 @@ def get_je_matching_query(
|
||||
# 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 @@ def get_je_matching_query(
|
||||
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_je_matching_query(
|
||||
"""
|
||||
|
||||
|
||||
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 @@ def get_si_matching_query(amount_condition):
|
||||
`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 @@ def get_pi_matching_query(amount_condition):
|
||||
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'}
|
||||
"""
|
||||
|
@ -12,8 +12,13 @@ frappe.ui.form.on("Bank Transaction", {
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
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 @@ frappe.ui.form.on("Bank Transaction", {
|
||||
"Journal Entry",
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Bank Transaction",
|
||||
];
|
||||
}
|
||||
});
|
||||
@ -49,7 +55,7 @@ const update_clearance_date = (frm, cdt, cdn) => {
|
||||
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") {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 @@ class BankTransaction(StatusUpdater):
|
||||
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 @@ def get_doctypes_for_bank_reconciliation():
|
||||
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 @@ def get_paid_amount(payment_entry, currency, bank_account):
|
||||
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 @@ def get_paid_amount(payment_entry, currency, bank_account):
|
||||
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 @@ def get_paid_amount(payment_entry, currency, bank_account):
|
||||
)
|
||||
|
||||
|
||||
@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
|
||||
|
@ -36,7 +36,7 @@ def validate_columns(data):
|
||||
|
||||
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 @@ def build_forest(data):
|
||||
is_group,
|
||||
account_type,
|
||||
root_type,
|
||||
account_currency,
|
||||
) = i
|
||||
|
||||
if not account_name:
|
||||
@ -253,6 +254,8 @@ def build_forest(data):
|
||||
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 @@ def get_template(template_type):
|
||||
"Is Group",
|
||||
"Account Type",
|
||||
"Root Type",
|
||||
"Account Currency",
|
||||
]
|
||||
writer = UnicodeWriter()
|
||||
writer.writerow(fields)
|
||||
|
@ -211,8 +211,7 @@ class ExchangeRateRevaluation(Document):
|
||||
# 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 @@ class ExchangeRateRevaluation(Document):
|
||||
|
||||
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 @@ class ExchangeRateRevaluation(Document):
|
||||
}
|
||||
)
|
||||
|
||||
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 @@ class ExchangeRateRevaluation(Document):
|
||||
"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()
|
||||
|
@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
|
||||
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) {
|
||||
|
@ -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",
|
||||
|
@ -89,7 +89,13 @@ class JournalEntry(AccountsController):
|
||||
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 @@ class JournalEntry(AccountsController):
|
||||
):
|
||||
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 @@ class JournalEntry(AccountsController):
|
||||
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 (
|
||||
|
@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges";
|
||||
|
||||
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);
|
||||
|
@ -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",
|
||||
|
@ -92,7 +92,13 @@ class PaymentEntry(AccountsController):
|
||||
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()
|
||||
|
@ -368,6 +368,7 @@ class PaymentReconciliation(Document):
|
||||
"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),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -45,21 +45,20 @@ class PaymentRequest(Document):
|
||||
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 @@ def get_amount(ref_doc, payment_account=None):
|
||||
"""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"))
|
||||
|
||||
|
@ -45,7 +45,10 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
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,
|
||||
|
@ -21,8 +21,24 @@ class POSClosingEntry(StatusUpdater):
|
||||
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:
|
||||
|
@ -161,7 +161,7 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
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 @@ class POSInvoice(SalesInvoice):
|
||||
).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 @@ class POSInvoice(SalesInvoice):
|
||||
),
|
||||
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 @@ def get_bundle_availability(bundle_item_code, warehouse):
|
||||
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"
|
||||
):
|
||||
|
@ -17,6 +17,22 @@ class POSInvoiceMergeLog(Document):
|
||||
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 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
|
||||
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
|
||||
|
||||
|
@ -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",
|
||||
|
@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
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
|
||||
|
@ -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 @@ class PurchaseInvoice(BuyingController):
|
||||
"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 @@ class PurchaseInvoice(BuyingController):
|
||||
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 @@ class PurchaseInvoice(BuyingController):
|
||||
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
|
||||
|
@ -1523,6 +1523,94 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
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")
|
||||
|
@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
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
|
||||
|
@ -397,6 +397,8 @@ class SalesInvoice(SellingController):
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Repost Item Valuation",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
"Payment Ledger Entry",
|
||||
)
|
||||
|
||||
|
@ -278,7 +278,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
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 @@ def is_valid_certificate(
|
||||
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
|
||||
|
@ -135,6 +135,34 @@ def get_assets(filters):
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and ads.asset = a.name and ads.docstatus=1 and ads.name = ds.parent and ifnull(ds.journal_entry, '') != ''
|
||||
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
|
||||
|
@ -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>
|
||||
|
@ -395,6 +395,7 @@ def get_column_names():
|
||||
|
||||
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 @@ class GrossProfitGenerator(object):
|
||||
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 @@ class GrossProfitGenerator(object):
|
||||
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 @@ class GrossProfitGenerator(object):
|
||||
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 @@ class GrossProfitGenerator(object):
|
||||
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 @@ class GrossProfitGenerator(object):
|
||||
"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 = {}
|
||||
|
@ -209,62 +209,62 @@ frappe.ui.form.on('Asset', {
|
||||
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({
|
||||
|
@ -413,11 +413,14 @@ class Asset(AccountsController):
|
||||
|
||||
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 @@ class Asset(AccountsController):
|
||||
|
||||
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 @@ class Asset(AccountsController):
|
||||
.where(gle.debit != 0)
|
||||
.where(gle.is_cancelled == 0)
|
||||
.orderby(gle.posting_date)
|
||||
.orderby(gle.creation)
|
||||
).run(as_dict=True)
|
||||
|
||||
return records
|
||||
|
@ -168,7 +168,7 @@ def make_depreciation_entry(asset_depr_schedule_name, date=None):
|
||||
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()
|
||||
|
||||
|
@ -43,9 +43,9 @@ erpnext.asset.set_accumulated_depreciation = function(frm) {
|
||||
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);
|
||||
})
|
||||
};
|
||||
|
@ -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",
|
||||
|
@ -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 @@ class AssetDepreciationSchedule(Document):
|
||||
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 @@ class AssetDepreciationSchedule(Document):
|
||||
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 @@ class AssetDepreciationSchedule(Document):
|
||||
):
|
||||
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 @@ class AssetDepreciationSchedule(Document):
|
||||
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 @@ class AssetDepreciationSchedule(Document):
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
|
@ -91,6 +91,9 @@ class AssetRepair(AccountsController):
|
||||
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."))
|
||||
|
@ -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 @@ def get_data(filters):
|
||||
]
|
||||
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 @@ def prepare_chart_data(data, filters):
|
||||
filters.filter_based_on,
|
||||
"Monthly",
|
||||
company=filters.company,
|
||||
ignore_fiscal_year=True,
|
||||
)
|
||||
|
||||
for d in period_list:
|
||||
@ -194,7 +191,7 @@ def get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters):
|
||||
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):
|
||||
|
@ -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",
|
||||
|
@ -21,3 +21,10 @@ class BuyingSettings(Document):
|
||||
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
|
||||
|
@ -124,12 +124,11 @@ frappe.ui.form.on("Request for Quotation",{
|
||||
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()
|
||||
)
|
||||
);
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@ -388,24 +389,26 @@ def create_rfq_items(sq_doc, supplier, data):
|
||||
|
||||
|
||||
@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()
|
||||
|
@ -8,6 +8,7 @@ from frappe.utils import nowdate
|
||||
|
||||
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 @@ class TestRequestforQuotation(FrappeTestCase):
|
||||
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):
|
||||
"""
|
||||
|
@ -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": [
|
||||
|
@ -22,14 +22,14 @@ frappe.query_reports["Subcontracted Item To Be Received"] = {
|
||||
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
|
||||
},
|
||||
]
|
||||
|
@ -22,14 +22,14 @@ frappe.query_reports["Subcontracted Raw Materials To Be Transferred"] = {
|
||||
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
|
||||
},
|
||||
]
|
||||
|
@ -204,6 +204,12 @@ class AccountsController(TransactionBase):
|
||||
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")
|
||||
|
@ -265,7 +265,10 @@ class BuyingController(SubcontractingController):
|
||||
) / 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
|
||||
|
@ -131,7 +131,7 @@ def validate_returned_items(doc):
|
||||
)
|
||||
|
||||
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 @@ def get_already_returned_items(doc):
|
||||
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 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
|
||||
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 @@ def get_filters(
|
||||
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 @@ def get_returned_serial_nos(child_doc, parent_doc):
|
||||
|
||||
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 @@ def get_returned_serial_nos(child_doc, parent_doc):
|
||||
]
|
||||
|
||||
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
|
||||
|
@ -84,6 +84,9 @@ class SellingController(StockController):
|
||||
)
|
||||
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 @@ class SellingController(StockController):
|
||||
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"))
|
||||
|
@ -409,7 +409,14 @@ class SubcontractingController(StockController):
|
||||
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
|
||||
|
@ -24,11 +24,19 @@ class calculate_taxes_and_totals(object):
|
||||
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 @@ class calculate_taxes_and_totals(object):
|
||||
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 @@ class calculate_taxes_and_totals(object):
|
||||
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 @@ class calculate_taxes_and_totals(object):
|
||||
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 @@ class calculate_taxes_and_totals(object):
|
||||
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 @@ class calculate_taxes_and_totals(object):
|
||||
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 @@ class calculate_taxes_and_totals(object):
|
||||
]
|
||||
)
|
||||
|
||||
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 @@ class calculate_taxes_and_totals(object):
|
||||
# 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 @@ class calculate_taxes_and_totals(object):
|
||||
)
|
||||
|
||||
# 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 @@ class calculate_taxes_and_totals(object):
|
||||
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 @@ class calculate_taxes_and_totals(object):
|
||||
|
||||
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 @@ class calculate_taxes_and_totals(object):
|
||||
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")
|
||||
)
|
||||
|
@ -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
|
||||
}
|
@ -19,10 +19,6 @@ frappe.ui.form.on("Opportunity", {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (frm.doc.opportunity_from && frm.doc.party_name){
|
||||
frm.trigger('set_contact_link');
|
||||
}
|
||||
},
|
||||
|
||||
validate: function(frm) {
|
||||
@ -130,6 +126,10 @@ frappe.ui.form.on("Opportunity", {
|
||||
} 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.ui.form.on("Opportunity", {
|
||||
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'}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -12,7 +12,7 @@ class PlaidConnector:
|
||||
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,
|
||||
|
@ -47,7 +47,7 @@ erpnext.integrations.plaidLink = class plaidLink {
|
||||
}
|
||||
|
||||
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();
|
||||
|
@ -70,7 +70,8 @@ def add_bank_accounts(response, bank, company):
|
||||
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 @@ def sync_transactions(bank, bank_account):
|
||||
)
|
||||
|
||||
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 @@ def new_bank_transaction(transaction):
|
||||
|
||||
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 @@ def new_bank_transaction(transaction):
|
||||
"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 @@ def new_bank_transaction(transaction):
|
||||
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 enqueue_synchronization():
|
||||
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))
|
||||
|
@ -125,6 +125,8 @@ class TestPlaidSettings(unittest.TestCase):
|
||||
"unofficial_currency_code": None,
|
||||
"name": "INTRST PYMNT",
|
||||
"transaction_type": "place",
|
||||
"transaction_code": "direct debit",
|
||||
"check_number": "3456789",
|
||||
"amount": -4.22,
|
||||
"location": {
|
||||
"city": None,
|
||||
|
@ -311,15 +311,10 @@ doc_events = {
|
||||
"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 @@ doc_events = {
|
||||
"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 @@ auto_cancel_exempted_doctypes = [
|
||||
|
||||
scheduler_events = {
|
||||
"cron": {
|
||||
"0/5 * * * *": [
|
||||
"0/15 * * * *": [
|
||||
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
|
||||
],
|
||||
"0/30 * * * *": [
|
||||
|
@ -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
|
||||
}
|
@ -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",
|
||||
|
@ -212,7 +212,7 @@ def resume_bom_cost_update_jobs():
|
||||
["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 @@ def get_processed_current_boms(
|
||||
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}
|
||||
|
@ -561,7 +561,34 @@ class JobCard(Document):
|
||||
)
|
||||
|
||||
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 @@ class JobCard(Document):
|
||||
exc=JobCardOverTransferError,
|
||||
)
|
||||
|
||||
for row in ste_doc.items:
|
||||
if not row.job_card_item:
|
||||
continue
|
||||
|
||||
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]
|
||||
job_card_items_transferred_qty = _get_job_card_items_transferred_qty(ste_doc)
|
||||
|
||||
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."
|
||||
|
@ -25,8 +25,9 @@ frappe.query_reports["BOM Stock Report"] = {
|
||||
],
|
||||
"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>`;
|
||||
|
@ -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_columns():
|
||||
|
||||
|
||||
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()
|
||||
|
@ -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
|
@ -250,18 +250,14 @@ erpnext.patches.v13_0.item_naming_series_not_mandatory
|
||||
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.v13_0.create_accounting_dimensions_for_asset_repair
|
||||
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 @@ erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
|
||||
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.remove_hr_and_payroll_modules # 20-07-2022
|
||||
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.v14_0.set_pick_list_status
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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()
|
@ -17,10 +17,11 @@ def execute():
|
||||
|
||||
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 @@ def delete_auto_email_reports(report):
|
||||
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)
|
||||
|
@ -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)
|
@ -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()
|
@ -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")
|
@ -27,7 +27,13 @@ def get_details_of_draft_or_submitted_depreciable_assets():
|
||||
|
||||
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)
|
||||
|
25
erpnext/patches/v15_0/delete_saudi_doctypes.py
Normal file
25
erpnext/patches/v15_0/delete_saudi_doctypes.py
Normal file
@ -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",
|
||||
)
|
12
erpnext/patches/v15_0/saudi_depreciation_warning.py
Normal file
12
erpnext/patches/v15_0/saudi_depreciation_warning.py
Normal file
@ -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",
|
||||
)
|
@ -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
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
@ -182,6 +182,9 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager {
|
||||
);
|
||||
} 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);
|
||||
|
||||
|
@ -20,7 +20,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
doctype: "Bank Transaction",
|
||||
filters: { name: this.bank_transaction_name },
|
||||
fieldname: [
|
||||
"date as reference_date",
|
||||
"date",
|
||||
"deposit",
|
||||
"withdrawal",
|
||||
"currency",
|
||||
@ -33,6 +33,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
"party",
|
||||
"unallocated_amount",
|
||||
"allocated_amount",
|
||||
"transaction_type",
|
||||
],
|
||||
},
|
||||
callback: (r) => {
|
||||
@ -41,11 +42,23 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
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 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
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 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
{
|
||||
name: __("Document Name"),
|
||||
editable: false,
|
||||
width: 150,
|
||||
width: 1,
|
||||
},
|
||||
{
|
||||
name: __("Reference Date"),
|
||||
@ -112,25 +124,19 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
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,
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -224,6 +230,16 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
fieldname: "exact_match",
|
||||
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",
|
||||
@ -289,7 +305,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
fieldtype: "Column Break",
|
||||
},
|
||||
{
|
||||
default: "Journal Entry Type",
|
||||
default: "Bank Entry",
|
||||
fieldname: "journal_entry_type",
|
||||
fieldtype: "Select",
|
||||
label: "Journal Entry Type",
|
||||
@ -364,7 +380,12 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
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 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
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 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
options: "Currency",
|
||||
read_only: 1,
|
||||
},
|
||||
|
||||
{
|
||||
fieldname: "unallocated_amount",
|
||||
fieldtype: "Currency",
|
||||
@ -593,4 +613,4 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
@ -91,6 +91,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
_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 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
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 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
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 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
});
|
||||
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 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
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 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
});
|
||||
|
||||
$.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 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
// 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 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
// 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 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
|
||||
_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 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
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 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
|
||||
// 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 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
filtered_items() {
|
||||
return this.frm.doc.items.filter(item => !item["is_alternative"]);
|
||||
}
|
||||
};
|
||||
|
@ -488,7 +488,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
() => {
|
||||
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 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
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();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -221,9 +221,9 @@ $.extend(erpnext.utils, {
|
||||
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 @@ $.extend(erpnext.utils, {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
@ -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) {
|
||||
|
||||
// }
|
||||
});
|
@ -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
|
||||
}
|
@ -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
|
@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2021, Havenir Solutions and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestKSAVATSalesAccount(unittest.TestCase):
|
||||
pass
|
@ -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');
|
||||
}
|
||||
});
|
@ -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
|
||||
}
|
@ -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
|
@ -1,5 +0,0 @@
|
||||
frappe.listview_settings['KSA VAT Setting'] = {
|
||||
onload () {
|
||||
frappe.breadcrumbs.add('Accounts');
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user