Merge branch 'develop' into alternative-items-quotation

This commit is contained in:
Marica
2023-02-23 13:20:28 +05:30
committed by GitHub
108 changed files with 1168 additions and 1620 deletions

View File

@@ -16,6 +16,7 @@ erpnext/maintenance/ @rohitwaghchaure @s-aga-r
erpnext/manufacturing/ @rohitwaghchaure @s-aga-r erpnext/manufacturing/ @rohitwaghchaure @s-aga-r
erpnext/quality_management/ @rohitwaghchaure @s-aga-r erpnext/quality_management/ @rohitwaghchaure @s-aga-r
erpnext/stock/ @rohitwaghchaure @s-aga-r erpnext/stock/ @rohitwaghchaure @s-aga-r
erpnext/subcontracting @rohitwaghchaure @s-aga-r
erpnext/crm/ @NagariaHussain erpnext/crm/ @NagariaHussain
erpnext/education/ @rutwikhdev erpnext/education/ @rutwikhdev

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

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

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

@@ -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()
@@ -238,21 +244,16 @@ class JournalEntry(AccountsController):
): ):
processed_assets.append(d.reference_name) processed_assets.append(d.reference_name)
asset = frappe.db.get_value( asset = frappe.get_doc("Asset", d.reference_name)
"Asset", d.reference_name, ["calculate_depreciation", "value_after_depreciation"], as_dict=1
)
if asset.calculate_depreciation: if asset.calculate_depreciation:
continue continue
depr_value = d.debit or d.credit depr_value = d.debit or d.credit
frappe.db.set_value( asset.db_set("value_after_depreciation", asset.value_after_depreciation - depr_value)
"Asset",
d.reference_name, asset.set_status()
"value_after_depreciation",
asset.value_after_depreciation - depr_value,
)
def update_inter_company_jv(self): def update_inter_company_jv(self):
if ( if (
@@ -348,12 +349,9 @@ class JournalEntry(AccountsController):
else: else:
depr_value = d.debit or d.credit depr_value = d.debit or d.credit
frappe.db.set_value( asset.db_set("value_after_depreciation", asset.value_after_depreciation + depr_value)
"Asset",
d.reference_name, asset.set_status()
"value_after_depreciation",
asset.value_after_depreciation + depr_value,
)
def unlink_inter_company_jv(self): def unlink_inter_company_jv(self):
if ( if (

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

View File

@@ -239,7 +239,7 @@
"depends_on": "paid_from", "depends_on": "paid_from",
"fieldname": "paid_from_account_currency", "fieldname": "paid_from_account_currency",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Account Currency", "label": "Account Currency (From)",
"options": "Currency", "options": "Currency",
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1,
@@ -249,7 +249,7 @@
"depends_on": "paid_from", "depends_on": "paid_from",
"fieldname": "paid_from_account_balance", "fieldname": "paid_from_account_balance",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Account Balance", "label": "Account Balance (From)",
"options": "paid_from_account_currency", "options": "paid_from_account_currency",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
@@ -272,7 +272,7 @@
"depends_on": "paid_to", "depends_on": "paid_to",
"fieldname": "paid_to_account_currency", "fieldname": "paid_to_account_currency",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Account Currency", "label": "Account Currency (To)",
"options": "Currency", "options": "Currency",
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1,
@@ -282,7 +282,7 @@
"depends_on": "paid_to", "depends_on": "paid_to",
"fieldname": "paid_to_account_balance", "fieldname": "paid_to_account_balance",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Account Balance", "label": "Account Balance (To)",
"options": "paid_to_account_currency", "options": "paid_to_account_currency",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
@@ -304,7 +304,7 @@
{ {
"fieldname": "source_exchange_rate", "fieldname": "source_exchange_rate",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Exchange Rate", "label": "Source Exchange Rate",
"precision": "9", "precision": "9",
"print_hide": 1, "print_hide": 1,
"reqd": 1 "reqd": 1
@@ -334,7 +334,7 @@
{ {
"fieldname": "target_exchange_rate", "fieldname": "target_exchange_rate",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Exchange Rate", "label": "Target Exchange Rate",
"precision": "9", "precision": "9",
"print_hide": 1, "print_hide": 1,
"reqd": 1 "reqd": 1
@@ -633,14 +633,14 @@
"depends_on": "eval:doc.party_type == 'Supplier'", "depends_on": "eval:doc.party_type == 'Supplier'",
"fieldname": "purchase_taxes_and_charges_template", "fieldname": "purchase_taxes_and_charges_template",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Taxes and Charges Template", "label": "Purchase Taxes and Charges Template",
"options": "Purchase Taxes and Charges Template" "options": "Purchase Taxes and Charges Template"
}, },
{ {
"depends_on": "eval: doc.party_type == 'Customer'", "depends_on": "eval: doc.party_type == 'Customer'",
"fieldname": "sales_taxes_and_charges_template", "fieldname": "sales_taxes_and_charges_template",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Taxes and Charges Template", "label": "Sales Taxes and Charges Template",
"options": "Sales Taxes and Charges Template" "options": "Sales Taxes and Charges Template"
}, },
{ {
@@ -733,7 +733,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-12-08 16:25:43.824051", "modified": "2023-02-14 04:52:30.478523",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",

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,7 +495,7 @@ 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) - flt(ref_doc.advance_paid)
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:

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

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

@@ -472,7 +472,7 @@
"description": "If rate is zero them item will be treated as \"Free Item\"", "description": "If rate is zero them item will be treated as \"Free Item\"",
"fieldname": "free_item_rate", "fieldname": "free_item_rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rate" "label": "Free Item Rate"
}, },
{ {
"collapsible": 1, "collapsible": 1,
@@ -608,7 +608,7 @@
"icon": "fa fa-gift", "icon": "fa fa-gift",
"idx": 1, "idx": 1,
"links": [], "links": [],
"modified": "2022-10-13 19:05:35.056304", "modified": "2023-02-14 04:53:34.887358",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Pricing Rule", "name": "Pricing Rule",

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)
@@ -1491,6 +1491,24 @@ class PurchaseInvoice(BuyingController):
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)
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

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

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

@@ -258,7 +258,7 @@ frappe.ui.form.on('Asset', {
$.each(depr_entries || [], function(i, v) { $.each(depr_entries || [], function(i, v) {
x_intervals.push(frappe.format(v.posting_date, { fieldtype: 'Date' })); x_intervals.push(frappe.format(v.posting_date, { fieldtype: 'Date' }));
let last_asset_value = asset_values[asset_values.length - 1] let last_asset_value = asset_values[asset_values.length - 1]
asset_values.push(last_asset_value - v.value); asset_values.push(flt(last_asset_value - v.value, precision('gross_purchase_amount')));
}); });
} }

View File

@@ -413,11 +413,14 @@ class Asset(AccountsController):
if self.journal_entry_for_scrap: if self.journal_entry_for_scrap:
status = "Scrapped" status = "Scrapped"
elif self.finance_books: else:
idx = self.get_default_finance_book_idx() or 0 expected_value_after_useful_life = 0
value_after_depreciation = self.value_after_depreciation
expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life if self.calculate_depreciation:
value_after_depreciation = self.finance_books[idx].value_after_depreciation idx = self.get_default_finance_book_idx() or 0
expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life
value_after_depreciation = self.finance_books[idx].value_after_depreciation
if flt(value_after_depreciation) <= expected_value_after_useful_life: if flt(value_after_depreciation) <= expected_value_after_useful_life:
status = "Fully Depreciated" status = "Fully Depreciated"
@@ -463,6 +466,7 @@ class Asset(AccountsController):
.where(gle.debit != 0) .where(gle.debit != 0)
.where(gle.is_cancelled == 0) .where(gle.is_cancelled == 0)
.orderby(gle.posting_date) .orderby(gle.posting_date)
.orderby(gle.creation)
).run(as_dict=True) ).run(as_dict=True)
return records return records

View File

@@ -168,7 +168,7 @@ def make_depreciation_entry(asset_depr_schedule_name, date=None):
row.value_after_depreciation -= d.depreciation_amount row.value_after_depreciation -= d.depreciation_amount
row.db_update() row.db_update()
frappe.db.set_value("Asset", asset_name, "depr_entry_posting_status", "Successful") asset.db_set("depr_entry_posting_status", "Successful")
asset.set_status() asset.set_status()

View File

@@ -91,6 +91,9 @@ class AssetRepair(AccountsController):
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes) make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
self.asset_doc.save() self.asset_doc.save()
def after_delete(self):
frappe.get_doc("Asset", self.asset).set_status()
def check_repair_status(self): def check_repair_status(self):
if self.repair_status == "Pending": if self.repair_status == "Pending":
frappe.throw(_("Please update Repair Status.")) frappe.throw(_("Please update Repair Status."))

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

@@ -21,6 +21,7 @@
"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 +141,12 @@
"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"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
@@ -147,7 +154,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-15 14:42:10.200679",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",

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

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

@@ -409,7 +409,14 @@ class SubcontractingController(StockController):
if self.available_materials.get(key) and self.available_materials[key]["batch_no"]: if self.available_materials.get(key) and self.available_materials[key]["batch_no"]:
new_rm_obj = None new_rm_obj = None
for batch_no, batch_qty in self.available_materials[key]["batch_no"].items(): for batch_no, batch_qty in self.available_materials[key]["batch_no"].items():
if batch_qty >= qty: if batch_qty >= qty or (
rm_obj.consumed_qty == 0
and self.backflush_based_on == "BOM"
and len(self.available_materials[key]["batch_no"]) == 1
):
if rm_obj.consumed_qty == 0:
self.__set_consumed_qty(rm_obj, qty)
self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty) self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty)
self.available_materials[key]["batch_no"][batch_no] -= qty self.available_materials[key]["batch_no"][batch_no] -= qty
return return

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"
}, },

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

@@ -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')
@@ -306,7 +304,6 @@ erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
execute:frappe.delete_doc("DocType", "Naming Series") execute:frappe.delete_doc("DocType", "Naming Series")
erpnext.patches.v13_0.job_card_status_on_hold erpnext.patches.v13_0.job_card_status_on_hold
erpnext.patches.v14_0.copy_is_subcontracted_value_to_is_old_subcontracting_flow erpnext.patches.v14_0.copy_is_subcontracted_value_to_is_old_subcontracting_flow
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.patches.v14_0.crm_ux_cleanup erpnext.patches.v14_0.crm_ux_cleanup
erpnext.patches.v14_0.migrate_existing_lead_notes_as_per_the_new_format erpnext.patches.v14_0.migrate_existing_lead_notes_as_per_the_new_format
erpnext.patches.v14_0.remove_india_localisation # 14-07-2022 erpnext.patches.v14_0.remove_india_localisation # 14-07-2022
@@ -315,7 +312,6 @@ erpnext.patches.v14_0.remove_hr_and_payroll_modules # 20-07-2022
erpnext.patches.v14_0.fix_crm_no_of_employees erpnext.patches.v14_0.fix_crm_no_of_employees
erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes
erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries
erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger
erpnext.patches.v13_0.update_schedule_type_in_loans erpnext.patches.v13_0.update_schedule_type_in_loans
erpnext.patches.v13_0.drop_unused_sle_index_parts erpnext.patches.v13_0.drop_unused_sle_index_parts
erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization
@@ -327,3 +323,6 @@ 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
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

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

@@ -408,7 +408,7 @@
"depends_on": "eval:(doc.frequency == \"Daily\" && doc.collect_progress == true)", "depends_on": "eval:(doc.frequency == \"Daily\" && doc.collect_progress == true)",
"fieldname": "daily_time_to_send", "fieldname": "daily_time_to_send",
"fieldtype": "Time", "fieldtype": "Time",
"label": "Time to send" "label": "Daily Time to send"
}, },
{ {
"depends_on": "eval:(doc.frequency == \"Weekly\" && doc.collect_progress == true)", "depends_on": "eval:(doc.frequency == \"Weekly\" && doc.collect_progress == true)",
@@ -421,7 +421,7 @@
"depends_on": "eval:(doc.frequency == \"Weekly\" && doc.collect_progress == true)", "depends_on": "eval:(doc.frequency == \"Weekly\" && doc.collect_progress == true)",
"fieldname": "weekly_time_to_send", "fieldname": "weekly_time_to_send",
"fieldtype": "Time", "fieldtype": "Time",
"label": "Time to send" "label": "Weekly Time to send"
}, },
{ {
"fieldname": "column_break_45", "fieldname": "column_break_45",
@@ -451,7 +451,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"max_attachments": 4, "max_attachments": 4,
"modified": "2022-06-23 16:45:06.108499", "modified": "2023-02-14 04:54:25.819620",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Project", "name": "Project",
@@ -497,4 +497,4 @@
"timeline_field": "customer", "timeline_field": "customer",
"title_field": "project_name", "title_field": "project_name",
"track_seen": 1 "track_seen": 1
} }

View File

@@ -282,21 +282,21 @@
{ {
"fieldname": "base_total_costing_amount", "fieldname": "base_total_costing_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Total Costing Amount", "label": "Base Total Costing Amount",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "base_total_billable_amount", "fieldname": "base_total_billable_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Total Billable Amount", "label": "Base Total Billable Amount",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "base_total_billed_amount", "fieldname": "base_total_billed_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Total Billed Amount", "label": "Base Total Billed Amount",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@@ -311,10 +311,11 @@
"idx": 1, "idx": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-06-15 22:08:53.930200", "modified": "2023-02-14 04:55:41.735991",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Timesheet", "name": "Timesheet",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@@ -388,5 +389,6 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [],
"title_field": "title" "title_field": "title"
} }

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

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

View File

@@ -1,47 +0,0 @@
[
{
"type": "Sales Account",
"accounts": [
{
"title": "Standard rated Sales",
"item_tax_template": "KSA VAT 5%",
"account": "VAT 5%"
},
{
"title": "Zero rated domestic sales",
"item_tax_template": "KSA VAT Zero",
"account": "VAT Zero"
},
{
"title": "Exempted sales",
"item_tax_template": "KSA VAT Exempted",
"account": "VAT Exempted"
}
]
},
{
"type": "Purchase Account",
"accounts": [
{
"title": "Standard rated domestic purchases",
"item_tax_template": "KSA VAT 5%",
"account": "VAT 5%"
},
{
"title": "Imports subject to VAT paid at customs",
"item_tax_template": "KSA Excise 50%",
"account": "Excise 50%"
},
{
"title": "Zero rated purchases",
"item_tax_template": "KSA VAT Zero",
"account": "VAT Zero"
},
{
"title": "Exempted purchases",
"item_tax_template": "KSA VAT Exempted",
"account": "VAT Exempted"
}
]
}
]

View File

@@ -1,46 +0,0 @@
import json
import os
import frappe
def create_ksa_vat_setting(company):
"""On creation of first company. Creates KSA VAT Setting"""
company = frappe.get_doc("Company", company)
file_path = os.path.join(os.path.dirname(__file__), "..", "data", "ksa_vat_settings.json")
with open(file_path, "r") as json_file:
account_data = json.load(json_file)
# Creating KSA VAT Setting
ksa_vat_setting = frappe.get_doc({"doctype": "KSA VAT Setting", "company": company.name})
for data in account_data:
if data["type"] == "Sales Account":
for row in data["accounts"]:
item_tax_template = row["item_tax_template"]
account = row["account"]
ksa_vat_setting.append(
"ksa_vat_sales_accounts",
{
"title": row["title"],
"item_tax_template": f"{item_tax_template} - {company.abbr}",
"account": f"{account} - {company.abbr}",
},
)
elif data["type"] == "Purchase Account":
for row in data["accounts"]:
item_tax_template = row["item_tax_template"]
account = row["account"]
ksa_vat_setting.append(
"ksa_vat_purchase_accounts",
{
"title": row["title"],
"item_tax_template": f"{item_tax_template} - {company.abbr}",
"account": f"{account} - {company.abbr}",
},
)
ksa_vat_setting.save()

View File

@@ -24,10 +24,10 @@
"account_manager", "account_manager",
"image", "image",
"defaults_tab", "defaults_tab",
"default_price_list", "default_currency",
"default_bank_account", "default_bank_account",
"column_break_14", "column_break_14",
"default_currency", "default_price_list",
"internal_customer_section", "internal_customer_section",
"is_internal_customer", "is_internal_customer",
"represents_company", "represents_company",
@@ -568,11 +568,10 @@
"link_fieldname": "party" "link_fieldname": "party"
} }
], ],
"modified": "2022-11-08 15:52:34.462657", "modified": "2023-02-18 11:04:46.343527",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Customer", "name": "Customer",
"name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field", "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [

View File

@@ -1,5 +1,6 @@
{ {
"actions": [], "actions": [],
"allow_import": 1,
"creation": "2021-08-27 19:28:07.559978", "creation": "2021-08-27 19:28:07.559978",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@@ -51,7 +52,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-09-14 13:27:58.612334", "modified": "2023-02-15 13:00:50.379713",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Party Specific Item", "name": "Party Specific Item",
@@ -72,6 +73,7 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"title_field": "party", "title_field": "party",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -216,7 +216,7 @@ def get_sales_order_details(company_list, filters):
) )
if filters.get("item_group"): if filters.get("item_group"):
query = query.where(db_so_item.item_group == frappe.db.escape(filters.item_group)) query = query.where(db_so_item.item_group == filters.item_group)
if filters.get("from_date"): if filters.get("from_date"):
query = query.where(db_so.transaction_date >= filters.from_date) query = query.where(db_so.transaction_date >= filters.from_date)
@@ -225,7 +225,7 @@ def get_sales_order_details(company_list, filters):
query = query.where(db_so.transaction_date <= filters.to_date) query = query.where(db_so.transaction_date <= filters.to_date)
if filters.get("item_code"): if filters.get("item_code"):
query = query.where(db_so_item.item_group == frappe.db.escape(filters.item_code)) query = query.where(db_so_item.item_code == filters.item_code)
if filters.get("customer"): if filters.get("customer"):
query = query.where(db_so.customer == filters.customer) query = query.where(db_so.customer == filters.customer)

View File

@@ -418,8 +418,6 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
callback: function(r) { callback: function(r) {
if(r.message) { if(r.message) {
frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message); frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message);
} else {
frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message);
} }
} }
}); });

View File

@@ -8,7 +8,6 @@ from frappe.permissions import (
get_doc_permissions, get_doc_permissions,
has_permission, has_permission,
remove_user_permission, remove_user_permission,
set_user_permission_if_allowed,
) )
from frappe.utils import cstr, getdate, today, validate_email_address from frappe.utils import cstr, getdate, today, validate_email_address
from frappe.utils.nestedset import NestedSet from frappe.utils.nestedset import NestedSet
@@ -96,7 +95,7 @@ class Employee(NestedSet):
return return
add_user_permission("Employee", self.name, self.user_id) add_user_permission("Employee", self.name, self.user_id)
set_user_permission_if_allowed("Company", self.company, self.user_id) add_user_permission("Company", self.company, self.user_id)
def update_user(self): def update_user(self):
# add employee role if missing # add employee role if missing

View File

@@ -3,13 +3,17 @@
import frappe import frappe
from frappe import _ from frappe import _, qb
from frappe.desk.notifications import clear_notifications from frappe.desk.notifications import clear_notifications
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint from frappe.utils import cint, create_batch
class TransactionDeletionRecord(Document): class TransactionDeletionRecord(Document):
def __init__(self, *args, **kwargs):
super(TransactionDeletionRecord, self).__init__(*args, **kwargs)
self.batch_size = 5000
def validate(self): def validate(self):
frappe.only_for("System Manager") frappe.only_for("System Manager")
self.validate_doctypes_to_be_ignored() self.validate_doctypes_to_be_ignored()
@@ -155,8 +159,9 @@ class TransactionDeletionRecord(Document):
"DocField", filters={"fieldtype": "Table", "parent": doctype}, pluck="options" "DocField", filters={"fieldtype": "Table", "parent": doctype}, pluck="options"
) )
for table in child_tables: for batch in create_batch(parent_docs_to_be_deleted, self.batch_size):
frappe.db.delete(table, {"parent": ["in", parent_docs_to_be_deleted]}) for table in child_tables:
frappe.db.delete(table, {"parent": ["in", batch]})
def delete_docs_linked_with_specified_company(self, doctype, company_fieldname): def delete_docs_linked_with_specified_company(self, doctype, company_fieldname):
frappe.db.delete(doctype, {company_fieldname: self.company}) frappe.db.delete(doctype, {company_fieldname: self.company})
@@ -181,13 +186,16 @@ class TransactionDeletionRecord(Document):
frappe.db.sql("""update `tabSeries` set current = %s where name=%s""", (last, prefix)) frappe.db.sql("""update `tabSeries` set current = %s where name=%s""", (last, prefix))
def delete_version_log(self, doctype, company_fieldname): def delete_version_log(self, doctype, company_fieldname):
frappe.db.sql( dt = qb.DocType(doctype)
"""delete from `tabVersion` where ref_doctype=%s and docname in names = qb.from_(dt).select(dt.name).where(dt[company_fieldname] == self.company).run(as_list=1)
(select name from `tab{0}` where `{1}`=%s)""".format( names = [x[0] for x in names]
doctype, company_fieldname
), if names:
(doctype, self.company), versions = qb.DocType("Version")
) for batch in create_batch(names, self.batch_size):
qb.from_(versions).delete().where(
(versions.ref_doctype == doctype) & (versions.docname.isin(batch))
).run()
def delete_communications(self, doctype, company_fieldname): def delete_communications(self, doctype, company_fieldname):
reference_docs = frappe.get_all(doctype, filters={company_fieldname: self.company}) reference_docs = frappe.get_all(doctype, filters={company_fieldname: self.company})
@@ -199,7 +207,8 @@ class TransactionDeletionRecord(Document):
) )
communication_names = [c.name for c in communications] communication_names = [c.name for c in communications]
frappe.delete_doc("Communication", communication_names, ignore_permissions=True) for batch in create_batch(communication_names, self.batch_size):
frappe.delete_doc("Communication", batch, ignore_permissions=True)
@frappe.whitelist() @frappe.whitelist()

View File

@@ -4015,34 +4015,6 @@
"tax_rate": 18.00 "tax_rate": 18.00
} }
}, },
"Saudi Arabia": {
"KSA VAT 15%": {
"account_name": "VAT 15%",
"tax_rate": 15.00
},
"KSA VAT 5%": {
"account_name": "VAT 5%",
"tax_rate": 5.00
},
"KSA VAT Zero": {
"account_name": "VAT Zero",
"tax_rate": 0.00
},
"KSA VAT Exempted": {
"account_name": "VAT Exempted",
"tax_rate": 0.00
},
"KSA Excise 50%": {
"account_name": "Excise 50%",
"tax_rate": 50.00
},
"KSA Excise 100%": {
"account_name": "Excise 100%",
"tax_rate": 100.00
}
},
"Serbia": { "Serbia": {
"Serbia Tax": { "Serbia Tax": {
"account_name": "VAT", "account_name": "VAT",

View File

@@ -97,12 +97,12 @@ frappe.ui.form.on("Delivery Note", {
} }
if (frm.doc.docstatus == 1 && !frm.doc.inter_company_reference) { if (frm.doc.docstatus == 1 && !frm.doc.inter_company_reference) {
let internal = me.frm.doc.is_internal_customer; let internal = frm.doc.is_internal_customer;
if (internal) { if (internal) {
let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Receipt" : let button_label = (frm.doc.company === frm.doc.represents_company) ? "Internal Purchase Receipt" :
"Inter Company Purchase Receipt"; "Inter Company Purchase Receipt";
me.frm.add_custom_button(button_label, function() { frm.add_custom_button(__(button_label), function() {
frappe.model.open_mapped_doc({ frappe.model.open_mapped_doc({
method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_inter_company_purchase_receipt', method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_inter_company_purchase_receipt',
frm: frm, frm: frm,

View File

@@ -521,6 +521,7 @@
"allow_bulk_edit": 1, "allow_bulk_edit": 1,
"fieldname": "items", "fieldname": "items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Delivery Note Item",
"oldfieldname": "delivery_note_details", "oldfieldname": "delivery_note_details",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Delivery Note Item", "options": "Delivery Note Item",
@@ -666,6 +667,7 @@
{ {
"fieldname": "taxes", "fieldname": "taxes",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Sales Taxes and Charges",
"oldfieldname": "other_charges", "oldfieldname": "other_charges",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Sales Taxes and Charges" "options": "Sales Taxes and Charges"
@@ -1401,7 +1403,7 @@
"idx": 146, "idx": 146,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-12-12 18:38:53.067799", "modified": "2023-02-14 04:45:44.179670",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note", "name": "Delivery Note",

View File

@@ -14,7 +14,7 @@ frappe.listview_settings['Delivery Note'] = {
return [__("Completed"), "green", "per_billed,=,100"]; return [__("Completed"), "green", "per_billed,=,100"];
} }
}, },
onload: function (listview) { onload: function (doclist) {
const action = () => { const action = () => {
const selected_docs = doclist.get_checked_items(); const selected_docs = doclist.get_checked_items();
const docnames = doclist.get_checked_items(true); const docnames = doclist.get_checked_items(true);
@@ -56,14 +56,14 @@ frappe.listview_settings['Delivery Note'] = {
// doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false); // doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false);
listview.page.add_action_item(__('Create Delivery Trip'), action); doclist.page.add_action_item(__('Create Delivery Trip'), action);
listview.page.add_action_item(__("Sales Invoice"), ()=>{ doclist.page.add_action_item(__("Sales Invoice"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Delivery Note", "Sales Invoice"); erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Sales Invoice");
}); });
listview.page.add_action_item(__("Packaging Slip From Delivery Note"), ()=>{ doclist.page.add_action_item(__("Packaging Slip From Delivery Note"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Delivery Note", "Packing Slip"); erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Packing Slip");
}); });
} }
}; };

View File

@@ -706,7 +706,7 @@
"depends_on": "enable_deferred_expense", "depends_on": "enable_deferred_expense",
"fieldname": "no_of_months_exp", "fieldname": "no_of_months_exp",
"fieldtype": "Int", "fieldtype": "Int",
"label": "No of Months" "label": "No of Months (Expense)"
}, },
{ {
"collapsible": 1, "collapsible": 1,
@@ -911,7 +911,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2023-01-07 22:45:00.341745", "modified": "2023-02-14 04:48:26.343620",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item", "name": "Item",

View File

@@ -358,7 +358,7 @@ class Item(Document):
check_list.append(d.item_tax_template) check_list.append(d.item_tax_template)
def validate_barcode(self): def validate_barcode(self):
from stdnum import ean import barcodenumber
if len(self.barcodes) > 0: if len(self.barcodes) > 0:
for item_barcode in self.barcodes: for item_barcode in self.barcodes:
@@ -376,19 +376,16 @@ class Item(Document):
item_barcode.barcode_type = ( item_barcode.barcode_type = (
"" if item_barcode.barcode_type not in options else item_barcode.barcode_type "" if item_barcode.barcode_type not in options else item_barcode.barcode_type
) )
if item_barcode.barcode_type and item_barcode.barcode_type.upper() in ( if item_barcode.barcode_type:
"EAN", barcode_type = convert_erpnext_to_barcodenumber(item_barcode.barcode_type.upper())
"UPC-A", if barcode_type in barcodenumber.barcodes():
"EAN-13", if not barcodenumber.check_code(barcode_type, item_barcode.barcode):
"EAN-8", frappe.throw(
): _("Barcode {0} is not a valid {1} code").format(
if not ean.is_valid(item_barcode.barcode): item_barcode.barcode, item_barcode.barcode_type
frappe.throw( ),
_("Barcode {0} is not a valid {1} code").format( InvalidBarcode,
item_barcode.barcode, item_barcode.barcode_type )
),
InvalidBarcode,
)
def validate_warehouse_for_reorder(self): def validate_warehouse_for_reorder(self):
"""Validate Reorder level table for duplicate and conditional mandatory""" """Validate Reorder level table for duplicate and conditional mandatory"""
@@ -985,6 +982,22 @@ class Item(Document):
) )
def convert_erpnext_to_barcodenumber(erpnext_number):
convert = {
"UPC-A": "UPCA",
"CODE-39": "CODE39",
"EAN": "EAN13",
"EAN-12": "EAN",
"EAN-8": "EAN8",
"ISBN-10": "ISBN10",
"ISBN-13": "ISBN13",
}
if erpnext_number in convert:
return convert[erpnext_number]
else:
return erpnext_number
def make_item_price(item, price_list_name, item_price): def make_item_price(item, price_list_name, item_price):
frappe.get_doc( frappe.get_doc(
{ {

View File

@@ -579,6 +579,19 @@ class TestItem(FrappeTestCase):
{ {
"barcode": "ARBITRARY_TEXT", "barcode": "ARBITRARY_TEXT",
}, },
{"barcode": "72527273070", "barcode_type": "UPC-A"},
{"barcode": "123456", "barcode_type": "CODE-39"},
{"barcode": "401268452363", "barcode_type": "EAN-12"},
{"barcode": "90311017", "barcode_type": "EAN-8"},
{"barcode": "0123456789012", "barcode_type": "GS1"},
{"barcode": "2211564566668", "barcode_type": "GTIN"},
{"barcode": "0256480249", "barcode_type": "ISBN"},
{"barcode": "0192552570", "barcode_type": "ISBN-10"},
{"barcode": "9781234567897", "barcode_type": "ISBN-13"},
{"barcode": "9771234567898", "barcode_type": "ISSN"},
{"barcode": "4581171967072", "barcode_type": "JAN"},
{"barcode": "12345678", "barcode_type": "PZN"},
{"barcode": "725272730706", "barcode_type": "UPC"},
] ]
create_item(item_code) create_item(item_code)
for barcode_properties in barcode_properties_list: for barcode_properties in barcode_properties_list:

View File

@@ -25,7 +25,7 @@
"fieldtype": "Select", "fieldtype": "Select",
"in_list_view": 1, "in_list_view": 1,
"label": "Barcode Type", "label": "Barcode Type",
"options": "\nEAN\nUPC-A" "options": "\nEAN\nUPC-A\nCODE-39\nEAN-12\nEAN-8\nGS1\nGTIN\nISBN\nISBN-10\nISBN-13\nISSN\nJAN\nPZN\nUPC"
}, },
{ {
"fieldname": "uom", "fieldname": "uom",

View File

@@ -55,7 +55,6 @@ class LandedCostVoucher(Document):
self.get_items_from_purchase_receipts() self.get_items_from_purchase_receipts()
self.set_applicable_charges_on_item() self.set_applicable_charges_on_item()
self.validate_applicable_charges_for_item()
def check_mandatory(self): def check_mandatory(self):
if not self.get("purchase_receipts"): if not self.get("purchase_receipts"):
@@ -115,6 +114,13 @@ class LandedCostVoucher(Document):
total_item_cost += item.get(based_on_field) total_item_cost += item.get(based_on_field)
for item in self.get("items"): for item in self.get("items"):
if not total_item_cost and not item.get(based_on_field):
frappe.throw(
_(
"It's not possible to distribute charges equally when total amount is zero, please set 'Distribute Charges Based On' as 'Quantity'"
)
)
item.applicable_charges = flt( item.applicable_charges = flt(
flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)), flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)),
item.precision("applicable_charges"), item.precision("applicable_charges"),
@@ -162,6 +168,7 @@ class LandedCostVoucher(Document):
) )
def on_submit(self): def on_submit(self):
self.validate_applicable_charges_for_item()
self.update_landed_cost() self.update_landed_cost()
def on_cancel(self): def on_cancel(self):

View File

@@ -175,6 +175,59 @@ class TestLandedCostVoucher(FrappeTestCase):
) )
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0) self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0)
def test_landed_cost_voucher_for_zero_purchase_rate(self):
"Test impact of LCV on future stock balances."
from erpnext.stock.doctype.item.test_item import make_item
item = make_item("LCV Stock Item", {"is_stock_item": 1})
warehouse = "Stores - _TC"
pr = make_purchase_receipt(
item_code=item.name,
warehouse=warehouse,
qty=10,
rate=0,
posting_date=add_days(frappe.utils.nowdate(), -2),
)
self.assertEqual(
frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
"stock_value_difference",
),
0,
)
lcv = make_landed_cost_voucher(
company=pr.company,
receipt_document_type="Purchase Receipt",
receipt_document=pr.name,
charges=100,
distribute_charges_based_on="Distribute Manually",
do_not_save=True,
)
lcv.get_items_from_purchase_receipts()
lcv.items[0].applicable_charges = 100
lcv.save()
lcv.submit()
self.assertTrue(
frappe.db.exists(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
)
)
self.assertEqual(
frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
"stock_value_difference",
),
100,
)
def test_landed_cost_voucher_against_purchase_invoice(self): def test_landed_cost_voucher_against_purchase_invoice(self):
pi = make_purchase_invoice( pi = make_purchase_invoice(
@@ -516,7 +569,7 @@ def make_landed_cost_voucher(**args):
lcv = frappe.new_doc("Landed Cost Voucher") lcv = frappe.new_doc("Landed Cost Voucher")
lcv.company = args.company or "_Test Company" lcv.company = args.company or "_Test Company"
lcv.distribute_charges_based_on = "Amount" lcv.distribute_charges_based_on = args.distribute_charges_based_on or "Amount"
lcv.set( lcv.set(
"purchase_receipts", "purchase_receipts",

View File

@@ -887,18 +887,10 @@ def update_billing_percentage(pr_doc, update_modified=True):
# Update Billing % based on pending accepted qty # Update Billing % based on pending accepted qty
total_amount, total_billed_amount = 0, 0 total_amount, total_billed_amount = 0, 0
for item in pr_doc.items: item_wise_returned_qty = get_item_wise_returned_qty(pr_doc)
return_data = frappe.get_all(
"Purchase Receipt",
fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"],
filters=[
["Purchase Receipt", "docstatus", "=", 1],
["Purchase Receipt", "is_return", "=", 1],
["Purchase Receipt Item", "purchase_receipt_item", "=", item.name],
],
)
returned_qty = return_data[0].qty if return_data else 0 for item in pr_doc.items:
returned_qty = flt(item_wise_returned_qty.get(item.name))
returned_amount = flt(returned_qty) * flt(item.rate) returned_amount = flt(returned_qty) * flt(item.rate)
pending_amount = flt(item.amount) - returned_amount pending_amount = flt(item.amount) - returned_amount
total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt
@@ -915,6 +907,27 @@ def update_billing_percentage(pr_doc, update_modified=True):
pr_doc.notify_update() pr_doc.notify_update()
def get_item_wise_returned_qty(pr_doc):
items = [d.name for d in pr_doc.items]
return frappe._dict(
frappe.get_all(
"Purchase Receipt",
fields=[
"`tabPurchase Receipt Item`.purchase_receipt_item",
"sum(abs(`tabPurchase Receipt Item`.qty)) as qty",
],
filters=[
["Purchase Receipt", "docstatus", "=", 1],
["Purchase Receipt", "is_return", "=", 1],
["Purchase Receipt Item", "purchase_receipt_item", "in", items],
],
group_by="`tabPurchase Receipt Item`.purchase_receipt_item",
as_list=1,
)
)
@frappe.whitelist() @frappe.whitelist()
def make_purchase_invoice(source_name, target_doc=None): def make_purchase_invoice(source_name, target_doc=None):
from erpnext.accounts.party import get_payment_terms_template from erpnext.accounts.party import get_payment_terms_template
@@ -1121,13 +1134,25 @@ def get_item_account_wise_additional_cost(purchase_document):
account.expense_account, {"amount": 0.0, "base_amount": 0.0} account.expense_account, {"amount": 0.0, "base_amount": 0.0}
) )
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][ if total_item_cost > 0:
"amount" item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
] += (account.amount * item.get(based_on_field) / total_item_cost) account.expense_account
]["amount"] += (
account.amount * item.get(based_on_field) / total_item_cost
)
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][ item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
"base_amount" account.expense_account
] += (account.base_amount * item.get(based_on_field) / total_item_cost) ]["base_amount"] += (
account.base_amount * item.get(based_on_field) / total_item_cost
)
else:
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
account.expense_account
]["amount"] += item.applicable_charges
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
account.expense_account
]["base_amount"] += item.applicable_charges
return item_account_wise_cost return item_account_wise_cost

View File

@@ -859,7 +859,8 @@
"label": "Purchase Receipt Item", "label": "Purchase Receipt Item",
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1,
"search_index": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
@@ -974,7 +975,8 @@
"label": "Purchase Invoice Item", "label": "Purchase Invoice Item",
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1,
"search_index": 1
}, },
{ {
"fieldname": "product_bundle", "fieldname": "product_bundle",
@@ -1010,7 +1012,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-11-02 12:49:28.746701", "modified": "2023-01-18 15:48:58.114923",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt Item", "name": "Purchase Receipt Item",

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