Merge branch 'develop' into prateekkaramchandani/develop

This commit is contained in:
Ankush Menat
2023-03-14 18:39:06 +05:30
150 changed files with 6941 additions and 6349 deletions

View File

@@ -7,7 +7,6 @@ on:
- '**.css' - '**.css'
- '**.md' - '**.md'
- '**.html' - '**.html'
- '**.csv'
push: push:
branches: [ develop ] branches: [ develop ]
paths-ignore: paths-ignore:

View File

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

View File

@@ -29,6 +29,7 @@ def create_charts(
"root_type", "root_type",
"is_group", "is_group",
"tax_rate", "tax_rate",
"account_currency",
]: ]:
account_number = cstr(child.get("account_number")).strip() account_number = cstr(child.get("account_number")).strip()
@@ -95,7 +96,17 @@ def identify_is_group(child):
is_group = child.get("is_group") is_group = child.get("is_group")
elif len( elif len(
set(child.keys()) 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 is_group = 1
else: else:
@@ -185,6 +196,7 @@ def get_account_tree_from_existing_company(existing_company):
"root_type", "root_type",
"tax_rate", "tax_rate",
"account_number", "account_number",
"account_currency",
], ],
order_by="lft, rgt", order_by="lft, rgt",
) )
@@ -267,6 +279,7 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
"root_type", "root_type",
"is_group", "is_group",
"tax_rate", "tax_rate",
"account_currency",
]: ]:
continue continue

View File

@@ -118,6 +118,10 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
} }
plaid_success(token, response) { 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' });
});
} }
}; };

View File

@@ -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( frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager(
{ {
$reconciliation_tool_cards: frm.get_field( $reconciliation_tool_cards: frm.get_field(
@@ -167,7 +167,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
currency: frm.currency, currency: frm.currency,
} }
); );
}, 500), },
render(frm) { render(frm) {
if (frm.doc.bank_account) { if (frm.doc.bank_account) {

View File

@@ -10,7 +10,7 @@ from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.custom import ConstantColumn
from frappe.utils import cint, flt 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 ( from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
get_amounts_not_reflected_in_system, get_amounts_not_reflected_in_system,
get_entries, get_entries,
@@ -28,7 +28,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
filters = [] filters = []
filters.append(["bank_account", "=", bank_account]) filters.append(["bank_account", "=", bank_account])
filters.append(["docstatus", "=", 1]) filters.append(["docstatus", "=", 1])
filters.append(["unallocated_amount", ">", 0]) filters.append(["unallocated_amount", ">", 0.0])
if to_date: if to_date:
filters.append(["date", "<=", to_date]) filters.append(["date", "<=", to_date])
if from_date: if from_date:
@@ -58,7 +58,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
@frappe.whitelist() @frappe.whitelist()
def get_account_balance(bank_account, till_date): def get_account_balance(bank_account, till_date):
# returns account balance till the specified 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( filters = frappe._dict(
{"account": account, "report_date": till_date, "include_pos_transactions": 1} {"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"]) 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: for d in data:
total_debit += flt(d.debit) total_debit += flt(d.debit)
total_credit += flt(d.credit) total_credit += flt(d.credit)
@@ -131,10 +131,8 @@ def create_journal_entry_bts(
fieldname=["name", "deposit", "withdrawal", "bank_account"], fieldname=["name", "deposit", "withdrawal", "bank_account"],
as_dict=True, as_dict=True,
)[0] )[0]
company_account = frappe.get_cached_value( company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
"Bank Account", bank_transaction.bank_account, "account" account_type = frappe.db.get_value("Account", second_account, "account_type")
)
account_type = frappe.get_cached_value("Account", second_account, "account_type")
if account_type in ["Receivable", "Payable"]: if account_type in ["Receivable", "Payable"]:
if not (party_type and party): if not (party_type and party):
frappe.throw( frappe.throw(
@@ -147,10 +145,8 @@ def create_journal_entry_bts(
accounts.append( accounts.append(
{ {
"account": second_account, "account": second_account,
"credit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0, "credit_in_account_currency": bank_transaction.deposit,
"debit_in_account_currency": bank_transaction.withdrawal "debit_in_account_currency": bank_transaction.withdrawal,
if bank_transaction.withdrawal > 0
else 0,
"party_type": party_type, "party_type": party_type,
"party": party, "party": party,
} }
@@ -160,14 +156,12 @@ def create_journal_entry_bts(
{ {
"account": company_account, "account": company_account,
"bank_account": bank_transaction.bank_account, "bank_account": bank_transaction.bank_account,
"credit_in_account_currency": bank_transaction.withdrawal "credit_in_account_currency": bank_transaction.withdrawal,
if bank_transaction.withdrawal > 0 "debit_in_account_currency": bank_transaction.deposit,
else 0,
"debit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
} }
) )
company = frappe.get_cached_value("Account", company_account, "company") company = frappe.get_value("Account", company_account, "company")
journal_entry_dict = { journal_entry_dict = {
"voucher_type": entry_type, "voucher_type": entry_type,
@@ -187,16 +181,22 @@ def create_journal_entry_bts(
journal_entry.insert() journal_entry.insert()
journal_entry.submit() journal_entry.submit()
if bank_transaction.deposit > 0: if bank_transaction.deposit > 0.0:
paid_amount = bank_transaction.deposit paid_amount = bank_transaction.deposit
else: else:
paid_amount = bank_transaction.withdrawal paid_amount = bank_transaction.withdrawal
vouchers = json.dumps( 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() @frappe.whitelist()
@@ -220,12 +220,10 @@ def create_payment_entry_bts(
as_dict=True, as_dict=True,
)[0] )[0]
paid_amount = bank_transaction.unallocated_amount 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( company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
"Bank Account", bank_transaction.bank_account, "account" company = frappe.get_value("Account", company_account, "company")
)
company = frappe.get_cached_value("Account", company_account, "company")
payment_entry_dict = { payment_entry_dict = {
"company": company, "company": company,
"payment_type": payment_type, "payment_type": payment_type,
@@ -261,9 +259,15 @@ def create_payment_entry_bts(
payment_entry.submit() payment_entry.submit()
vouchers = json.dumps( 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() @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 # updated clear date of all the vouchers based on the bank transaction
vouchers = json.loads(vouchers) vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
company_account = frappe.get_cached_value("Bank Account", transaction.bank_account, "account") transaction.add_payment_entries(vouchers)
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()
return frappe.get_doc("Bank Transaction", bank_transaction_name) 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 = frappe.db.get_values(
"Bank Account", transaction.bank_account, ["account", "company"], as_dict=True "Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
)[0] )[0]
(account, company) = (bank_account.account, bank_account.company) (gl_account, company) = (bank_account.account, bank_account.company)
matching = check_matching( matching = check_matching(
account, gl_account,
company, company,
transaction, transaction,
document_types, document_types,
@@ -428,7 +380,27 @@ def get_linked_payments(
from_reference_date, from_reference_date,
to_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( def check_matching(
@@ -442,6 +414,7 @@ def check_matching(
from_reference_date, from_reference_date,
to_reference_date, to_reference_date,
): ):
exact_match = True if "exact_match" in document_types else False
# combine all types of vouchers # combine all types of vouchers
subquery = get_queries( subquery = get_queries(
bank_account, bank_account,
@@ -453,10 +426,11 @@ def check_matching(
filter_by_reference_date, filter_by_reference_date,
from_reference_date, from_reference_date,
to_reference_date, to_reference_date,
exact_match,
) )
filters = { filters = {
"amount": transaction.unallocated_amount, "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, "reference_no": transaction.reference_number,
"party_type": transaction.party_type, "party_type": transaction.party_type,
"party": transaction.party, "party": transaction.party,
@@ -465,7 +439,9 @@ def check_matching(
matching_vouchers = [] 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: for query in subquery:
matching_vouchers.extend( matching_vouchers.extend(
@@ -487,10 +463,10 @@ def get_queries(
filter_by_reference_date, filter_by_reference_date,
from_reference_date, from_reference_date,
to_reference_date, to_reference_date,
exact_match,
): ):
# get queries to get matching vouchers # get queries to get matching vouchers
amount_condition = "=" if "exact_match" in document_types else "<=" account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from"
queries = [] queries = []
# get matching queries from all the apps # get matching queries from all the apps
@@ -501,7 +477,7 @@ def get_queries(
company, company,
transaction, transaction,
document_types, document_types,
amount_condition, exact_match,
account_from_to, account_from_to,
from_date, from_date,
to_date, to_date,
@@ -520,7 +496,7 @@ def get_matching_queries(
company, company,
transaction, transaction,
document_types, document_types,
amount_condition, exact_match,
account_from_to, account_from_to,
from_date, from_date,
to_date, to_date,
@@ -530,8 +506,8 @@ def get_matching_queries(
): ):
queries = [] queries = []
if "payment_entry" in document_types: if "payment_entry" in document_types:
pe_amount_matching = get_pe_matching_query( query = get_pe_matching_query(
amount_condition, exact_match,
account_from_to, account_from_to,
transaction, transaction,
from_date, from_date,
@@ -540,11 +516,11 @@ def get_matching_queries(
from_reference_date, from_reference_date,
to_reference_date, to_reference_date,
) )
queries.extend([pe_amount_matching]) queries.append(query)
if "journal_entry" in document_types: if "journal_entry" in document_types:
je_amount_matching = get_je_matching_query( query = get_je_matching_query(
amount_condition, exact_match,
transaction, transaction,
from_date, from_date,
to_date, to_date,
@@ -552,34 +528,70 @@ def get_matching_queries(
from_reference_date, from_reference_date,
to_reference_date, to_reference_date,
) )
queries.extend([je_amount_matching]) queries.append(query)
if transaction.deposit > 0 and "sales_invoice" in document_types: if transaction.deposit > 0.0 and "sales_invoice" in document_types:
si_amount_matching = get_si_matching_query(amount_condition) query = get_si_matching_query(exact_match)
queries.extend([si_amount_matching]) queries.append(query)
if transaction.withdrawal > 0: if transaction.withdrawal > 0.0:
if "purchase_invoice" in document_types: if "purchase_invoice" in document_types:
pi_amount_matching = get_pi_matching_query(amount_condition) query = get_pi_matching_query(exact_match)
queries.extend([pi_amount_matching]) queries.append(query)
if "bank_transaction" in document_types:
query = get_bt_matching_query(exact_match, transaction)
queries.append(query)
return queries 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 = [] vouchers = []
amount_condition = True if "exact_match" in document_types else False
if transaction.withdrawal > 0 and "loan_disbursement" in document_types: if transaction.withdrawal > 0.0 and "loan_disbursement" in document_types:
vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters)) vouchers.extend(get_ld_matching_query(bank_account, exact_match, filters))
if transaction.deposit > 0 and "loan_repayment" in document_types: if transaction.deposit > 0.0 and "loan_repayment" in document_types:
vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters)) vouchers.extend(get_lr_matching_query(bank_account, exact_match, filters))
return vouchers 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") loan_disbursement = frappe.qb.DocType("Loan Disbursement")
matching_reference = loan_disbursement.reference_number == filters.get("reference_number") matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
matching_party = loan_disbursement.applicant_type == filters.get( 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) .where(loan_disbursement.disbursement_account == bank_account)
) )
if amount_condition: if exact_match:
query.where(loan_disbursement.disbursed_amount == filters.get("amount")) query.where(loan_disbursement.disbursed_amount == filters.get("amount"))
else: else:
query.where(loan_disbursement.disbursed_amount <= filters.get("amount")) query.where(loan_disbursement.disbursed_amount > 0.0)
vouchers = query.run(as_list=True) vouchers = query.run(as_list=True)
return vouchers 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") loan_repayment = frappe.qb.DocType("Loan Repayment")
matching_reference = loan_repayment.reference_number == filters.get("reference_number") matching_reference = loan_repayment.reference_number == filters.get("reference_number")
matching_party = loan_repayment.applicant_type == filters.get( 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"): if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_repayment.repay_from_salary == 0)) query = query.where((loan_repayment.repay_from_salary == 0))
if amount_condition: if exact_match:
query.where(loan_repayment.amount_paid == filters.get("amount")) query.where(loan_repayment.amount_paid == filters.get("amount"))
else: else:
query.where(loan_repayment.amount_paid <= filters.get("amount")) query.where(loan_repayment.amount_paid > 0.0)
vouchers = query.run() vouchers = query.run()
@@ -659,7 +671,7 @@ def get_lr_matching_query(bank_account, amount_condition, filters):
def get_pe_matching_query( def get_pe_matching_query(
amount_condition, exact_match,
account_from_to, account_from_to,
transaction, transaction,
from_date, from_date,
@@ -669,7 +681,7 @@ def get_pe_matching_query(
to_reference_date, to_reference_date,
): ):
# get matching payment entries query # get matching payment entries query
if transaction.deposit > 0: if transaction.deposit > 0.0:
currency_field = "paid_to_account_currency as currency" currency_field = "paid_to_account_currency as currency"
else: else:
currency_field = "paid_from_account_currency as currency" currency_field = "paid_from_account_currency as currency"
@@ -684,7 +696,8 @@ def get_pe_matching_query(
return f""" return f"""
SELECT SELECT
(CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END (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, + 1 ) AS rank,
'Payment Entry' as doctype, 'Payment Entry' as doctype,
name, name,
@@ -698,20 +711,19 @@ def get_pe_matching_query(
FROM FROM
`tabPayment Entry` `tabPayment Entry`
WHERE WHERE
paid_amount {amount_condition} %(amount)s docstatus = 1
AND docstatus = 1
AND payment_type IN (%(payment_type)s, 'Internal Transfer') AND payment_type IN (%(payment_type)s, 'Internal Transfer')
AND ifnull(clearance_date, '') = "" AND ifnull(clearance_date, '') = ""
AND {account_from_to} = %(bank_account)s AND {account_from_to} = %(bank_account)s
AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
{filter_by_date} {filter_by_date}
{filter_by_reference_no} {filter_by_reference_no}
order by{order_by} order by{order_by}
""" """
def get_je_matching_query( def get_je_matching_query(
amount_condition, exact_match,
transaction, transaction,
from_date, from_date,
to_date, to_date,
@@ -723,7 +735,7 @@ def get_je_matching_query(
# We have mapping at the bank level # We have mapping at the bank level
# So one bank could have both types of bank accounts like asset and liability # So one bank could have both types of bank accounts like asset and liability
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type # So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
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}'" filter_by_date = f"AND je.posting_date between '{from_date}' and '{to_date}'"
order_by = " je.posting_date" order_by = " je.posting_date"
filter_by_reference_no = "" filter_by_reference_no = ""
@@ -735,26 +747,29 @@ def get_je_matching_query(
return f""" return f"""
SELECT SELECT
(CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END (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 , + 1) AS rank ,
'Journal Entry' as doctype, 'Journal Entry' AS doctype,
je.name, je.name,
jea.{cr_or_dr}_in_account_currency as paid_amount, jea.{cr_or_dr}_in_account_currency AS paid_amount,
je.cheque_no as reference_no, je.cheque_no AS reference_no,
je.cheque_date as reference_date, je.cheque_date AS reference_date,
je.pay_to_recd_from as party, je.pay_to_recd_from AS party,
jea.party_type, jea.party_type,
je.posting_date, je.posting_date,
jea.account_currency as currency jea.account_currency AS currency
FROM FROM
`tabJournal Entry Account` as jea `tabJournal Entry Account` AS jea
JOIN JOIN
`tabJournal Entry` as je `tabJournal Entry` AS je
ON ON
jea.parent = je.name jea.parent = je.name
WHERE 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.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 AND je.docstatus = 1
{filter_by_date} {filter_by_date}
{filter_by_reference_no} {filter_by_reference_no}
@@ -762,11 +777,12 @@ def get_je_matching_query(
""" """
def get_si_matching_query(amount_condition): def get_si_matching_query(exact_match):
# get matchin sales invoice query # get matching sales invoice query
return f""" return f"""
SELECT 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, + 1 ) AS rank,
'Sales Invoice' as doctype, 'Sales Invoice' as doctype,
si.name, si.name,
@@ -784,18 +800,20 @@ def get_si_matching_query(amount_condition):
`tabSales Invoice` as si `tabSales Invoice` as si
ON ON
sip.parent = si.name 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.account = %(bank_account)s
AND sip.amount {amount_condition} %(amount)s AND sip.amount {'= %(amount)s' if exact_match else '> 0.0'}
AND si.docstatus = 1
""" """
def get_pi_matching_query(amount_condition): def get_pi_matching_query(exact_match):
# get matching purchase invoice query # get matching purchase invoice query when they are also used as payment entries (is_paid)
return f""" return f"""
SELECT SELECT
( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END ( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END
+ CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
+ 1 ) AS rank, + 1 ) AS rank,
'Purchase Invoice' as doctype, 'Purchase Invoice' as doctype,
name, name,
@@ -809,9 +827,9 @@ def get_pi_matching_query(amount_condition):
FROM FROM
`tabPurchase Invoice` `tabPurchase Invoice`
WHERE WHERE
paid_amount {amount_condition} %(amount)s docstatus = 1
AND docstatus = 1
AND is_paid = 1 AND is_paid = 1
AND ifnull(clearance_date, '') = "" 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'}
""" """

View File

@@ -12,8 +12,13 @@ frappe.ui.form.on("Bank Transaction", {
}; };
}); });
}, },
refresh(frm) {
bank_account: function(frm) { frm.add_custom_button(__('Unreconcile Transaction'), () => {
frm.call('remove_payment_entries')
.then( () => frm.refresh() );
});
},
bank_account: function (frm) {
set_bank_statement_filter(frm); set_bank_statement_filter(frm);
}, },
@@ -34,6 +39,7 @@ frappe.ui.form.on("Bank Transaction", {
"Journal Entry", "Journal Entry",
"Sales Invoice", "Sales Invoice",
"Purchase Invoice", "Purchase Invoice",
"Bank Transaction",
]; ];
} }
}); });
@@ -49,7 +55,7 @@ const update_clearance_date = (frm, cdt, cdn) => {
frappe frappe
.xcall( .xcall(
"erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment", "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) => { .then((e) => {
if (e == "success") { if (e == "success") {

View File

@@ -20,9 +20,11 @@
"currency", "currency",
"section_break_10", "section_break_10",
"description", "description",
"section_break_14",
"reference_number", "reference_number",
"column_break_10",
"transaction_id", "transaction_id",
"transaction_type",
"section_break_14",
"payment_entries", "payment_entries",
"section_break_18", "section_break_18",
"allocated_amount", "allocated_amount",
@@ -190,11 +192,21 @@
"label": "Withdrawal", "label": "Withdrawal",
"oldfieldname": "credit", "oldfieldname": "credit",
"options": "currency" "options": "currency"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "transaction_type",
"fieldtype": "Data",
"label": "Transaction Type",
"length": 50
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-03-21 19:05:04.208222", "modified": "2022-05-29 18:36:50.475964",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Transaction", "name": "Bank Transaction",
@@ -248,4 +260,4 @@
"states": [], "states": [],
"title_field": "bank_account", "title_field": "bank_account",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -1,9 +1,6 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
from functools import reduce
import frappe import frappe
from frappe.utils import flt from frappe.utils import flt
@@ -18,72 +15,137 @@ class BankTransaction(StatusUpdater):
self.clear_linked_payment_entries() self.clear_linked_payment_entries()
self.set_status() self.set_status()
_saving_flag = False
# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
def on_update_after_submit(self): def on_update_after_submit(self):
self.update_allocations() "Run on save(). Avoid recursion caused by multiple saves"
self.clear_linked_payment_entries() if not self._saving_flag:
self.set_status(update=True) self._saving_flag = True
self.clear_linked_payment_entries()
self.update_allocations()
self._saving_flag = False
def on_cancel(self): def on_cancel(self):
self.clear_linked_payment_entries(for_cancel=True) self.clear_linked_payment_entries(for_cancel=True)
self.set_status(update=True) self.set_status(update=True)
def update_allocations(self): def update_allocations(self):
"The doctype does not allow modifications after submission, so write to the db direct"
if self.payment_entries: if self.payment_entries:
allocated_amount = reduce( allocated_amount = sum(p.allocated_amount for p in self.payment_entries)
lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries]
)
else: else:
allocated_amount = 0 allocated_amount = 0.0
if allocated_amount: amount = abs(flt(self.withdrawal) - flt(self.deposit))
frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount)) self.db_set("allocated_amount", flt(allocated_amount))
frappe.db.set_value( self.db_set("unallocated_amount", amount - flt(allocated_amount))
self.doctype, self.reload()
self.name, self.set_status(update=True)
"unallocated_amount",
abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount),
)
else: def add_payment_entries(self, vouchers):
frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0) "Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
frappe.db.set_value( if 0.0 >= self.unallocated_amount:
self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit)) frappe.throw(frappe._(f"Bank Transaction {self.name} is already fully reconciled"))
)
amount = self.deposit or self.withdrawal added = False
if amount == self.allocated_amount: for voucher in vouchers:
frappe.db.set_value(self.doctype, self.name, "status", "Reconciled") # 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() 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: for payment_entry in self.payment_entries:
if payment_entry.payment_document == "Sales Invoice": self.remove_payment_entry(payment_entry)
self.clear_sales_invoice(payment_entry, for_cancel=for_cancel) # runs on_update_after_submit
elif payment_entry.payment_document in get_doctypes_for_bank_reconciliation(): self.save()
self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
def clear_simple_entry(self, payment_entry, for_cancel=False): def remove_payment_entry(self, payment_entry):
if payment_entry.payment_document == "Payment Entry": "Clear payment entry and clearance"
if ( self.clear_linked_payment_entry(payment_entry, for_cancel=True)
frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type") self.remove(payment_entry)
== "Internal Transfer"
):
if len(get_reconciled_bank_transactions(payment_entry)) < 2:
return
clearance_date = self.date if not for_cancel else None def clear_linked_payment_entries(self, for_cancel=False):
frappe.db.set_value( if for_cancel:
payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", clearance_date 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): def clear_linked_payment_entry(self, payment_entry, for_cancel=False):
clearance_date = self.date if not for_cancel else None clearance_date = None if for_cancel else self.date
frappe.db.set_value( set_voucher_clearance(
"Sales Invoice Payment", payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self
dict(parenttype=payment_entry.payment_document, parent=payment_entry.payment_entry),
"clearance_date",
clearance_date,
) )
@@ -93,38 +155,112 @@ def get_doctypes_for_bank_reconciliation():
return frappe.get_hooks("bank_reconciliation_doctypes") return frappe.get_hooks("bank_reconciliation_doctypes")
def get_reconciled_bank_transactions(payment_entry): def get_clearance_details(transaction, payment_entry):
reconciled_bank_transactions = frappe.get_all( """
"Bank Transaction Payments", There should only be one bank gle for a voucher.
filters={"payment_entry": payment_entry.payment_entry}, Could be none for a Bank Transaction.
fields=["parent"], 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): def get_related_bank_gl_entries(doctype, docname):
return frappe.db.sql( # nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
result = frappe.db.sql(
""" """
SELECT SELECT
SUM(btp.allocated_amount) as allocated_amount, ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount,
bt.name gle.account AS gl_account
FROM FROM
`tabBank Transaction Payments` as btp `tabGL Entry` gle
LEFT JOIN LEFT JOIN
`tabBank Transaction` bt ON bt.name=btp.parent `tabAccount` ac ON ac.name=gle.account
WHERE WHERE
btp.payment_document = %s ac.account_type = 'Bank'
AND AND gle.voucher_type = %(doctype)s
btp.payment_entry = %s AND gle.voucher_no = %(docname)s
AND AND is_cancelled = 0
bt.docstatus = 1""", """,
(payment_entry.payment_document, payment_entry.payment_entry), dict(doctype=doctype, docname=docname),
as_dict=True, 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"]: if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
paid_amount_field = "paid_amount" 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": elif payment_entry.payment_document == "Journal Entry":
return frappe.db.get_value( return frappe.db.get_value(
"Journal Entry Account", "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)", "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" 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: else:
frappe.throw( frappe.throw(
"Please reconcile {0}: {1} manually".format( "Please reconcile {0}: {1} manually".format(
@@ -174,18 +316,55 @@ def get_paid_amount(payment_entry, currency, bank_account):
) )
@frappe.whitelist() def set_voucher_clearance(doctype, docname, clearance_date, self):
def unclear_reference_payment(doctype, docname): if doctype in [
if frappe.db.exists(doctype, docname): "Payment Entry",
doc = frappe.get_doc(doctype, docname) "Journal Entry",
if doctype == "Sales Invoice": "Purchase Invoice",
frappe.db.set_value( "Expense Claim",
"Sales Invoice Payment", "Loan Repayment",
dict(parenttype=doc.payment_document, parent=doc.payment_entry), "Loan Disbursement",
"clearance_date", ]:
None, if (
) doctype == "Payment Entry"
else: and frappe.db.get_value("Payment Entry", docname, "payment_type") == "Internal Transfer"
frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None) 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

View File

@@ -36,7 +36,7 @@ def validate_columns(data):
no_of_columns = max([len(d) for d in data]) no_of_columns = max([len(d) for d in data])
if no_of_columns > 7: if no_of_columns > 8:
frappe.throw( frappe.throw(
_("More columns found than expected. Please compare the uploaded file with standard template"), _("More columns found than expected. Please compare the uploaded file with standard template"),
title=(_("Wrong Template")), title=(_("Wrong Template")),
@@ -233,6 +233,7 @@ def build_forest(data):
is_group, is_group,
account_type, account_type,
root_type, root_type,
account_currency,
) = i ) = i
if not account_name: if not account_name:
@@ -253,6 +254,8 @@ def build_forest(data):
charts_map[account_name]["account_type"] = account_type charts_map[account_name]["account_type"] = account_type
if root_type: if root_type:
charts_map[account_name]["root_type"] = 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] path = return_parent(data, account_name)[::-1]
paths.append(path) # List of path is created paths.append(path) # List of path is created
line_no += 1 line_no += 1
@@ -315,6 +318,7 @@ def get_template(template_type):
"Is Group", "Is Group",
"Account Type", "Account Type",
"Root Type", "Root Type",
"Account Currency",
] ]
writer = UnicodeWriter() writer = UnicodeWriter()
writer.writerow(fields) writer.writerow(fields)

View File

@@ -211,8 +211,7 @@ class ExchangeRateRevaluation(Document):
# Handle Accounts with '0' balance in Account/Base Currency # Handle Accounts with '0' balance in Account/Base Currency
for d in [x for x in account_details if x.zero_balance]: 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 current_exchange_rate = new_exchange_rate = 0
new_balance_in_account_currency = 0 # this will be '0' new_balance_in_account_currency = 0 # this will be '0'
@@ -399,6 +398,9 @@ class ExchangeRateRevaluation(Document):
journal_entry_accounts = [] journal_entry_accounts = []
for d in accounts: for d in accounts:
if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")):
continue
dr_or_cr = ( dr_or_cr = (
"debit_in_account_currency" "debit_in_account_currency"
if d.get("balance_in_account_currency") > 0 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, "account": unrealized_exchange_gain_loss_account,
"balance": get_balance_on(unrealized_exchange_gain_loss_account), "balance": get_balance_on(unrealized_exchange_gain_loss_account),
@@ -460,10 +468,9 @@ class ExchangeRateRevaluation(Document):
"exchange_rate": 1, "exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation", "reference_type": "Exchange Rate Revaluation",
"reference_name": self.name, "reference_name": self.name,
} },
) )
journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_amounts_in_company_currency() journal_entry.set_amounts_in_company_currency()
journal_entry.set_total_debit_credit() journal_entry.set_total_debit_credit()
journal_entry.save() journal_entry.save()

View File

@@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
frappe.ui.form.on("Journal Entry", { frappe.ui.form.on("Journal Entry", {
setup: function(frm) { setup: function(frm) {
frm.add_fetch("bank_account", "account", "account"); 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) { refresh: function(frm) {

View File

@@ -137,7 +137,8 @@
"fieldname": "finance_book", "fieldname": "finance_book",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Finance Book", "label": "Finance Book",
"options": "Finance Book" "options": "Finance Book",
"read_only": 1
}, },
{ {
"fieldname": "2_add_edit_gl_entries", "fieldname": "2_add_edit_gl_entries",
@@ -538,7 +539,7 @@
"idx": 176, "idx": 176,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-01-17 12:53:53.280620", "modified": "2023-03-01 14:58:59.286591",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry", "name": "Journal Entry",

View File

@@ -89,7 +89,13 @@ class JournalEntry(AccountsController):
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
unlink_ref_doc_from_payment_entries(self) 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.make_gl_entries(1)
self.update_advance_paid() self.update_advance_paid()
self.unlink_advance_entry_reference() self.unlink_advance_entry_reference()

View File

@@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges";
frappe.ui.form.on('Payment Entry', { frappe.ui.form.on('Payment Entry', {
onload: function(frm) { 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.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
@@ -217,7 +217,6 @@ frappe.ui.form.on('Payment Entry', {
frm.toggle_display("set_exchange_gain_loss", frm.toggle_display("set_exchange_gain_loss",
frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount); frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount);
frm.refresh_fields();
}, },
set_dynamic_labels: function(frm) { set_dynamic_labels: function(frm) {

View File

@@ -92,7 +92,13 @@ class PaymentEntry(AccountsController):
self.set_status() self.set_status()
def on_cancel(self): 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.make_gl_entries(cancel=1)
self.update_outstanding_amounts() self.update_outstanding_amounts()
self.update_advance_paid() self.update_advance_paid()

View File

@@ -368,6 +368,7 @@ class PaymentReconciliation(Document):
"exchange_rate": 1, "exchange_rate": 1,
"cost_center": erpnext.get_default_cost_center(self.company), "cost_center": erpnext.get_default_cost_center(self.company),
reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount), reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount),
reverse_dr_or_cr: flt(row.difference_amount),
} }
) )

View File

@@ -495,26 +495,22 @@ def get_amount(ref_doc, payment_account=None):
"""get amount based on doctype""" """get amount based on doctype"""
dt = ref_doc.doctype dt = ref_doc.doctype
if dt in ["Sales Order", "Purchase Order"]: 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"]: elif dt in ["Sales Invoice", "Purchase Invoice"]:
if ref_doc.party_account_currency == ref_doc.currency: if ref_doc.party_account_currency == ref_doc.currency:
grand_total = flt(ref_doc.outstanding_amount) grand_total = flt(ref_doc.outstanding_amount)
else: else:
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
elif dt == "POS Invoice": elif dt == "POS Invoice":
for pay in ref_doc.payments: for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account: if pay.type == "Phone" and pay.account == payment_account:
grand_total = pay.amount grand_total = pay.amount
break break
elif dt == "Fees": elif dt == "Fees":
grand_total = ref_doc.outstanding_amount grand_total = ref_doc.outstanding_amount
if grand_total > 0: if grand_total > 0:
return grand_total return grand_total
else: else:
frappe.throw(_("Payment Entry is already created")) frappe.throw(_("Payment Entry is already created"))

View File

@@ -45,7 +45,10 @@ class TestPaymentRequest(unittest.TestCase):
frappe.get_doc(method).insert(ignore_permissions=True) frappe.get_doc(method).insert(ignore_permissions=True)
def test_payment_request_linkings(self): 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( pr = make_payment_request(
dt="Sales Order", dt="Sales Order",
dn=so_inr.name, dn=so_inr.name,

View File

@@ -21,8 +21,24 @@ class POSClosingEntry(StatusUpdater):
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open": 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")) frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
self.validate_duplicate_pos_invoices()
self.validate_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): def validate_pos_invoices(self):
invalid_rows = [] invalid_rows = []
for d in self.pos_transactions: for d in self.pos_transactions:

View File

@@ -161,7 +161,7 @@ class POSInvoice(SalesInvoice):
bold_item_name = frappe.bold(item.item_name) bold_item_name = frappe.bold(item.item_name)
bold_extra_batch_qty_needed = frappe.bold( 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) 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), ).format(item.idx, bold_invalid_batch_no, bold_item_name),
title=_("Item Unavailable"), 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( frappe.throw(
_( _(
"Row #{}: Batch No. {} of item {} has less than required stock available, {} more required" "Row #{}: Batch No. {} of item {} has less than required stock available, {} more required"
@@ -246,7 +246,7 @@ class POSInvoice(SalesInvoice):
), ),
title=_("Item Unavailable"), 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( frappe.throw(
_( _(
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}." "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) item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
available_qty = item_bin_qty - item_pos_reserved_qty 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( if bundle_bin_qty > max_available_bundles and frappe.get_value(
"Item", item.item_code, "is_stock_item" "Item", item.item_code, "is_stock_item"
): ):

View File

@@ -17,6 +17,22 @@ class POSInvoiceMergeLog(Document):
def validate(self): def validate(self):
self.validate_customer() self.validate_customer()
self.validate_pos_invoice_status() 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): def validate_customer(self):
if self.merge_invoices_based_on == "Customer Group": 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: if closing_entry:
closing_entry.set_status(update=True, status="Failed") 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) closing_entry.db_set("error_message", error_message)
raise raise

View File

@@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
super.onload(); super.onload();
// Ignore linked advances // 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) { if(!this.frm.doc.__islocal) {
// show credit_to in print format // show credit_to in print format

View File

@@ -5,6 +5,7 @@
import frappe import frappe
from frappe import _, throw from frappe import _, throw
from frappe.model.mapper import get_mapped_doc 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 from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
import erpnext import erpnext
@@ -1416,6 +1417,8 @@ class PurchaseInvoice(BuyingController):
"GL Entry", "GL Entry",
"Stock Ledger Entry", "Stock Ledger Entry",
"Repost Item Valuation", "Repost Item Valuation",
"Repost Payment Ledger",
"Repost Payment Ledger Items",
"Payment Ledger Entry", "Payment Ledger Entry",
"Tax Withheld Vouchers", "Tax Withheld Vouchers",
) )
@@ -1463,19 +1466,16 @@ class PurchaseInvoice(BuyingController):
def update_billing_status_in_pr(self, update_modified=True): def update_billing_status_in_pr(self, update_modified=True):
updated_pr = [] updated_pr = []
po_details = [] po_details = []
pr_details_billed_amt = self.get_pr_details_billed_amt()
for d in self.get("items"): for d in self.get("items"):
if d.pr_detail: 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( frappe.db.set_value(
"Purchase Receipt Item", "Purchase Receipt Item",
d.pr_detail, d.pr_detail,
"billed_amt", "billed_amt",
billed_amt, flt(pr_details_billed_amt.get(d.pr_detail)),
update_modified=update_modified, update_modified=update_modified,
) )
updated_pr.append(d.purchase_receipt) updated_pr.append(d.purchase_receipt)
@@ -1485,11 +1485,35 @@ class PurchaseInvoice(BuyingController):
if po_details: if po_details:
updated_pr += update_billed_amount_based_on_po(po_details, update_modified) 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): for pr in set(updated_pr):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage
pr_doc = frappe.get_doc("Purchase Receipt", pr) 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): def on_recurring(self, reference_doc, auto_repeat_doc):
self.due_date = None self.due_date = None

View File

@@ -1523,6 +1523,94 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
company.enable_provisional_accounting_for_non_stock_items = 0 company.enable_provisional_accounting_for_non_stock_items = 0
company.save() 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): def test_item_less_defaults(self):
pi = frappe.new_doc("Purchase Invoice") pi = frappe.new_doc("Purchase Invoice")

View File

@@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
super.onload(); super.onload();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', 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) { if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format // show debit_to in print format

View File

@@ -397,6 +397,8 @@ class SalesInvoice(SellingController):
"GL Entry", "GL Entry",
"Stock Ledger Entry", "Stock Ledger Entry",
"Repost Item Valuation", "Repost Item Valuation",
"Repost Payment Ledger",
"Repost Payment Ledger Items",
"Payment Ledger Entry", "Payment Ledger Entry",
) )

View File

@@ -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) tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
if cint(tax_details.round_off_tax_amount): 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 return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount
@@ -603,3 +603,20 @@ def is_valid_certificate(
valid = True valid = True
return valid 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

View File

@@ -138,7 +138,8 @@ def prepare_companywise_opening_balance(asset_data, liability_data, equity_data,
for data in [asset_data, liability_data, equity_data]: for data in [asset_data, liability_data, equity_data]:
if data: if data:
account_name = get_root_account_name(data[0].root_type, company) account_name = get_root_account_name(data[0].root_type, company)
opening_value += get_opening_balance(account_name, data, company) or 0.0 if account_name:
opening_value += get_opening_balance(account_name, data, company) or 0.0
opening_balance[company] = opening_value opening_balance[company] = opening_value
@@ -155,7 +156,7 @@ def get_opening_balance(account_name, data, company):
def get_root_account_name(root_type, company): def get_root_account_name(root_type, company):
return frappe.get_all( root_account = frappe.get_all(
"Account", "Account",
fields=["account_name"], fields=["account_name"],
filters={ filters={
@@ -165,7 +166,10 @@ def get_root_account_name(root_type, company):
"parent_account": ("is", "not set"), "parent_account": ("is", "not set"),
}, },
as_list=1, as_list=1,
)[0][0] )
if root_account:
return root_account[0][0]
def get_profit_loss_data(fiscal_year, companies, columns, filters): def get_profit_loss_data(fiscal_year, companies, columns, filters):

View File

@@ -38,8 +38,11 @@
{% if(data[i].posting_date) { %} {% if(data[i].posting_date) { %}
<td>{%= frappe.datetime.str_to_user(data[i].posting_date) %}</td> <td>{%= frappe.datetime.str_to_user(data[i].posting_date) %}</td>
<td>{%= data[i].voucher_type %} <td>{%= data[i].voucher_type %}
<br>{%= data[i].voucher_no %}</td> <br>{%= data[i].voucher_no %}
<td> </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)) { %} {% if(!(filters.party || filters.account)) { %}
{%= data[i].party || data[i].account %} {%= data[i].party || data[i].account %}
<br> <br>
@@ -49,11 +52,14 @@
{% if(data[i].bill_no) { %} {% if(data[i].bill_no) { %}
<br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %} <br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
{% } %} {% } %}
</td> </span>
<td style="text-align: right"> </td>
{%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %}</td> <td style="text-align: right">
<td style="text-align: right"> {%= format_currency(data[i].debit, filters.presentation_currency) %}
{%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %}</td> </td>
<td style="text-align: right">
{%= format_currency(data[i].credit, filters.presentation_currency) %}
</td>
{% } else { %} {% } else { %}
<td></td> <td></td>
<td></td> <td></td>

View File

@@ -395,6 +395,7 @@ def get_column_names():
class GrossProfitGenerator(object): class GrossProfitGenerator(object):
def __init__(self, filters=None): def __init__(self, filters=None):
self.sle = {}
self.data = [] self.data = []
self.average_buying_rate = {} self.average_buying_rate = {}
self.filters = frappe._dict(filters) self.filters = frappe._dict(filters)
@@ -404,7 +405,6 @@ class GrossProfitGenerator(object):
if filters.group_by == "Invoice": if filters.group_by == "Invoice":
self.group_items_by_invoice() self.group_items_by_invoice()
self.load_stock_ledger_entries()
self.load_product_bundle() self.load_product_bundle()
self.load_non_stock_items() self.load_non_stock_items()
self.get_returned_invoice_items() self.get_returned_invoice_items()
@@ -633,7 +633,7 @@ class GrossProfitGenerator(object):
return flt(row.qty) * item_rate return flt(row.qty) * item_rate
else: 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: if (row.update_stock or row.dn_detail) and my_sle:
parenttype, parent = row.parenttype, row.parent parenttype, parent = row.parenttype, row.parent
if row.dn_detail: if row.dn_detail:
@@ -651,7 +651,7 @@ class GrossProfitGenerator(object):
dn["item_row"], dn["item_row"],
dn["warehouse"], 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( return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, item_row, item_code 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): def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code):
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import Sum
delivery_note = frappe.qb.DocType("Delivery Note")
delivery_note_item = frappe.qb.DocType("Delivery Note Item") delivery_note_item = frappe.qb.DocType("Delivery Note Item")
query = ( query = (
frappe.qb.from_(delivery_note) frappe.qb.from_(delivery_note_item)
.inner_join(delivery_note_item)
.on(delivery_note.name == delivery_note_item.parent)
.select(Sum(delivery_note_item.incoming_rate * delivery_note_item.stock_qty)) .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.item_code == item_code)
.where(delivery_note_item.against_sales_order == sales_order) .where(delivery_note_item.against_sales_order == sales_order)
.where(delivery_note_item.so_detail == so_detail) .where(delivery_note_item.so_detail == so_detail)
@@ -947,24 +944,36 @@ class GrossProfitGenerator(object):
"Item", item_code, ["item_name", "description", "item_group", "brand"] "Item", item_code, ["item_name", "description", "item_group", "brand"]
) )
def load_stock_ledger_entries(self): def get_stock_ledger_entries(self, item_code, warehouse):
res = frappe.db.sql( if item_code and warehouse:
"""select item_code, voucher_type, voucher_no, if (item_code, warehouse) not in self.sle:
voucher_detail_no, stock_value, warehouse, actual_qty as qty sle = qb.DocType("Stock Ledger Entry")
from `tabStock Ledger Entry` res = (
where company=%(company)s and is_cancelled = 0 qb.from_(sle)
order by .select(
item_code desc, warehouse desc, posting_date desc, sle.item_code,
posting_time desc, creation desc""", sle.voucher_type,
self.filters, sle.voucher_no,
as_dict=True, sle.voucher_detail_no,
) sle.stock_value,
self.sle = {} sle.warehouse,
for r in res: sle.actual_qty.as_("qty"),
if (r.item_code, r.warehouse) not in self.sle: )
self.sle[(r.item_code, r.warehouse)] = [] .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): def load_product_bundle(self):
self.product_bundles = {} self.product_bundles = {}

View File

@@ -43,9 +43,9 @@ erpnext.asset.set_accumulated_depreciation = function(frm) {
if(frm.doc.depreciation_method != "Manual") return; if(frm.doc.depreciation_method != "Manual") return;
var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation); 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); accumulated_depreciation += flt(row.depreciation_amount);
frappe.model.set_value(row.doctype, row.name, frappe.model.set_value(row.doctype, row.name, "accumulated_depreciation_amount", accumulated_depreciation);
"accumulated_depreciation_amount", accumulated_depreciation);
}) })
}; };

View File

@@ -10,7 +10,9 @@
"asset", "asset",
"naming_series", "naming_series",
"column_break_2", "column_break_2",
"gross_purchase_amount",
"opening_accumulated_depreciation", "opening_accumulated_depreciation",
"number_of_depreciations_booked",
"finance_book", "finance_book",
"finance_book_id", "finance_book_id",
"depreciation_details_section", "depreciation_details_section",
@@ -148,18 +150,36 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "opening_accumulated_depreciation",
"fieldname": "opening_accumulated_depreciation", "fieldname": "opening_accumulated_depreciation",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1,
"label": "Opening Accumulated Depreciation", "label": "Opening Accumulated Depreciation",
"options": "Company:company:default_currency", "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 "read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-01-16 21:08:21.421260", "modified": "2023-02-26 16:37:23.734806",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Depreciation Schedule", "name": "Asset Depreciation Schedule",

View File

@@ -4,7 +4,15 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document 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): class AssetDepreciationSchedule(Document):
@@ -83,15 +91,58 @@ class AssetDepreciationSchedule(Document):
date_of_return=None, date_of_return=None,
update_asset_finance_book_row=True, 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.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): def set_draft_asset_depr_schedule_details(self, asset_doc, row):
self.asset = asset_doc.name self.asset = asset_doc.name
self.finance_book = row.finance_book self.finance_book = row.finance_book
self.finance_book_id = row.idx self.finance_book_id = row.idx
self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation 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.depreciation_method = row.depreciation_method
self.total_number_of_depreciations = row.total_number_of_depreciations self.total_number_of_depreciations = row.total_number_of_depreciations
self.frequency_of_depreciation = row.frequency_of_depreciation self.frequency_of_depreciation = row.frequency_of_depreciation
@@ -102,7 +153,7 @@ class AssetDepreciationSchedule(Document):
def make_depr_schedule( def make_depr_schedule(
self, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True 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 = [] self.depreciation_schedule = []
if not asset_doc.available_for_use_date: if not asset_doc.available_for_use_date:
@@ -293,7 +344,9 @@ class AssetDepreciationSchedule(Document):
ignore_booked_entry=False, ignore_booked_entry=False,
): ):
straight_line_idx = [ 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) accumulated_depreciation = flt(self.opening_accumulated_depreciation)

View File

@@ -151,6 +151,7 @@ def prepare_chart_data(data, filters):
filters.filter_based_on, filters.filter_based_on,
"Monthly", "Monthly",
company=filters.company, company=filters.company,
ignore_fiscal_year=True,
) )
for d in period_list: for d in period_list:

View File

@@ -18,9 +18,11 @@
"pr_required", "pr_required",
"column_break_12", "column_break_12",
"maintain_same_rate", "maintain_same_rate",
"set_landed_cost_based_on_purchase_invoice_rate",
"allow_multiple_items", "allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice", "bill_for_rejected_quantity_in_purchase_invoice",
"disable_last_purchase_rate", "disable_last_purchase_rate",
"show_pay_button",
"subcontract", "subcontract",
"backflush_raw_materials_of_subcontract_based_on", "backflush_raw_materials_of_subcontract_based_on",
"column_break_11", "column_break_11",
@@ -140,6 +142,20 @@
"fieldname": "disable_last_purchase_rate", "fieldname": "disable_last_purchase_rate",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disable Last Purchase Rate" "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", "icon": "fa fa-cog",
@@ -147,7 +163,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-01-09 17:08:28.828173", "modified": "2023-02-28 15:41:32.686805",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",

View File

@@ -21,3 +21,10 @@ class BuyingSettings(Document):
self.get("supp_master_name") == "Naming Series", self.get("supp_master_name") == "Naming Series",
hide_name_field=False, 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

View File

@@ -23,7 +23,6 @@
"default_bank_account", "default_bank_account",
"column_break_10", "column_break_10",
"default_price_list", "default_price_list",
"payment_terms",
"internal_supplier_section", "internal_supplier_section",
"is_internal_supplier", "is_internal_supplier",
"represents_company", "represents_company",
@@ -53,6 +52,7 @@
"supplier_primary_address", "supplier_primary_address",
"primary_address", "primary_address",
"accounting_tab", "accounting_tab",
"payment_terms",
"accounts", "accounts",
"settings_tab", "settings_tab",
"allow_purchase_invoice_creation_without_purchase_order", "allow_purchase_invoice_creation_without_purchase_order",
@@ -457,11 +457,10 @@
"link_fieldname": "party" "link_fieldname": "party"
} }
], ],
"modified": "2022-11-09 18:02:59.075203", "modified": "2023-02-18 11:05:50.592270",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier", "name": "Supplier",
"name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field", "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [

View File

@@ -22,14 +22,14 @@ frappe.query_reports["Subcontracted Item To Be Received"] = {
fieldname:"from_date", fieldname:"from_date",
label: __("From Date"), label: __("From Date"),
fieldtype: "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 reqd: 1
}, },
{ {
fieldname:"to_date", fieldname:"to_date",
label: __("To Date"), label: __("To Date"),
fieldtype: "Date", fieldtype: "Date",
default: frappe.datetime.add_days(frappe.datetime.month_start(),-1), default: frappe.datetime.get_today(),
reqd: 1 reqd: 1
}, },
] ]

View File

@@ -22,14 +22,14 @@ frappe.query_reports["Subcontracted Raw Materials To Be Transferred"] = {
fieldname:"from_date", fieldname:"from_date",
label: __("From Date"), label: __("From Date"),
fieldtype: "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 reqd: 1
}, },
{ {
fieldname:"to_date", fieldname:"to_date",
label: __("To Date"), label: __("To Date"),
fieldtype: "Date", fieldtype: "Date",
default: frappe.datetime.add_days(frappe.datetime.month_start(),-1), default: frappe.datetime.get_today(),
reqd: 1 reqd: 1
}, },
] ]

View File

@@ -204,6 +204,12 @@ class AccountsController(TransactionBase):
validate_einvoice_fields(self) validate_einvoice_fields(self)
def on_trash(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 # delete sl and gl entries on deletion of transaction
if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"): if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"):
ple = frappe.qb.DocType("Payment Ledger Entry") ple = frappe.qb.DocType("Payment Ledger Entry")

View File

@@ -265,7 +265,10 @@ class BuyingController(SubcontractingController):
) / qty_in_stock_uom ) / qty_in_stock_uom
else: else:
item.valuation_rate = ( 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 ) / qty_in_stock_uom
else: else:
item.valuation_rate = 0.0 item.valuation_rate = 0.0

View File

@@ -131,7 +131,7 @@ def validate_returned_items(doc):
) )
elif ref.serial_no: 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)) frappe.throw(_("Row # {0}: Serial No is mandatory").format(d.idx))
else: else:
serial_nos = get_serial_nos(d.serial_no) 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 child.parent = par.name and par.docstatus = 1
and par.is_return = 1 and par.return_against = %s and par.is_return = 1 and par.return_against = %s
group by item_code group by item_code
for update
""".format( """.format(
column, doc.doctype, doc.doctype column, doc.doctype, doc.doctype
), ),
@@ -401,6 +400,16 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
if serial_nos: if serial_nos:
target_doc.serial_no = "\n".join(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"]: if doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
returned_qty_map = get_returned_qty_map_for_row( returned_qty_map = get_returned_qty_map_for_row(
source_parent.name, source_parent.supplier, source_doc.name, doctype source_parent.name, source_parent.supplier, source_doc.name, doctype
@@ -611,7 +620,7 @@ def get_filters(
return 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 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
return_ref_field = frappe.scrub(child_doc.doctype) return_ref_field = frappe.scrub(child_doc.doctype)
@@ -620,7 +629,7 @@ def get_returned_serial_nos(child_doc, parent_doc):
serial_nos = [] serial_nos = []
fields = ["`{0}`.`serial_no`".format("tab" + child_doc.doctype)] fields = [f"`{'tab' + child_doc.doctype}`.`{serial_no_field}`"]
filters = [ filters = [
[parent_doc.doctype, "return_against", "=", parent_doc.name], [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): 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 return serial_nos

View File

@@ -136,7 +136,7 @@ class SellingController(StockController):
self.in_words = money_in_words(amount, self.currency) self.in_words = money_in_words(amount, self.currency)
def calculate_commission(self): 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 return
self.round_floats_in(self, ("amount_eligible_for_commission", "commission_rate")) self.round_floats_in(self, ("amount_eligible_for_commission", "commission_rate"))

View File

@@ -24,11 +24,19 @@ class calculate_taxes_and_totals(object):
def __init__(self, doc: Document): def __init__(self, doc: Document):
self.doc = doc self.doc = doc
frappe.flags.round_off_applicable_accounts = [] 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) get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
self.calculate() 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): def calculate(self):
if not len(self.doc.get("items")): if not len(self._items):
return return
self.discount_amount_applied = False self.discount_amount_applied = False
@@ -70,7 +78,7 @@ class calculate_taxes_and_totals(object):
if hasattr(self.doc, "tax_withholding_net_total"): if hasattr(self.doc, "tax_withholding_net_total"):
sum_net_amount = 0 sum_net_amount = 0
sum_base_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: if hasattr(item, "apply_tds") and item.apply_tds:
sum_net_amount += item.net_amount sum_net_amount += item.net_amount
sum_base_net_amount += item.base_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 self.doc.base_tax_withholding_net_total = sum_base_net_amount
def validate_item_tax_template(self): 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"): if item.item_code and item.get("item_tax_template"):
item_doc = frappe.get_cached_doc("Item", item.item_code) item_doc = frappe.get_cached_doc("Item", item.item_code)
args = { args = {
@@ -137,7 +145,7 @@ class calculate_taxes_and_totals(object):
return return
if not self.discount_amount_applied: if not self.discount_amount_applied:
for item in self.doc.get("items"): for item in self._items:
self.doc.round_floats_in(item) self.doc.round_floats_in(item)
if item.discount_percentage == 100: 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")): if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
return 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) item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
cumulated_tax_fraction = 0 cumulated_tax_fraction = 0
total_inclusive_tax_amount_per_qty = 0 total_inclusive_tax_amount_per_qty = 0
@@ -317,7 +325,7 @@ class calculate_taxes_and_totals(object):
self.doc.total self.doc.total
) = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0 ) = 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 += item.amount
self.doc.total_qty += item.qty self.doc.total_qty += item.qty
self.doc.base_total += item.base_amount 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) item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
for i, tax in enumerate(self.doc.get("taxes")): for i, tax in enumerate(self.doc.get("taxes")):
# tax_amount represents the amount of tax for the current step # 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 # Adjust divisional loss to the last item
if tax.charge_type == "Actual": if tax.charge_type == "Actual":
actual_tax_dict[tax.idx] -= current_tax_amount 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] current_tax_amount += actual_tax_dict[tax.idx]
# accumulate tax amount into tax.tax_amount # accumulate tax amount into tax.tax_amount
@@ -391,7 +399,7 @@ class calculate_taxes_and_totals(object):
) )
# set precision in the last item iteration # 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.round_off_totals(tax)
self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"]) 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): def calculate_total_net_weight(self):
if self.doc.meta.get_field("total_net_weight"): if self.doc.meta.get_field("total_net_weight"):
self.doc.total_net_weight = 0.0 self.doc.total_net_weight = 0.0
for d in self.doc.items: for d in self._items:
if d.total_weight: if d.total_weight:
self.doc.total_net_weight += 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: if total_for_discount_amount:
# calculate item amount after 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 = ( distributed_amount = (
flt(self.doc.discount_amount) * item.net_amount / total_for_discount_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" self.doc.apply_discount_on == "Net Total"
or not taxes or not taxes
or total_for_discount_amount == self.doc.net_total 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( discount_amount_loss = flt(
self.doc.net_total - net_total - self.doc.discount_amount, self.doc.precision("net_total") self.doc.net_total - net_total - self.doc.discount_amount, self.doc.precision("net_total")
) )

View File

@@ -76,12 +76,9 @@ def get_transaction_list(
ignore_permissions = False ignore_permissions = False
if not filters: if not filters:
filters = [] filters = {}
if doctype in ["Supplier Quotation", "Purchase Invoice"]: filters["docstatus"] = ["<", "2"] if doctype in ["Supplier Quotation", "Purchase Invoice"] else 1
filters.append((doctype, "docstatus", "<", 2))
else:
filters.append((doctype, "docstatus", "=", 1))
if (user != "Guest" and is_website_user()) or doctype == "Request for Quotation": if (user != "Guest" and is_website_user()) or doctype == "Request for Quotation":
parties_doctype = ( parties_doctype = (
@@ -92,12 +89,12 @@ def get_transaction_list(
if customers: if customers:
if doctype == "Quotation": if doctype == "Quotation":
filters.append(("quotation_to", "=", "Customer")) filters["quotation_to"] = "Customer"
filters.append(("party_name", "in", customers)) filters["party_name"] = ["in", customers]
else: else:
filters.append(("customer", "in", customers)) filters["customer"] = ["in", customers]
elif suppliers: elif suppliers:
filters.append(("supplier", "in", suppliers)) filters["supplier"] = ["in", suppliers]
elif not custom: elif not custom:
return [] return []
@@ -110,7 +107,7 @@ def get_transaction_list(
if not customers and not suppliers and custom: if not customers and not suppliers and custom:
ignore_permissions = False ignore_permissions = False
filters = [] filters = {}
transactions = get_list_for_transactions( transactions = get_list_for_transactions(
doctype, doctype,

View File

@@ -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) { validate: function(frm) {
@@ -130,6 +126,10 @@ frappe.ui.form.on("Opportunity", {
} else { } else {
frappe.contacts.clear_address_and_contact(frm); 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) { 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'} frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Customer'}
} else if(frm.doc.opportunity_from == "Lead" && frm.doc.party_name) { } else if(frm.doc.opportunity_from == "Lead" && frm.doc.party_name) {
frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Lead'} 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'}
} }
}, },

View File

@@ -12,7 +12,7 @@ class PlaidConnector:
def __init__(self, access_token=None): def __init__(self, access_token=None):
self.access_token = access_token self.access_token = access_token
self.settings = frappe.get_single("Plaid Settings") self.settings = frappe.get_single("Plaid Settings")
self.products = ["auth", "transactions"] self.products = ["transactions"]
self.client_name = frappe.local.site self.client_name = frappe.local.site
self.client = plaid.Client( self.client = plaid.Client(
client_id=self.settings.plaid_client_id, client_id=self.settings.plaid_client_id,

View File

@@ -47,7 +47,7 @@ erpnext.integrations.plaidLink = class plaidLink {
} }
async init_config() { async init_config() {
this.product = ["auth", "transactions"]; this.product = ["transactions"];
this.plaid_env = this.frm.doc.plaid_env; this.plaid_env = this.frm.doc.plaid_env;
this.client_name = frappe.boot.sitename; this.client_name = frappe.boot.sitename;
this.token = await this.get_link_token(); this.token = await this.get_link_token();

View File

@@ -70,7 +70,8 @@ def add_bank_accounts(response, bank, company):
except TypeError: except TypeError:
pass pass
bank = json.loads(bank) if isinstance(bank, str):
bank = json.loads(bank)
result = [] result = []
default_gl_account = get_default_bank_cash_account(company, "Bank") default_gl_account = get_default_bank_cash_account(company, "Bank")
@@ -177,16 +178,15 @@ def sync_transactions(bank, bank_account):
) )
result = [] result = []
for transaction in reversed(transactions): if transactions:
result += new_bank_transaction(transaction) for transaction in reversed(transactions):
result += new_bank_transaction(transaction)
if result: if result:
last_transaction_date = frappe.db.get_value("Bank Transaction", result.pop(), "date") last_transaction_date = frappe.db.get_value("Bank Transaction", result.pop(), "date")
frappe.logger().info( frappe.logger().info(
"Plaid added {} new Bank Transactions from '{}' between {} and {}".format( f"Plaid added {len(result)} new Bank Transactions from '{bank_account}' between {start_date} and {end_date}"
len(result), bank_account, start_date, end_date
)
) )
frappe.db.set_value( 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"])) bank_account = frappe.db.get_value("Bank Account", dict(integration_id=transaction["account_id"]))
if float(transaction["amount"]) >= 0: amount = float(transaction["amount"])
debit = 0 if amount >= 0.0:
credit = float(transaction["amount"]) deposit = 0.0
withdrawal = amount
else: else:
debit = abs(float(transaction["amount"])) deposit = abs(amount)
credit = 0 withdrawal = 0.0
status = "Pending" if transaction["pending"] == "True" else "Settled" status = "Pending" if transaction["pending"] == "True" else "Settled"
tags = [] tags = []
try: try:
tags += transaction["category"] tags += transaction["category"]
tags += ["Plaid Cat. {}".format(transaction["category_id"])] tags += [f'Plaid Cat. {transaction["category_id"]}']
except KeyError: except KeyError:
pass pass
@@ -254,11 +255,18 @@ def new_bank_transaction(transaction):
"date": getdate(transaction["date"]), "date": getdate(transaction["date"]),
"status": status, "status": status,
"bank_account": bank_account, "bank_account": bank_account,
"deposit": debit, "deposit": deposit,
"withdrawal": credit, "withdrawal": withdrawal,
"currency": transaction["iso_currency_code"], "currency": transaction["iso_currency_code"],
"transaction_id": transaction["transaction_id"], "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"], "description": transaction["name"],
} }
) )
@@ -271,7 +279,7 @@ def new_bank_transaction(transaction):
result.append(new_transaction.name) result.append(new_transaction.name)
except Exception: except Exception:
frappe.throw(title=_("Bank transaction creation error")) frappe.throw(_("Bank transaction creation error"))
return result return result
@@ -300,3 +308,26 @@ def enqueue_synchronization():
def get_link_token_for_update(access_token): def get_link_token_for_update(access_token):
plaid = PlaidConnector(access_token) plaid = PlaidConnector(access_token)
return plaid.get_link_token(update_mode=True) 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))

View File

@@ -125,6 +125,8 @@ class TestPlaidSettings(unittest.TestCase):
"unofficial_currency_code": None, "unofficial_currency_code": None,
"name": "INTRST PYMNT", "name": "INTRST PYMNT",
"transaction_type": "place", "transaction_type": "place",
"transaction_code": "direct debit",
"check_number": "3456789",
"amount": -4.22, "amount": -4.22,
"location": { "location": {
"city": None, "city": None,

View File

@@ -311,15 +311,10 @@ doc_events = {
"on_submit": [ "on_submit": [
"erpnext.regional.create_transaction_log", "erpnext.regional.create_transaction_log",
"erpnext.regional.italy.utils.sales_invoice_on_submit", "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", "on_trash": "erpnext.regional.check_deletion_permission",
}, },
"POS Invoice": {"on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]},
"Purchase Invoice": { "Purchase Invoice": {
"validate": [ "validate": [
"erpnext.regional.united_arab_emirates.utils.update_grand_total_for_rcm", "erpnext.regional.united_arab_emirates.utils.update_grand_total_for_rcm",
@@ -347,7 +342,6 @@ doc_events = {
"Email Unsubscribe": { "Email Unsubscribe": {
"after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient" "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": { "Integration Request": {
"validate": "erpnext.accounts.doctype.payment_request.payment_request.validate_payment" "validate": "erpnext.accounts.doctype.payment_request.payment_request.validate_payment"
}, },
@@ -362,7 +356,7 @@ auto_cancel_exempted_doctypes = [
scheduler_events = { scheduler_events = {
"cron": { "cron": {
"0/5 * * * *": [ "0/15 * * * *": [
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs", "erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
], ],
"0/30 * * * *": [ "0/30 * * * *": [

View File

@@ -64,8 +64,6 @@
"fieldtype": "Section Break" "fieldtype": "Section Break"
}, },
{ {
"fetch_from": "prevdoc_detail_docname.sales_person",
"fetch_if_empty": 1,
"fieldname": "service_person", "fieldname": "service_person",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
@@ -110,13 +108,15 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-05-27 17:47:21.474282", "modified": "2023-02-27 11:09:33.114458",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Maintenance", "module": "Maintenance",
"name": "Maintenance Visit Purpose", "name": "Maintenance Visit Purpose",
"naming_rule": "Random",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -212,7 +212,7 @@ def resume_bom_cost_update_jobs():
["name", "boms_updated", "status"], ["name", "boms_updated", "status"],
) )
incomplete_level = any(row.get("status") == "Pending" for row in bom_batches) 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 continue
# Prep parent BOMs & updated processed BOMs for next level # Prep parent BOMs & updated processed BOMs for next level
@@ -252,6 +252,9 @@ def get_processed_current_boms(
current_boms = [] current_boms = []
for row in bom_batches: for row in bom_batches:
if not row.boms_updated:
continue
boms_updated = json.loads(row.boms_updated) boms_updated = json.loads(row.boms_updated)
current_boms.extend(boms_updated) current_boms.extend(boms_updated)
boms_updated_dict = {bom: True for bom in boms_updated} boms_updated_dict = {bom: True for bom in boms_updated}

View File

@@ -561,7 +561,34 @@ class JobCard(Document):
) )
def set_transferred_qty_in_job_card_item(self, ste_doc): 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): def _validate_over_transfer(row, transferred_qty):
"Block over transfer of items if not allowed in settings." "Block over transfer of items if not allowed in settings."
@@ -578,29 +605,23 @@ class JobCard(Document):
exc=JobCardOverTransferError, exc=JobCardOverTransferError,
) )
for row in ste_doc.items: job_card_items_transferred_qty = _get_job_card_items_transferred_qty(ste_doc)
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]
if job_card_items_transferred_qty:
allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer") 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): def set_transferred_qty(self, update_status=False):
"Set total FG Qty in Job Card for which RM was transferred." "Set total FG Qty in Job Card for which RM was transferred."

View File

@@ -506,7 +506,7 @@ frappe.ui.form.on("Work Order Item", {
callback: function(r) { callback: function(r) {
if (r.message) { if (r.message) {
frappe.model.set_value(cdt, cdn, { frappe.model.set_value(cdt, cdn, {
"required_qty": 1, "required_qty": row.required_qty || 1,
"item_name": r.message.item_name, "item_name": r.message.item_name,
"description": r.message.description, "description": r.message.description,
"source_warehouse": r.message.default_warehouse, "source_warehouse": r.message.default_warehouse,

View File

@@ -25,8 +25,9 @@ frappe.query_reports["BOM Stock Report"] = {
], ],
"formatter": function(value, row, column, data, default_formatter) { "formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data); value = default_formatter(value, row, column, data);
if (column.id == "item") { 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>`; value = `<a style='color:green' href="/app/item/${data['item']}" data-doctype="Item">${data['item']}</a>`;
} else { } else {
value = `<a style='color:red' href="/app/item/${data['item']}" data-doctype="Item">${data['item']}</a>`; value = `<a style='color:red' href="/app/item/${data['item']}" data-doctype="Item">${data['item']}</a>`;

View File

@@ -4,7 +4,8 @@
import frappe import frappe
from frappe import _ 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 from pypika.terms import ExistsCriterion
@@ -34,57 +35,55 @@ def get_columns():
def get_bom_stock(filters): def get_bom_stock(filters):
qty_to_produce = filters.get("qty_to_produce") or 1 qty_to_produce = filters.get("qty_to_produce")
if int(qty_to_produce) < 0: if cint(qty_to_produce) <= 0:
frappe.throw(_("Quantity to Produce can not be less than Zero")) frappe.throw(_("Quantity to Produce should be greater than zero."))
if filters.get("show_exploded_view"): if filters.get("show_exploded_view"):
bom_item_table = "BOM Explosion Item" bom_item_table = "BOM Explosion Item"
else: else:
bom_item_table = "BOM Item" bom_item_table = "BOM Item"
bin = frappe.qb.DocType("Bin") warehouse_details = frappe.db.get_value(
bom = frappe.qb.DocType("BOM") "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
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)
) )
if filters.get("warehouse"): BOM = frappe.qb.DocType("BOM")
warehouse_details = frappe.db.get_value( BOM_ITEM = frappe.qb.DocType(bom_item_table)
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 BIN = frappe.qb.DocType("Bin")
) WH = frappe.qb.DocType("Warehouse")
CONDITIONS = ()
if warehouse_details: if warehouse_details:
wh = frappe.qb.DocType("Warehouse") CONDITIONS = ExistsCriterion(
query = query.where( frappe.qb.from_(WH)
ExistsCriterion( .select(WH.name)
frappe.qb.from_(wh) .where(
.select(wh.name) (WH.lft >= warehouse_details.lft)
.where( & (WH.rgt <= warehouse_details.rgt)
(wh.lft >= warehouse_details.lft) & (BIN.warehouse == WH.name)
& (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()

View File

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

View File

@@ -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.update_category_in_ltds_certificate
erpnext.patches.v13_0.fetch_thumbnail_in_website_items erpnext.patches.v13_0.fetch_thumbnail_in_website_items
erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit 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.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.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.agriculture_deprecation_warning
erpnext.patches.v13_0.hospitality_deprecation_warning erpnext.patches.v13_0.hospitality_deprecation_warning
erpnext.patches.v13_0.update_asset_quantity_field erpnext.patches.v13_0.update_asset_quantity_field
erpnext.patches.v13_0.delete_bank_reconciliation_detail erpnext.patches.v13_0.delete_bank_reconciliation_detail
erpnext.patches.v13_0.enable_provisional_accounting erpnext.patches.v13_0.enable_provisional_accounting
erpnext.patches.v13_0.non_profit_deprecation_warning 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_india_localisation_deprecation_warning
erpnext.patches.v13_0.show_hr_payroll_deprecation_warning erpnext.patches.v13_0.show_hr_payroll_deprecation_warning
erpnext.patches.v13_0.reset_corrupt_defaults 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.delete_taxjar_doctypes
erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets
erpnext.patches.v14_0.update_reference_due_date_in_journal_entry 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] [post_model_sync]
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings') execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
@@ -325,6 +323,5 @@ erpnext.patches.v14_0.update_entry_type_for_journal_entry
erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers
erpnext.patches.v14_0.set_pick_list_status erpnext.patches.v14_0.set_pick_list_status
erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries
# below 2 migration patches should always run last # below migration patches should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger

View File

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

View File

@@ -17,10 +17,11 @@ def execute():
for report in reports_to_delete: for report in reports_to_delete:
if frappe.db.exists("Report", report): if frappe.db.exists("Report", report):
delete_links_from_desktop_icons(report)
delete_auto_email_reports(report) delete_auto_email_reports(report)
check_and_delete_linked_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): 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"]) auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"])
for auto_email_report in auto_email_reports: for auto_email_report in auto_email_reports:
frappe.delete_doc("Auto Email Report", auto_email_report[0]) 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)

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import frappe import frappe
from frappe import qb from frappe import qb
from frappe.query_builder import Case, CustomFunction from frappe.query_builder import CustomFunction
from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Count, IfNull from frappe.query_builder.functions import Count, IfNull
from frappe.utils import flt from frappe.utils import flt
@@ -18,9 +18,21 @@ def create_accounting_dimension_fields():
make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"]) make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"])
def generate_name_for_payment_ledger_entries(gl_entries, start): def generate_name_and_calculate_amount(gl_entries, start, receivable_accounts):
for index, entry in enumerate(gl_entries, 0): for index, entry in enumerate(gl_entries, 0):
entry.name = start + index entry.name = start + index
if entry.account in receivable_accounts:
entry.account_type = "Receivable"
entry.amount = entry.debit - entry.credit
entry.amount_in_account_currency = (
entry.debit_in_account_currency - entry.credit_in_account_currency
)
else:
entry.account_type = "Payable"
entry.amount = entry.credit - entry.debit
entry.amount_in_account_currency = (
entry.credit_in_account_currency - entry.debit_in_account_currency
)
def get_columns(): def get_columns():
@@ -49,6 +61,9 @@ def get_columns():
"finance_book", "finance_book",
] ]
if frappe.db.has_column("Payment Ledger Entry", "remarks"):
columns.append("remarks")
dimensions_and_defaults = get_dimensions() dimensions_and_defaults = get_dimensions()
if dimensions_and_defaults: if dimensions_and_defaults:
for dimension in dimensions_and_defaults[0]: for dimension in dimensions_and_defaults[0]:
@@ -99,12 +114,17 @@ def execute():
ifelse = CustomFunction("IF", ["condition", "then", "else"]) ifelse = CustomFunction("IF", ["condition", "then", "else"])
# Get Records Count # Get Records Count
accounts = ( relavant_accounts = (
qb.from_(account) qb.from_(account)
.select(account.name) .select(account.name, account.account_type)
.where((account.account_type == "Receivable") | (account.account_type == "Payable")) .where((account.account_type == "Receivable") | (account.account_type == "Payable"))
.orderby(account.name) .orderby(account.name)
.run(as_dict=True)
) )
receivable_accounts = [x.name for x in relavant_accounts if x.account_type == "Receivable"]
accounts = [x.name for x in relavant_accounts]
un_processed = ( un_processed = (
qb.from_(gl) qb.from_(gl)
.select(Count(gl.name)) .select(Count(gl.name))
@@ -122,37 +142,21 @@ def execute():
while True: while True:
if last_name: if last_name:
where_clause = gl.name.gt(last_name) & (gl.is_cancelled == 0) where_clause = gl.name.gt(last_name) & gl.account.isin(accounts) & gl.is_cancelled == 0
else: else:
where_clause = gl.is_cancelled == 0 where_clause = gl.account.isin(accounts) & gl.is_cancelled == 0
gl_entries = ( gl_entries = (
qb.from_(gl) qb.from_(gl)
.inner_join(account)
.on((gl.account == account.name) & (account.account_type.isin(["Receivable", "Payable"])))
.select( .select(
gl.star, gl.star,
ConstantColumn(1).as_("docstatus"), ConstantColumn(1).as_("docstatus"),
account.account_type.as_("account_type"),
IfNull( IfNull(
ifelse(gl.against_voucher_type == "", None, gl.against_voucher_type), gl.voucher_type ifelse(gl.against_voucher_type == "", None, gl.against_voucher_type), gl.voucher_type
).as_("against_voucher_type"), ).as_("against_voucher_type"),
IfNull(ifelse(gl.against_voucher == "", None, gl.against_voucher), gl.voucher_no).as_( IfNull(ifelse(gl.against_voucher == "", None, gl.against_voucher), gl.voucher_no).as_(
"against_voucher_no" "against_voucher_no"
), ),
# convert debit/credit to amount
Case()
.when(account.account_type == "Receivable", gl.debit - gl.credit)
.else_(gl.credit - gl.debit)
.as_("amount"),
# convert debit/credit in account currency to amount in account currency
Case()
.when(
account.account_type == "Receivable",
gl.debit_in_account_currency - gl.credit_in_account_currency,
)
.else_(gl.credit_in_account_currency - gl.debit_in_account_currency)
.as_("amount_in_account_currency"),
) )
.where(where_clause) .where(where_clause)
.orderby(gl.name) .orderby(gl.name)
@@ -163,8 +167,8 @@ def execute():
if gl_entries: if gl_entries:
last_name = gl_entries[-1].name last_name = gl_entries[-1].name
# primary key(name) for payment ledger records # add primary key(name) and calculate based on debit and credit
generate_name_for_payment_ledger_entries(gl_entries, processed) generate_name_and_calculate_amount(gl_entries, processed, receivable_accounts)
try: try:
insert_query = build_insert_query() insert_query = build_insert_query()

View File

@@ -1,98 +0,0 @@
import frappe
from frappe import qb
from frappe.query_builder import CustomFunction
from frappe.query_builder.functions import Count, IfNull
from frappe.utils import flt
def execute():
"""
Migrate 'remarks' field from 'tabGL Entry' to 'tabPayment Ledger Entry'
"""
if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"):
gle = qb.DocType("GL Entry")
ple = qb.DocType("Payment Ledger Entry")
# Get empty PLE records
un_processed = (
qb.from_(ple).select(Count(ple.name)).where((ple.remarks.isnull()) & (ple.delinked == 0)).run()
)[0][0]
if un_processed:
print(f"Remarks for {un_processed} Payment Ledger records will be updated from GL Entry")
ifelse = CustomFunction("IF", ["condition", "then", "else"])
processed = 0
last_percent_update = 0
batch_size = 1000
last_name = None
while True:
if last_name:
where_clause = (ple.name.gt(last_name)) & (ple.remarks.isnull()) & (ple.delinked == 0)
else:
where_clause = (ple.remarks.isnull()) & (ple.delinked == 0)
# results are deterministic
names = (
qb.from_(ple).select(ple.name).where(where_clause).orderby(ple.name).limit(batch_size).run()
)
if names:
last_name = names[-1][0]
pl_entries = (
qb.from_(ple)
.left_join(gle)
.on(
(ple.account == gle.account)
& (ple.party_type == gle.party_type)
& (ple.party == gle.party)
& (ple.voucher_type == gle.voucher_type)
& (ple.voucher_no == gle.voucher_no)
& (
ple.against_voucher_type
== IfNull(
ifelse(gle.against_voucher_type == "", None, gle.against_voucher_type), gle.voucher_type
)
)
& (
ple.against_voucher_no
== IfNull(ifelse(gle.against_voucher == "", None, gle.against_voucher), gle.voucher_no)
)
& (ple.company == gle.company)
& (
((ple.account_type == "Receivable") & (ple.amount == (gle.debit - gle.credit)))
| (ple.account_type == "Payable") & (ple.amount == (gle.credit - gle.debit))
)
& (gle.remarks.notnull())
& (gle.is_cancelled == 0)
)
.select(ple.name)
.distinct()
.select(
gle.remarks.as_("gle_remarks"),
)
.where(ple.name.isin(names))
.run(as_dict=True)
)
if pl_entries:
for entry in pl_entries:
query = qb.update(ple).set(ple.remarks, entry.gle_remarks).where((ple.name == entry.name))
query.run()
frappe.db.commit()
processed += len(pl_entries)
percentage = flt((processed / un_processed) * 100, 2)
if percentage - last_percent_update > 1:
print(f"{percentage}% ({processed}) PLE records updated")
last_percent_update = percentage
else:
break
print("Remarks succesfully migrated")

View File

@@ -27,7 +27,13 @@ def get_details_of_draft_or_submitted_depreciable_assets():
records = ( records = (
frappe.qb.from_(asset) 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.calculate_depreciation == 1)
.where(asset.docstatus < 2) .where(asset.docstatus < 2)
).run(as_dict=True) ).run(as_dict=True)

View 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",
)

View 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",
)

View File

@@ -182,6 +182,9 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager {
); );
} else { } else {
this.transactions.splice(transaction_index, 1); 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); this.datatable.refresh(this.transactions, this.columns);

View File

@@ -20,7 +20,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
doctype: "Bank Transaction", doctype: "Bank Transaction",
filters: { name: this.bank_transaction_name }, filters: { name: this.bank_transaction_name },
fieldname: [ fieldname: [
"date as reference_date", "date",
"deposit", "deposit",
"withdrawal", "withdrawal",
"currency", "currency",
@@ -33,6 +33,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
"party", "party",
"unallocated_amount", "unallocated_amount",
"allocated_amount", "allocated_amount",
"transaction_type",
], ],
}, },
callback: (r) => { callback: (r) => {
@@ -41,11 +42,23 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
r.message.payment_entry = 1; r.message.payment_entry = 1;
r.message.journal_entry = 1; r.message.journal_entry = 1;
this.dialog.set_values(r.message); this.dialog.set_values(r.message);
this.copy_data_to_voucher();
this.dialog.show(); 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) { get_linked_vouchers(document_types) {
frappe.call({ frappe.call({
method: method:
@@ -75,10 +88,9 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
row[1], row[1],
row[2], row[2],
reference_date, reference_date,
row[8],
format_currency(row[3], row[9]), format_currency(row[3], row[9]),
row[6],
row[4], row[4],
row[6],
]); ]);
}); });
this.get_dt_columns(); this.get_dt_columns();
@@ -104,7 +116,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
{ {
name: __("Document Name"), name: __("Document Name"),
editable: false, editable: false,
width: 150, width: 1,
}, },
{ {
name: __("Reference Date"), name: __("Reference Date"),
@@ -112,25 +124,19 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
width: 120, width: 120,
}, },
{ {
name: "Posting Date", name: __("Remaining"),
editable: false,
width: 120,
},
{
name: __("Amount"),
editable: false, editable: false,
width: 100, width: 100,
}, },
{
name: __("Party"),
editable: false,
width: 120,
},
{ {
name: __("Reference Number"), name: __("Reference Number"),
editable: false, 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", fieldname: "exact_match",
onchange: () => this.update_options(), 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", fieldtype: "Section Break",
fieldname: "section_break_1", fieldname: "section_break_1",
@@ -289,7 +305,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
fieldtype: "Column Break", fieldtype: "Column Break",
}, },
{ {
default: "Journal Entry Type", default: "Bank Entry",
fieldname: "journal_entry_type", fieldname: "journal_entry_type",
fieldtype: "Select", fieldtype: "Select",
label: "Journal Entry Type", label: "Journal Entry Type",
@@ -364,7 +380,12 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
fieldtype: "Section Break", fieldtype: "Section Break",
fieldname: "details_section", fieldname: "details_section",
label: "Transaction Details", label: "Transaction Details",
collapsible: 1, },
{
fieldname: "date",
fieldtype: "Date",
label: "Date",
read_only: 1,
}, },
{ {
fieldname: "deposit", fieldname: "deposit",
@@ -381,14 +402,14 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
read_only: 1, read_only: 1,
}, },
{ {
fieldname: "description", fieldname: "column_break_17",
fieldtype: "Small Text", fieldtype: "Column Break",
label: "Description",
read_only: 1, read_only: 1,
}, },
{ {
fieldname: "column_break_17", fieldname: "description",
fieldtype: "Column Break", fieldtype: "Small Text",
label: "Description",
read_only: 1, read_only: 1,
}, },
{ {
@@ -398,7 +419,6 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
options: "Currency", options: "Currency",
read_only: 1, read_only: 1,
}, },
{ {
fieldname: "unallocated_amount", fieldname: "unallocated_amount",
fieldtype: "Currency", fieldtype: "Currency",
@@ -593,4 +613,4 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
} }
} }
}; };

View File

@@ -91,6 +91,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
} }
_calculate_taxes_and_totals() { _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.validate_conversion_rate();
this.calculate_item_values(); this.calculate_item_values();
this.initialize_taxes(); this.initialize_taxes();
@@ -122,7 +125,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
calculate_item_values() { calculate_item_values() {
var me = this; var me = this;
if (!this.discount_amount_applied) { 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); frappe.model.round_floats_in(item);
item.net_rate = item.rate; item.net_rate = item.rate;
item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty; 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)); item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item));
} }
else { else {
let qty = item.qty || 1; // allow for '0' qty on Credit/Debit notes
qty = me.frm.doc.is_return ? -1 * qty : qty; let qty = item.qty || -1
item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item)); 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; 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 item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
var cumulated_tax_fraction = 0.0; var cumulated_tax_fraction = 0.0;
var total_inclusive_tax_amount_per_qty = 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; 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; 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 += item.amount;
me.frm.doc.total_qty += item.qty; me.frm.doc.total_qty += item.qty;
me.frm.doc.base_total += item.base_amount; 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); var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
$.each(me.frm.doc["taxes"] || [], function(i, tax) { $.each(me.frm.doc["taxes"] || [], function(i, tax) {
// tax_amount represents the amount of tax for the current step // 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 // Adjust divisional loss to the last item
if (tax.charge_type == "Actual") { if (tax.charge_type == "Actual") {
actual_tax_dict[tax.idx] -= current_tax_amount; 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]; 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 // 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.round_off_totals(tax);
me.set_in_company_currency(tax, me.set_in_company_currency(tax,
["tax_amount", "tax_amount_after_discount_amount"]); ["tax_amount", "tax_amount_after_discount_amount"]);
@@ -599,10 +602,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
_cleanup() { _cleanup() {
this.frm.doc.base_in_words = this.frm.doc.in_words = ""; 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(items && items.length) {
if(!frappe.meta.get_docfield(this.frm.doc["items"][0].doctype, "item_tax_amount", this.frm.doctype)) { if(!frappe.meta.get_docfield(items[0].doctype, "item_tax_amount", this.frm.doctype)) {
$.each(this.frm.doc["items"] || [], function(i, item) { $.each(items || [], function(i, item) {
delete item["item_tax_amount"]; delete item["item_tax_amount"];
}); });
} }
@@ -655,7 +659,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
var net_total = 0; var net_total = 0;
// calculate item amount after Discount Amount // calculate item amount after Discount Amount
if (total_for_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; distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount;
item.net_amount = flt(item.net_amount - distributed_amount, item.net_amount = flt(item.net_amount - distributed_amount,
precision("base_amount", item)); 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 // 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")) 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 var discount_amount_loss = flt(me.frm.doc.net_total - net_total
- me.frm.doc.discount_amount, precision("net_total")); - me.frm.doc.discount_amount, precision("net_total"));
item.net_amount = flt(item.net_amount + discount_amount_loss, 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"]);
}
}; };

View File

@@ -488,7 +488,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
() => { () => {
var d = locals[cdt][cdn]; var d = locals[cdt][cdn];
me.add_taxes_from_item_tax_template(d.item_tax_rate); 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); me.apply_product_discount(d);
} }
}, },
@@ -1884,11 +1884,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
get_advances() { get_advances() {
if(!this.frm.is_return) { if(!this.frm.is_return) {
var me = this;
return this.frm.call({ return this.frm.call({
method: "set_advances", method: "set_advances",
doc: this.frm.doc, doc: this.frm.doc,
callback: function(r, rt) { callback: function(r, rt) {
refresh_field("advances"); refresh_field("advances");
me.frm.dirty();
} }
}) })
} }

View File

@@ -221,9 +221,9 @@ $.extend(erpnext.utils, {
callback: function(r) { callback: function(r) {
if (r.message && r.message.length) { if (r.message && r.message.length) {
r.message.forEach((dimension) => { 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, { filters.splice(index, 0, {
"fieldname": dimension["fieldname"], "fieldname": dimension["fieldname"],
"label": __(dimension["doctype"]), "label": __(dimension["doctype"]),
@@ -232,6 +232,11 @@ $.extend(erpnext.utils, {
return frappe.db.get_link_options(dimension["doctype"], txt); 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);
}
} }
}); });
} }

View File

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

View File

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

View File

@@ -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) {
// }
});

View File

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

View File

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

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2021, Havenir Solutions and Contributors
# See license.txt
# import frappe
import unittest
class TestKSAVATSalesAccount(unittest.TestCase):
pass

View File

@@ -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');
}
});

View File

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

View File

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

View File

@@ -1,5 +0,0 @@
frappe.listview_settings['KSA VAT Setting'] = {
onload () {
frappe.breadcrumbs.add('Accounts');
}
}

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2021, Havenir Solutions and Contributors
# See license.txt
# import frappe
import unittest
class TestKSAVATSetting(unittest.TestCase):
pass

View File

@@ -1,32 +0,0 @@
{
"absolute_value": 0,
"align_labels_right": 0,
"creation": "2021-12-07 13:25:05.424827",
"css": "",
"custom_format": 1,
"default_print_language": "en",
"disabled": 1,
"doc_type": "POS Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font_size": 0,
"html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n<p class=\"text-center\" style=\"margin-bottom: 1rem\">\n\t{{ doc.company }}<br>\n\t<b>{{ doc.select_print_heading or _(\"Invoice\") }}</b><br>\n\t<img src={{doc.ksa_einv_qr}}>\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Cashier\") }}:</b> {{ doc.owner }}<br>\n\t<b>{{ _(\"Customer\") }}:</b> {{ doc.customer_name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t<b>{{ _(\"Time\") }}:</b> {{ doc.get_formatted(\"posting_time\") }}<br>\n</p>\n\n<hr>\n<table class=\"table table-condensed\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"40%\">{{ _(\"Item\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"35%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"SR.No\") }}:</b><br>\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"net_amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.change_amount -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t{%- endif -%}\n\t</tbody>\n</table>\n<hr>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
"idx": 0,
"line_breaks": 0,
"margin_bottom": 0.0,
"margin_left": 0.0,
"margin_right": 0.0,
"margin_top": 0.0,
"modified": "2021-12-08 10:25:01.930885",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA POS Invoice",
"owner": "Administrator",
"page_number": "Hide",
"print_format_builder": 0,
"print_format_builder_beta": 0,
"print_format_type": "Jinja",
"raw_printing": 0,
"show_section_headings": 0,
"standard": "Yes"
}

File diff suppressed because one or more lines are too long

View File

@@ -1,59 +0,0 @@
// Copyright (c) 2016, Havenir Solutions and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["KSA VAT"] = {
onload() {
frappe.breadcrumbs.add('Accounts');
},
"filters": [
{
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"reqd": 1,
"default": frappe.defaults.get_user_default("Company")
},
{
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1),
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.datetime.get_today()
}
],
"formatter": function(value, row, column, data, default_formatter) {
if (data
&& (data.title=='VAT on Sales' || data.title=='VAT on Purchases')
&& data.title==value) {
value = $(`<span>${value}</span>`);
var $value = $(value).css("font-weight", "bold");
value = $value.wrap("<p></p>").parent().html();
return value
}else if (data.title=='Grand Total'){
if (data.title==value) {
value = $(`<span>${value}</span>`);
var $value = $(value).css("font-weight", "bold");
value = $value.wrap("<p></p>").parent().html();
return value
}else{
value = default_formatter(value, row, column, data);
value = $(`<span>${value}</span>`);
var $value = $(value).css("font-weight", "bold");
value = $value.wrap("<p></p>").parent().html();
return value
}
}else{
value = default_formatter(value, row, column, data);
return value;
}
},
};

View File

@@ -1,32 +0,0 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-07-13 08:54:38.000949",
"disable_prepared_report": 1,
"disabled": 1,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-08-26 04:14:37.202594",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA VAT",
"owner": "Administrator",
"prepared_report": 1,
"ref_doctype": "GL Entry",
"report_name": "KSA VAT",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
},
{
"role": "Accounts Manager"
},
{
"role": "Accounts User"
}
]
}

View File

@@ -1,231 +0,0 @@
# Copyright (c) 2013, Havenir Solutions and contributors
# For license information, please see license.txt
import json
import frappe
from frappe import _
from frappe.utils import get_url_to_list
def execute(filters=None):
columns = columns = get_columns()
data = get_data(filters)
return columns, data
def get_columns():
return [
{
"fieldname": "title",
"label": _("Title"),
"fieldtype": "Data",
"width": 300,
},
{
"fieldname": "amount",
"label": _("Amount (SAR)"),
"fieldtype": "Currency",
"options": "currency",
"width": 150,
},
{
"fieldname": "adjustment_amount",
"label": _("Adjustment (SAR)"),
"fieldtype": "Currency",
"options": "currency",
"width": 150,
},
{
"fieldname": "vat_amount",
"label": _("VAT Amount (SAR)"),
"fieldtype": "Currency",
"options": "currency",
"width": 150,
},
{
"fieldname": "currency",
"label": _("Currency"),
"fieldtype": "Currency",
"width": 150,
"hidden": 1,
},
]
def get_data(filters):
data = []
# Validate if vat settings exist
company = filters.get("company")
company_currency = frappe.get_cached_value("Company", company, "default_currency")
if frappe.db.exists("KSA VAT Setting", company) is None:
url = get_url_to_list("KSA VAT Setting")
frappe.msgprint(_('Create <a href="{}">KSA VAT Setting</a> for this company').format(url))
return data
ksa_vat_setting = frappe.get_doc("KSA VAT Setting", company)
# Sales Heading
append_data(data, "VAT on Sales", "", "", "", company_currency)
grand_total_taxable_amount = 0
grand_total_taxable_adjustment_amount = 0
grand_total_tax = 0
for vat_setting in ksa_vat_setting.ksa_vat_sales_accounts:
(
total_taxable_amount,
total_taxable_adjustment_amount,
total_tax,
) = get_tax_data_for_each_vat_setting(vat_setting, filters, "Sales Invoice")
# Adding results to data
append_data(
data,
vat_setting.title,
total_taxable_amount,
total_taxable_adjustment_amount,
total_tax,
company_currency,
)
grand_total_taxable_amount += total_taxable_amount
grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
grand_total_tax += total_tax
# Sales Grand Total
append_data(
data,
"Grand Total",
grand_total_taxable_amount,
grand_total_taxable_adjustment_amount,
grand_total_tax,
company_currency,
)
# Blank Line
append_data(data, "", "", "", "", company_currency)
# Purchase Heading
append_data(data, "VAT on Purchases", "", "", "", company_currency)
grand_total_taxable_amount = 0
grand_total_taxable_adjustment_amount = 0
grand_total_tax = 0
for vat_setting in ksa_vat_setting.ksa_vat_purchase_accounts:
(
total_taxable_amount,
total_taxable_adjustment_amount,
total_tax,
) = get_tax_data_for_each_vat_setting(vat_setting, filters, "Purchase Invoice")
# Adding results to data
append_data(
data,
vat_setting.title,
total_taxable_amount,
total_taxable_adjustment_amount,
total_tax,
company_currency,
)
grand_total_taxable_amount += total_taxable_amount
grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
grand_total_tax += total_tax
# Purchase Grand Total
append_data(
data,
"Grand Total",
grand_total_taxable_amount,
grand_total_taxable_adjustment_amount,
grand_total_tax,
company_currency,
)
return data
def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype):
"""
(KSA, {filters}, 'Sales Invoice') => 500, 153, 10 \n
calculates and returns \n
total_taxable_amount, total_taxable_adjustment_amount, total_tax"""
from_date = filters.get("from_date")
to_date = filters.get("to_date")
# Initiate variables
total_taxable_amount = 0
total_taxable_adjustment_amount = 0
total_tax = 0
# Fetch All Invoices
invoices = frappe.get_all(
doctype,
filters={"docstatus": 1, "posting_date": ["between", [from_date, to_date]]},
fields=["name", "is_return"],
)
for invoice in invoices:
invoice_items = frappe.get_all(
f"{doctype} Item",
filters={
"docstatus": 1,
"parent": invoice.name,
"item_tax_template": vat_setting.item_tax_template,
},
fields=["item_code", "net_amount"],
)
for item in invoice_items:
# Summing up total taxable amount
if invoice.is_return == 0:
total_taxable_amount += item.net_amount
if invoice.is_return == 1:
total_taxable_adjustment_amount += item.net_amount
# Summing up total tax
total_tax += get_tax_amount(item.item_code, vat_setting.account, doctype, invoice.name)
return total_taxable_amount, total_taxable_adjustment_amount, total_tax
def append_data(data, title, amount, adjustment_amount, vat_amount, company_currency):
"""Returns data with appended value."""
data.append(
{
"title": _(title),
"amount": amount,
"adjustment_amount": adjustment_amount,
"vat_amount": vat_amount,
"currency": company_currency,
}
)
def get_tax_amount(item_code, account_head, doctype, parent):
if doctype == "Sales Invoice":
tax_doctype = "Sales Taxes and Charges"
elif doctype == "Purchase Invoice":
tax_doctype = "Purchase Taxes and Charges"
item_wise_tax_detail = frappe.get_value(
tax_doctype,
{"docstatus": 1, "parent": parent, "account_head": account_head},
"item_wise_tax_detail",
)
tax_amount = 0
if item_wise_tax_detail and len(item_wise_tax_detail) > 0:
item_wise_tax_detail = json.loads(item_wise_tax_detail)
for key, value in item_wise_tax_detail.items():
if key == item_code:
tax_amount = value[1]
break
return tax_amount

View File

@@ -1,173 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.permissions import add_permission, update_permission_property
from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import (
create_ksa_vat_setting,
)
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
def setup(company=None, patch=True):
add_print_formats()
add_permissions()
make_custom_fields()
def add_print_formats():
frappe.reload_doc("regional", "print_format", "detailed_tax_invoice", force=True)
frappe.reload_doc("regional", "print_format", "simplified_tax_invoice", force=True)
frappe.reload_doc("regional", "print_format", "tax_invoice", force=True)
frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True)
frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True)
for d in (
"Simplified Tax Invoice",
"Detailed Tax Invoice",
"Tax Invoice",
"KSA VAT Invoice",
"KSA POS Invoice",
):
frappe.db.set_value("Print Format", d, "disabled", 0)
def add_permissions():
"""Add Permissions for KSA VAT Setting."""
add_permission("KSA VAT Setting", "All", 0)
for role in ("Accounts Manager", "Accounts User", "System Manager"):
add_permission("KSA VAT Setting", role, 0)
update_permission_property("KSA VAT Setting", role, 0, "write", 1)
update_permission_property("KSA VAT Setting", role, 0, "create", 1)
"""Enable KSA VAT Report"""
frappe.db.set_value("Report", "KSA VAT", "disabled", 0)
def make_custom_fields():
"""Create Custom fields
- QR code Image file
- Company Name in Arabic
- Address in Arabic
"""
is_zero_rated = dict(
fieldname="is_zero_rated",
label="Is Zero Rated",
fieldtype="Check",
fetch_from="item_code.is_zero_rated",
insert_after="description",
print_hide=1,
)
is_exempt = dict(
fieldname="is_exempt",
label="Is Exempt",
fieldtype="Check",
fetch_from="item_code.is_exempt",
insert_after="is_zero_rated",
print_hide=1,
)
purchase_invoice_fields = [
dict(
fieldname="company_trn",
label="Company TRN",
fieldtype="Read Only",
insert_after="shipping_address",
fetch_from="company.tax_id",
print_hide=1,
),
dict(
fieldname="supplier_name_in_arabic",
label="Supplier Name in Arabic",
fieldtype="Read Only",
insert_after="supplier_name",
fetch_from="supplier.supplier_name_in_arabic",
print_hide=1,
),
]
sales_invoice_fields = [
dict(
fieldname="company_trn",
label="Company TRN",
fieldtype="Read Only",
insert_after="company_address",
fetch_from="company.tax_id",
print_hide=1,
),
dict(
fieldname="customer_name_in_arabic",
label="Customer Name in Arabic",
fieldtype="Read Only",
insert_after="customer_name",
fetch_from="customer.customer_name_in_arabic",
print_hide=1,
),
dict(
fieldname="ksa_einv_qr",
label="KSA E-Invoicing QR",
fieldtype="Attach Image",
read_only=1,
no_copy=1,
hidden=1,
),
]
custom_fields = {
"Item": [is_zero_rated, is_exempt],
"Customer": [
dict(
fieldname="customer_name_in_arabic",
label="Customer Name in Arabic",
fieldtype="Data",
insert_after="customer_name",
),
],
"Supplier": [
dict(
fieldname="supplier_name_in_arabic",
label="Supplier Name in Arabic",
fieldtype="Data",
insert_after="supplier_name",
),
],
"Purchase Invoice": purchase_invoice_fields,
"Purchase Order": purchase_invoice_fields,
"Purchase Receipt": purchase_invoice_fields,
"Sales Invoice": sales_invoice_fields,
"POS Invoice": sales_invoice_fields,
"Sales Order": sales_invoice_fields,
"Delivery Note": sales_invoice_fields,
"Sales Invoice Item": [is_zero_rated, is_exempt],
"POS Invoice Item": [is_zero_rated, is_exempt],
"Purchase Invoice Item": [is_zero_rated, is_exempt],
"Sales Order Item": [is_zero_rated, is_exempt],
"Delivery Note Item": [is_zero_rated, is_exempt],
"Quotation Item": [is_zero_rated, is_exempt],
"Purchase Order Item": [is_zero_rated, is_exempt],
"Purchase Receipt Item": [is_zero_rated, is_exempt],
"Supplier Quotation Item": [is_zero_rated, is_exempt],
"Address": [
dict(
fieldname="address_in_arabic",
label="Address in Arabic",
fieldtype="Data",
insert_after="address_line2",
)
],
"Company": [
dict(
fieldname="company_name_in_arabic",
label="Company Name In Arabic",
fieldtype="Data",
insert_after="company_name",
)
],
}
create_custom_fields(custom_fields, ignore_validate=True, update=True)
def update_regional_tax_settings(country, company):
create_ksa_vat_setting(company)

View File

@@ -1,169 +0,0 @@
import io
import os
from base64 import b64encode
import frappe
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.utils.data import add_to_date, get_time, getdate
from pyqrcode import create as qr_create
from erpnext import get_region
def create_qr_code(doc, method=None):
region = get_region(doc.company)
if region not in ["Saudi Arabia"]:
return
# if QR Code field not present, create it. Invoices without QR are invalid as per law.
if not hasattr(doc, "ksa_einv_qr"):
create_custom_fields(
{
doc.doctype: [
dict(
fieldname="ksa_einv_qr",
label="KSA E-Invoicing QR",
fieldtype="Attach Image",
read_only=1,
no_copy=1,
hidden=1,
)
]
}
)
# Don't create QR Code if it already exists
qr_code = doc.get("ksa_einv_qr")
if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}):
return
meta = frappe.get_meta(doc.doctype)
if "ksa_einv_qr" in [d.fieldname for d in meta.get_image_fields()]:
"""TLV conversion for
1. Seller's Name
2. VAT Number
3. Time Stamp
4. Invoice Amount
5. VAT Amount
"""
tlv_array = []
# Sellers Name
seller_name = frappe.db.get_value("Company", doc.company, "company_name_in_arabic")
if not seller_name:
frappe.throw(_("Arabic name missing for {} in the company document").format(doc.company))
tag = bytes([1]).hex()
length = bytes([len(seller_name.encode("utf-8"))]).hex()
value = seller_name.encode("utf-8").hex()
tlv_array.append("".join([tag, length, value]))
# VAT Number
tax_id = frappe.db.get_value("Company", doc.company, "tax_id")
if not tax_id:
frappe.throw(_("Tax ID missing for {} in the company document").format(doc.company))
tag = bytes([2]).hex()
length = bytes([len(tax_id)]).hex()
value = tax_id.encode("utf-8").hex()
tlv_array.append("".join([tag, length, value]))
# Time Stamp
posting_date = getdate(doc.posting_date)
time = get_time(doc.posting_time)
seconds = time.hour * 60 * 60 + time.minute * 60 + time.second
time_stamp = add_to_date(posting_date, seconds=seconds)
time_stamp = time_stamp.strftime("%Y-%m-%dT%H:%M:%SZ")
tag = bytes([3]).hex()
length = bytes([len(time_stamp)]).hex()
value = time_stamp.encode("utf-8").hex()
tlv_array.append("".join([tag, length, value]))
# Invoice Amount
invoice_amount = str(doc.base_grand_total)
tag = bytes([4]).hex()
length = bytes([len(invoice_amount)]).hex()
value = invoice_amount.encode("utf-8").hex()
tlv_array.append("".join([tag, length, value]))
# VAT Amount
vat_amount = str(get_vat_amount(doc))
tag = bytes([5]).hex()
length = bytes([len(vat_amount)]).hex()
value = vat_amount.encode("utf-8").hex()
tlv_array.append("".join([tag, length, value]))
# Joining bytes into one
tlv_buff = "".join(tlv_array)
# base64 conversion for QR Code
base64_string = b64encode(bytes.fromhex(tlv_buff)).decode()
qr_image = io.BytesIO()
url = qr_create(base64_string, error="L")
url.png(qr_image, scale=2, quiet_zone=1)
name = frappe.generate_hash(doc.name, 5)
# making file
filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
_file = frappe.get_doc(
{
"doctype": "File",
"file_name": filename,
"is_private": 0,
"content": qr_image.getvalue(),
"attached_to_doctype": doc.get("doctype"),
"attached_to_name": doc.get("name"),
"attached_to_field": "ksa_einv_qr",
}
)
_file.save()
# assigning to document
doc.db_set("ksa_einv_qr", _file.file_url)
doc.notify_update()
def get_vat_amount(doc):
vat_settings = frappe.db.get_value("KSA VAT Setting", {"company": doc.company})
vat_accounts = []
vat_amount = 0
if vat_settings:
vat_settings_doc = frappe.get_cached_doc("KSA VAT Setting", vat_settings)
for row in vat_settings_doc.get("ksa_vat_sales_accounts"):
vat_accounts.append(row.account)
for tax in doc.get("taxes"):
if tax.account_head in vat_accounts:
vat_amount += tax.base_tax_amount
return vat_amount
def delete_qr_code_file(doc, method=None):
region = get_region(doc.company)
if region not in ["Saudi Arabia"]:
return
if hasattr(doc, "ksa_einv_qr"):
if doc.get("ksa_einv_qr"):
file_doc = frappe.get_list("File", {"file_url": doc.get("ksa_einv_qr")})
if len(file_doc):
frappe.delete_doc("File", file_doc[0].name)
def delete_vat_settings_for_company(doc, method=None):
if doc.country != "Saudi Arabia":
return
if frappe.db.exists("KSA VAT Setting", doc.name):
frappe.delete_doc("KSA VAT Setting", doc.name)

Some files were not shown because too many files have changed in this diff Show More