diff --git a/CODEOWNERS b/CODEOWNERS index b4503017f12..c4ea16328e6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -16,6 +16,7 @@ erpnext/maintenance/ @rohitwaghchaure @s-aga-r erpnext/manufacturing/ @rohitwaghchaure @s-aga-r erpnext/quality_management/ @rohitwaghchaure @s-aga-r erpnext/stock/ @rohitwaghchaure @s-aga-r +erpnext/subcontracting @rohitwaghchaure @s-aga-r erpnext/crm/ @NagariaHussain erpnext/education/ @rutwikhdev diff --git a/README.md b/README.md index 0708266a470..44bd7296881 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ New passwords will be created for the ERPNext "Administrator" user, the MariaDB 1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community. 2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext. 3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers. -4. [Telegram Group](https://t.me/erpnexthelp) - Get instant help from huge community of users. +4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users. ## Contributing diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js index 059e1d31588..35d606ba3ae 100644 --- a/erpnext/accounts/doctype/bank/bank.js +++ b/erpnext/accounts/doctype/bank/bank.js @@ -118,6 +118,10 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink { } plaid_success(token, response) { - frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' }); + frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.update_bank_account_ids', { + response: response, + }).then(() => { + frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' }); + }); } }; diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js index c083189eb27..ae84154f2df 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js @@ -155,7 +155,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", { } }, - render_chart: frappe.utils.debounce((frm) => { + render_chart(frm) { frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager( { $reconciliation_tool_cards: frm.get_field( @@ -167,7 +167,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", { currency: frm.currency, } ); - }, 500), + }, render(frm) { if (frm.doc.bank_account) { diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 4ba6146c923..c4a23a640c3 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -10,7 +10,7 @@ from frappe.model.document import Document from frappe.query_builder.custom import ConstantColumn from frappe.utils import cint, flt -from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_paid_amount +from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import ( get_amounts_not_reflected_in_system, get_entries, @@ -28,7 +28,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None): filters = [] filters.append(["bank_account", "=", bank_account]) filters.append(["docstatus", "=", 1]) - filters.append(["unallocated_amount", ">", 0]) + filters.append(["unallocated_amount", ">", 0.0]) if to_date: filters.append(["date", "<=", to_date]) if from_date: @@ -58,7 +58,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None): @frappe.whitelist() def get_account_balance(bank_account, till_date): # returns account balance till the specified date - account = frappe.get_cached_value("Bank Account", bank_account, "account") + account = frappe.db.get_value("Bank Account", bank_account, "account") filters = frappe._dict( {"account": account, "report_date": till_date, "include_pos_transactions": 1} ) @@ -66,7 +66,7 @@ def get_account_balance(bank_account, till_date): balance_as_per_system = get_balance_on(filters["account"], filters["report_date"]) - total_debit, total_credit = 0, 0 + total_debit, total_credit = 0.0, 0.0 for d in data: total_debit += flt(d.debit) total_credit += flt(d.credit) @@ -131,10 +131,8 @@ def create_journal_entry_bts( fieldname=["name", "deposit", "withdrawal", "bank_account"], as_dict=True, )[0] - company_account = frappe.get_cached_value( - "Bank Account", bank_transaction.bank_account, "account" - ) - account_type = frappe.get_cached_value("Account", second_account, "account_type") + company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account") + account_type = frappe.db.get_value("Account", second_account, "account_type") if account_type in ["Receivable", "Payable"]: if not (party_type and party): frappe.throw( @@ -147,10 +145,8 @@ def create_journal_entry_bts( accounts.append( { "account": second_account, - "credit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0, - "debit_in_account_currency": bank_transaction.withdrawal - if bank_transaction.withdrawal > 0 - else 0, + "credit_in_account_currency": bank_transaction.deposit, + "debit_in_account_currency": bank_transaction.withdrawal, "party_type": party_type, "party": party, } @@ -160,14 +156,12 @@ def create_journal_entry_bts( { "account": company_account, "bank_account": bank_transaction.bank_account, - "credit_in_account_currency": bank_transaction.withdrawal - if bank_transaction.withdrawal > 0 - else 0, - "debit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0, + "credit_in_account_currency": bank_transaction.withdrawal, + "debit_in_account_currency": bank_transaction.deposit, } ) - company = frappe.get_cached_value("Account", company_account, "company") + company = frappe.get_value("Account", company_account, "company") journal_entry_dict = { "voucher_type": entry_type, @@ -187,16 +181,22 @@ def create_journal_entry_bts( journal_entry.insert() journal_entry.submit() - if bank_transaction.deposit > 0: + if bank_transaction.deposit > 0.0: paid_amount = bank_transaction.deposit else: paid_amount = bank_transaction.withdrawal vouchers = json.dumps( - [{"payment_doctype": "Journal Entry", "payment_name": journal_entry.name, "amount": paid_amount}] + [ + { + "payment_doctype": "Journal Entry", + "payment_name": journal_entry.name, + "amount": paid_amount, + } + ] ) - return reconcile_vouchers(bank_transaction.name, vouchers) + return reconcile_vouchers(bank_transaction_name, vouchers) @frappe.whitelist() @@ -220,12 +220,10 @@ def create_payment_entry_bts( as_dict=True, )[0] paid_amount = bank_transaction.unallocated_amount - payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay" + payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay" - company_account = frappe.get_cached_value( - "Bank Account", bank_transaction.bank_account, "account" - ) - company = frappe.get_cached_value("Account", company_account, "company") + company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account") + company = frappe.get_value("Account", company_account, "company") payment_entry_dict = { "company": company, "payment_type": payment_type, @@ -261,9 +259,15 @@ def create_payment_entry_bts( payment_entry.submit() vouchers = json.dumps( - [{"payment_doctype": "Payment Entry", "payment_name": payment_entry.name, "amount": paid_amount}] + [ + { + "payment_doctype": "Payment Entry", + "payment_name": payment_entry.name, + "amount": paid_amount, + } + ] ) - return reconcile_vouchers(bank_transaction.name, vouchers) + return reconcile_vouchers(bank_transaction_name, vouchers) @frappe.whitelist() @@ -345,59 +349,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers): # updated clear date of all the vouchers based on the bank transaction vouchers = json.loads(vouchers) transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) - company_account = frappe.get_cached_value("Bank Account", transaction.bank_account, "account") - - if transaction.unallocated_amount == 0: - frappe.throw(_("This bank transaction is already fully reconciled")) - total_amount = 0 - for voucher in vouchers: - voucher["payment_entry"] = frappe.get_doc(voucher["payment_doctype"], voucher["payment_name"]) - total_amount += get_paid_amount( - frappe._dict( - { - "payment_document": voucher["payment_doctype"], - "payment_entry": voucher["payment_name"], - } - ), - transaction.currency, - company_account, - ) - - if total_amount > transaction.unallocated_amount: - frappe.throw( - _( - "The sum total of amounts of all selected vouchers should be less than the unallocated amount of the bank transaction" - ) - ) - account = frappe.get_cached_value("Bank Account", transaction.bank_account, "account") - - for voucher in vouchers: - gl_entry = frappe.db.get_value( - "GL Entry", - dict( - account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"] - ), - ["credit_in_account_currency as credit", "debit_in_account_currency as debit"], - as_dict=1, - ) - gl_amount, transaction_amount = ( - (gl_entry.credit, transaction.deposit) - if gl_entry.credit > 0 - else (gl_entry.debit, transaction.withdrawal) - ) - allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount - - transaction.append( - "payment_entries", - { - "payment_document": voucher["payment_entry"].doctype, - "payment_entry": voucher["payment_entry"].name, - "allocated_amount": allocated_amount, - }, - ) - - transaction.save() - transaction.update_allocations() + transaction.add_payment_entries(vouchers) return frappe.get_doc("Bank Transaction", bank_transaction_name) @@ -416,9 +368,9 @@ def get_linked_payments( bank_account = frappe.db.get_values( "Bank Account", transaction.bank_account, ["account", "company"], as_dict=True )[0] - (account, company) = (bank_account.account, bank_account.company) + (gl_account, company) = (bank_account.account, bank_account.company) matching = check_matching( - account, + gl_account, company, transaction, document_types, @@ -428,7 +380,27 @@ def get_linked_payments( from_reference_date, to_reference_date, ) - return matching + return subtract_allocations(gl_account, matching) + + +def subtract_allocations(gl_account, vouchers): + "Look up & subtract any existing Bank Transaction allocations" + copied = [] + for voucher in vouchers: + rows = get_total_allocated_amount(voucher[1], voucher[2]) + amount = None + for row in rows: + if row["gl_account"] == gl_account: + amount = row["total"] + break + + if amount: + l = list(voucher) + l[3] -= amount + copied.append(tuple(l)) + else: + copied.append(voucher) + return copied def check_matching( @@ -442,6 +414,7 @@ def check_matching( from_reference_date, to_reference_date, ): + exact_match = True if "exact_match" in document_types else False # combine all types of vouchers subquery = get_queries( bank_account, @@ -453,10 +426,11 @@ def check_matching( filter_by_reference_date, from_reference_date, to_reference_date, + exact_match, ) filters = { "amount": transaction.unallocated_amount, - "payment_type": "Receive" if transaction.deposit > 0 else "Pay", + "payment_type": "Receive" if transaction.deposit > 0.0 else "Pay", "reference_no": transaction.reference_number, "party_type": transaction.party_type, "party": transaction.party, @@ -465,7 +439,9 @@ def check_matching( matching_vouchers = [] - matching_vouchers.extend(get_loan_vouchers(bank_account, transaction, document_types, filters)) + matching_vouchers.extend( + get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match) + ) for query in subquery: matching_vouchers.extend( @@ -487,10 +463,10 @@ def get_queries( filter_by_reference_date, from_reference_date, to_reference_date, + exact_match, ): # get queries to get matching vouchers - amount_condition = "=" if "exact_match" in document_types else "<=" - account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from" + account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from" queries = [] # get matching queries from all the apps @@ -501,7 +477,7 @@ def get_queries( company, transaction, document_types, - amount_condition, + exact_match, account_from_to, from_date, to_date, @@ -520,7 +496,7 @@ def get_matching_queries( company, transaction, document_types, - amount_condition, + exact_match, account_from_to, from_date, to_date, @@ -530,8 +506,8 @@ def get_matching_queries( ): queries = [] if "payment_entry" in document_types: - pe_amount_matching = get_pe_matching_query( - amount_condition, + query = get_pe_matching_query( + exact_match, account_from_to, transaction, from_date, @@ -540,11 +516,11 @@ def get_matching_queries( from_reference_date, to_reference_date, ) - queries.extend([pe_amount_matching]) + queries.append(query) if "journal_entry" in document_types: - je_amount_matching = get_je_matching_query( - amount_condition, + query = get_je_matching_query( + exact_match, transaction, from_date, to_date, @@ -552,34 +528,70 @@ def get_matching_queries( from_reference_date, to_reference_date, ) - queries.extend([je_amount_matching]) + queries.append(query) - if transaction.deposit > 0 and "sales_invoice" in document_types: - si_amount_matching = get_si_matching_query(amount_condition) - queries.extend([si_amount_matching]) + if transaction.deposit > 0.0 and "sales_invoice" in document_types: + query = get_si_matching_query(exact_match) + queries.append(query) - if transaction.withdrawal > 0: + if transaction.withdrawal > 0.0: if "purchase_invoice" in document_types: - pi_amount_matching = get_pi_matching_query(amount_condition) - queries.extend([pi_amount_matching]) + query = get_pi_matching_query(exact_match) + queries.append(query) + + if "bank_transaction" in document_types: + query = get_bt_matching_query(exact_match, transaction) + queries.append(query) return queries -def get_loan_vouchers(bank_account, transaction, document_types, filters): +def get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match): vouchers = [] - amount_condition = True if "exact_match" in document_types else False - if transaction.withdrawal > 0 and "loan_disbursement" in document_types: - vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters)) + if transaction.withdrawal > 0.0 and "loan_disbursement" in document_types: + vouchers.extend(get_ld_matching_query(bank_account, exact_match, filters)) - if transaction.deposit > 0 and "loan_repayment" in document_types: - vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters)) + if transaction.deposit > 0.0 and "loan_repayment" in document_types: + vouchers.extend(get_lr_matching_query(bank_account, exact_match, filters)) return vouchers -def get_ld_matching_query(bank_account, amount_condition, filters): +def get_bt_matching_query(exact_match, transaction): + # get matching bank transaction query + # find bank transactions in the same bank account with opposite sign + # same bank account must have same company and currency + field = "deposit" if transaction.withdrawal > 0.0 else "withdrawal" + + return f""" + + SELECT + (CASE WHEN reference_number = %(reference_no)s THEN 1 ELSE 0 END + + CASE WHEN {field} = %(amount)s THEN 1 ELSE 0 END + + CASE WHEN ( party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END + + CASE WHEN unallocated_amount = %(amount)s THEN 1 ELSE 0 END + + 1) AS rank, + 'Bank Transaction' AS doctype, + name, + unallocated_amount AS paid_amount, + reference_number AS reference_no, + date AS reference_date, + party, + party_type, + date AS posting_date, + currency + FROM + `tabBank Transaction` + WHERE + status != 'Reconciled' + AND name != '{transaction.name}' + AND bank_account = '{transaction.bank_account}' + AND {field} {'= %(amount)s' if exact_match else '> 0.0'} + """ + + +def get_ld_matching_query(bank_account, exact_match, filters): loan_disbursement = frappe.qb.DocType("Loan Disbursement") matching_reference = loan_disbursement.reference_number == filters.get("reference_number") matching_party = loan_disbursement.applicant_type == filters.get( @@ -607,17 +619,17 @@ def get_ld_matching_query(bank_account, amount_condition, filters): .where(loan_disbursement.disbursement_account == bank_account) ) - if amount_condition: + if exact_match: query.where(loan_disbursement.disbursed_amount == filters.get("amount")) else: - query.where(loan_disbursement.disbursed_amount <= filters.get("amount")) + query.where(loan_disbursement.disbursed_amount > 0.0) vouchers = query.run(as_list=True) return vouchers -def get_lr_matching_query(bank_account, amount_condition, filters): +def get_lr_matching_query(bank_account, exact_match, filters): loan_repayment = frappe.qb.DocType("Loan Repayment") matching_reference = loan_repayment.reference_number == filters.get("reference_number") matching_party = loan_repayment.applicant_type == filters.get( @@ -648,10 +660,10 @@ def get_lr_matching_query(bank_account, amount_condition, filters): if frappe.db.has_column("Loan Repayment", "repay_from_salary"): query = query.where((loan_repayment.repay_from_salary == 0)) - if amount_condition: + if exact_match: query.where(loan_repayment.amount_paid == filters.get("amount")) else: - query.where(loan_repayment.amount_paid <= filters.get("amount")) + query.where(loan_repayment.amount_paid > 0.0) vouchers = query.run() @@ -659,7 +671,7 @@ def get_lr_matching_query(bank_account, amount_condition, filters): def get_pe_matching_query( - amount_condition, + exact_match, account_from_to, transaction, from_date, @@ -669,7 +681,7 @@ def get_pe_matching_query( to_reference_date, ): # get matching payment entries query - if transaction.deposit > 0: + if transaction.deposit > 0.0: currency_field = "paid_to_account_currency as currency" else: currency_field = "paid_from_account_currency as currency" @@ -684,7 +696,8 @@ def get_pe_matching_query( return f""" SELECT (CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END - + CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END + + CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END + + CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END + 1 ) AS rank, 'Payment Entry' as doctype, name, @@ -698,20 +711,19 @@ def get_pe_matching_query( FROM `tabPayment Entry` WHERE - paid_amount {amount_condition} %(amount)s - AND docstatus = 1 + docstatus = 1 AND payment_type IN (%(payment_type)s, 'Internal Transfer') AND ifnull(clearance_date, '') = "" AND {account_from_to} = %(bank_account)s + AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'} {filter_by_date} {filter_by_reference_no} order by{order_by} - """ def get_je_matching_query( - amount_condition, + exact_match, transaction, from_date, to_date, @@ -723,7 +735,7 @@ def get_je_matching_query( # We have mapping at the bank level # So one bank could have both types of bank accounts like asset and liability # So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type - cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit" + cr_or_dr = "credit" if transaction.withdrawal > 0.0 else "debit" filter_by_date = f"AND je.posting_date between '{from_date}' and '{to_date}'" order_by = " je.posting_date" filter_by_reference_no = "" @@ -735,26 +747,29 @@ def get_je_matching_query( return f""" SELECT (CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END + + CASE WHEN jea.{cr_or_dr}_in_account_currency = %(amount)s THEN 1 ELSE 0 END + 1) AS rank , - 'Journal Entry' as doctype, + 'Journal Entry' AS doctype, je.name, - jea.{cr_or_dr}_in_account_currency as paid_amount, - je.cheque_no as reference_no, - je.cheque_date as reference_date, - je.pay_to_recd_from as party, + jea.{cr_or_dr}_in_account_currency AS paid_amount, + je.cheque_no AS reference_no, + je.cheque_date AS reference_date, + je.pay_to_recd_from AS party, jea.party_type, je.posting_date, - jea.account_currency as currency + jea.account_currency AS currency FROM - `tabJournal Entry Account` as jea + `tabJournal Entry Account` AS jea JOIN - `tabJournal Entry` as je + `tabJournal Entry` AS je ON jea.parent = je.name WHERE - (je.clearance_date is null or je.clearance_date='0000-00-00') + je.docstatus = 1 + AND je.voucher_type NOT IN ('Opening Entry') + AND (je.clearance_date IS NULL OR je.clearance_date='0000-00-00') AND jea.account = %(bank_account)s - AND jea.{cr_or_dr}_in_account_currency {amount_condition} %(amount)s + AND jea.{cr_or_dr}_in_account_currency {'= %(amount)s' if exact_match else '> 0.0'} AND je.docstatus = 1 {filter_by_date} {filter_by_reference_no} @@ -762,11 +777,12 @@ def get_je_matching_query( """ -def get_si_matching_query(amount_condition): - # get matchin sales invoice query +def get_si_matching_query(exact_match): + # get matching sales invoice query return f""" SELECT - ( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END + ( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END + + CASE WHEN sip.amount = %(amount)s THEN 1 ELSE 0 END + 1 ) AS rank, 'Sales Invoice' as doctype, si.name, @@ -784,18 +800,20 @@ def get_si_matching_query(amount_condition): `tabSales Invoice` as si ON sip.parent = si.name - WHERE (sip.clearance_date is null or sip.clearance_date='0000-00-00') + WHERE + si.docstatus = 1 + AND (sip.clearance_date is null or sip.clearance_date='0000-00-00') AND sip.account = %(bank_account)s - AND sip.amount {amount_condition} %(amount)s - AND si.docstatus = 1 + AND sip.amount {'= %(amount)s' if exact_match else '> 0.0'} """ -def get_pi_matching_query(amount_condition): - # get matching purchase invoice query +def get_pi_matching_query(exact_match): + # get matching purchase invoice query when they are also used as payment entries (is_paid) return f""" SELECT ( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END + + CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END + 1 ) AS rank, 'Purchase Invoice' as doctype, name, @@ -809,9 +827,9 @@ def get_pi_matching_query(amount_condition): FROM `tabPurchase Invoice` WHERE - paid_amount {amount_condition} %(amount)s - AND docstatus = 1 + docstatus = 1 AND is_paid = 1 AND ifnull(clearance_date, '') = "" - AND cash_bank_account = %(bank_account)s + AND cash_bank_account = %(bank_account)s + AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'} """ diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.js b/erpnext/accounts/doctype/bank_transaction/bank_transaction.js index 6f2900a6808..e548b4c7e9a 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.js +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.js @@ -12,8 +12,13 @@ frappe.ui.form.on("Bank Transaction", { }; }); }, - - bank_account: function(frm) { + refresh(frm) { + frm.add_custom_button(__('Unreconcile Transaction'), () => { + frm.call('remove_payment_entries') + .then( () => frm.refresh() ); + }); + }, + bank_account: function (frm) { set_bank_statement_filter(frm); }, @@ -34,6 +39,7 @@ frappe.ui.form.on("Bank Transaction", { "Journal Entry", "Sales Invoice", "Purchase Invoice", + "Bank Transaction", ]; } }); @@ -49,7 +55,7 @@ const update_clearance_date = (frm, cdt, cdn) => { frappe .xcall( "erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment", - { doctype: cdt, docname: cdn } + { doctype: cdt, docname: cdn, bt_name: frm.doc.name } ) .then((e) => { if (e == "success") { diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json index 2bdaa1049b7..768d2f0fa45 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json @@ -20,9 +20,11 @@ "currency", "section_break_10", "description", - "section_break_14", "reference_number", + "column_break_10", "transaction_id", + "transaction_type", + "section_break_14", "payment_entries", "section_break_18", "allocated_amount", @@ -190,11 +192,21 @@ "label": "Withdrawal", "oldfieldname": "credit", "options": "currency" + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, + { + "fieldname": "transaction_type", + "fieldtype": "Data", + "label": "Transaction Type", + "length": 50 } ], "is_submittable": 1, "links": [], - "modified": "2022-03-21 19:05:04.208222", + "modified": "2022-05-29 18:36:50.475964", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Transaction", @@ -248,4 +260,4 @@ "states": [], "title_field": "bank_account", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 9b36c93a0f3..15162376c15 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -1,9 +1,6 @@ # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - -from functools import reduce - import frappe from frappe.utils import flt @@ -18,72 +15,137 @@ class BankTransaction(StatusUpdater): self.clear_linked_payment_entries() self.set_status() + _saving_flag = False + + # nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting def on_update_after_submit(self): - self.update_allocations() - self.clear_linked_payment_entries() - self.set_status(update=True) + "Run on save(). Avoid recursion caused by multiple saves" + if not self._saving_flag: + self._saving_flag = True + self.clear_linked_payment_entries() + self.update_allocations() + self._saving_flag = False def on_cancel(self): self.clear_linked_payment_entries(for_cancel=True) self.set_status(update=True) def update_allocations(self): + "The doctype does not allow modifications after submission, so write to the db direct" if self.payment_entries: - allocated_amount = reduce( - lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries] - ) + allocated_amount = sum(p.allocated_amount for p in self.payment_entries) else: - allocated_amount = 0 + allocated_amount = 0.0 - if allocated_amount: - frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount)) - frappe.db.set_value( - self.doctype, - self.name, - "unallocated_amount", - abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount), - ) + amount = abs(flt(self.withdrawal) - flt(self.deposit)) + self.db_set("allocated_amount", flt(allocated_amount)) + self.db_set("unallocated_amount", amount - flt(allocated_amount)) + self.reload() + self.set_status(update=True) - else: - frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0) - frappe.db.set_value( - self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit)) - ) + def add_payment_entries(self, vouchers): + "Add the vouchers with zero allocation. Save() will perform the allocations and clearance" + if 0.0 >= self.unallocated_amount: + frappe.throw(frappe._(f"Bank Transaction {self.name} is already fully reconciled")) - amount = self.deposit or self.withdrawal - if amount == self.allocated_amount: - frappe.db.set_value(self.doctype, self.name, "status", "Reconciled") + added = False + for voucher in vouchers: + # Can't add same voucher twice + found = False + for pe in self.payment_entries: + if ( + pe.payment_document == voucher["payment_doctype"] + and pe.payment_entry == voucher["payment_name"] + ): + found = True + + if not found: + pe = { + "payment_document": voucher["payment_doctype"], + "payment_entry": voucher["payment_name"], + "allocated_amount": 0.0, # Temporary + } + child = self.append("payment_entries", pe) + added = True + + # runs on_update_after_submit + if added: + self.save() + + def allocate_payment_entries(self): + """Refactored from bank reconciliation tool. + Non-zero allocations must be amended/cleared manually + Get the bank transaction amount (b) and remove as we allocate + For each payment_entry if allocated_amount == 0: + - get the amount already allocated against all transactions (t), need latest date + - get the voucher amount (from gl) (v) + - allocate (a = v - t) + - a = 0: should already be cleared, so clear & remove payment_entry + - 0 < a <= u: allocate a & clear + - 0 < a, a > u: allocate u + - 0 > a: Error: already over-allocated + - clear means: set the latest transaction date as clearance date + """ + gl_bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account") + remaining_amount = self.unallocated_amount + for payment_entry in self.payment_entries: + if payment_entry.allocated_amount == 0.0: + unallocated_amount, should_clear, latest_transaction = get_clearance_details( + self, payment_entry + ) + + if 0.0 == unallocated_amount: + if should_clear: + latest_transaction.clear_linked_payment_entry(payment_entry) + self.db_delete_payment_entry(payment_entry) + + elif remaining_amount <= 0.0: + self.db_delete_payment_entry(payment_entry) + + elif 0.0 < unallocated_amount and unallocated_amount <= remaining_amount: + payment_entry.db_set("allocated_amount", unallocated_amount) + remaining_amount -= unallocated_amount + if should_clear: + latest_transaction.clear_linked_payment_entry(payment_entry) + + elif 0.0 < unallocated_amount and unallocated_amount > remaining_amount: + payment_entry.db_set("allocated_amount", remaining_amount) + remaining_amount = 0.0 + + elif 0.0 > unallocated_amount: + self.db_delete_payment_entry(payment_entry) + frappe.throw( + frappe._(f"Voucher {payment_entry.payment_entry} is over-allocated by {unallocated_amount}") + ) self.reload() - def clear_linked_payment_entries(self, for_cancel=False): + def db_delete_payment_entry(self, payment_entry): + frappe.db.delete("Bank Transaction Payments", {"name": payment_entry.name}) + + @frappe.whitelist() + def remove_payment_entries(self): for payment_entry in self.payment_entries: - if payment_entry.payment_document == "Sales Invoice": - self.clear_sales_invoice(payment_entry, for_cancel=for_cancel) - elif payment_entry.payment_document in get_doctypes_for_bank_reconciliation(): - self.clear_simple_entry(payment_entry, for_cancel=for_cancel) + self.remove_payment_entry(payment_entry) + # runs on_update_after_submit + self.save() - def clear_simple_entry(self, payment_entry, for_cancel=False): - if payment_entry.payment_document == "Payment Entry": - if ( - frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type") - == "Internal Transfer" - ): - if len(get_reconciled_bank_transactions(payment_entry)) < 2: - return + def remove_payment_entry(self, payment_entry): + "Clear payment entry and clearance" + self.clear_linked_payment_entry(payment_entry, for_cancel=True) + self.remove(payment_entry) - clearance_date = self.date if not for_cancel else None - frappe.db.set_value( - payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", clearance_date - ) + def clear_linked_payment_entries(self, for_cancel=False): + if for_cancel: + for payment_entry in self.payment_entries: + self.clear_linked_payment_entry(payment_entry, for_cancel) + else: + self.allocate_payment_entries() - def clear_sales_invoice(self, payment_entry, for_cancel=False): - clearance_date = self.date if not for_cancel else None - frappe.db.set_value( - "Sales Invoice Payment", - dict(parenttype=payment_entry.payment_document, parent=payment_entry.payment_entry), - "clearance_date", - clearance_date, + def clear_linked_payment_entry(self, payment_entry, for_cancel=False): + clearance_date = None if for_cancel else self.date + set_voucher_clearance( + payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self ) @@ -93,38 +155,112 @@ def get_doctypes_for_bank_reconciliation(): return frappe.get_hooks("bank_reconciliation_doctypes") -def get_reconciled_bank_transactions(payment_entry): - reconciled_bank_transactions = frappe.get_all( - "Bank Transaction Payments", - filters={"payment_entry": payment_entry.payment_entry}, - fields=["parent"], +def get_clearance_details(transaction, payment_entry): + """ + There should only be one bank gle for a voucher. + Could be none for a Bank Transaction. + But if a JE, could affect two banks. + Should only clear the voucher if all bank gles are allocated. + """ + gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account") + gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry) + bt_allocations = get_total_allocated_amount( + payment_entry.payment_document, payment_entry.payment_entry ) - return reconciled_bank_transactions + unallocated_amount = min( + transaction.unallocated_amount, + get_paid_amount(payment_entry, transaction.currency, gl_bank_account), + ) + unmatched_gles = len(gles) + latest_transaction = transaction + for gle in gles: + if gle["gl_account"] == gl_bank_account: + if gle["amount"] <= 0.0: + frappe.throw( + frappe._(f"Voucher {payment_entry.payment_entry} value is broken: {gle['amount']}") + ) + + unmatched_gles -= 1 + unallocated_amount = gle["amount"] + for a in bt_allocations: + if a["gl_account"] == gle["gl_account"]: + unallocated_amount = gle["amount"] - a["total"] + if frappe.utils.getdate(transaction.date) < a["latest_date"]: + latest_transaction = frappe.get_doc("Bank Transaction", a["latest_name"]) + else: + # Must be a Journal Entry affecting more than one bank + for a in bt_allocations: + if a["gl_account"] == gle["gl_account"] and a["total"] == gle["amount"]: + unmatched_gles -= 1 + + return unallocated_amount, unmatched_gles == 0, latest_transaction -def get_total_allocated_amount(payment_entry): - return frappe.db.sql( +def get_related_bank_gl_entries(doctype, docname): + # nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql + result = frappe.db.sql( """ SELECT - SUM(btp.allocated_amount) as allocated_amount, - bt.name + ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount, + gle.account AS gl_account FROM - `tabBank Transaction Payments` as btp + `tabGL Entry` gle LEFT JOIN - `tabBank Transaction` bt ON bt.name=btp.parent + `tabAccount` ac ON ac.name=gle.account WHERE - btp.payment_document = %s - AND - btp.payment_entry = %s - AND - bt.docstatus = 1""", - (payment_entry.payment_document, payment_entry.payment_entry), + ac.account_type = 'Bank' + AND gle.voucher_type = %(doctype)s + AND gle.voucher_no = %(docname)s + AND is_cancelled = 0 + """, + dict(doctype=doctype, docname=docname), as_dict=True, ) + return result -def get_paid_amount(payment_entry, currency, bank_account): +def get_total_allocated_amount(doctype, docname): + """ + Gets the sum of allocations for a voucher on each bank GL account + along with the latest bank transaction name & date + NOTE: query may also include just saved vouchers/payments but with zero allocated_amount + """ + # nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql + result = frappe.db.sql( + """ + SELECT total, latest_name, latest_date, gl_account FROM ( + SELECT + ROW_NUMBER() OVER w AS rownum, + SUM(btp.allocated_amount) OVER(PARTITION BY ba.account) AS total, + FIRST_VALUE(bt.name) OVER w AS latest_name, + FIRST_VALUE(bt.date) OVER w AS latest_date, + ba.account AS gl_account + FROM + `tabBank Transaction Payments` btp + LEFT JOIN `tabBank Transaction` bt ON bt.name=btp.parent + LEFT JOIN `tabBank Account` ba ON ba.name=bt.bank_account + WHERE + btp.payment_document = %(doctype)s + AND btp.payment_entry = %(docname)s + AND bt.docstatus = 1 + WINDOW w AS (PARTITION BY ba.account ORDER BY bt.date desc) + ) temp + WHERE + rownum = 1 + """, + dict(doctype=doctype, docname=docname), + as_dict=True, + ) + for row in result: + # Why is this *sometimes* a byte string? + if isinstance(row["latest_name"], bytes): + row["latest_name"] = row["latest_name"].decode() + row["latest_date"] = frappe.utils.getdate(row["latest_date"]) + return result + + +def get_paid_amount(payment_entry, currency, gl_bank_account): if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]: paid_amount_field = "paid_amount" @@ -147,7 +283,7 @@ def get_paid_amount(payment_entry, currency, bank_account): elif payment_entry.payment_document == "Journal Entry": return frappe.db.get_value( "Journal Entry Account", - {"parent": payment_entry.payment_entry, "account": bank_account}, + {"parent": payment_entry.payment_entry, "account": gl_bank_account}, "sum(credit_in_account_currency)", ) @@ -166,6 +302,12 @@ def get_paid_amount(payment_entry, currency, bank_account): payment_entry.payment_document, payment_entry.payment_entry, "amount_paid" ) + elif payment_entry.payment_document == "Bank Transaction": + dep, wth = frappe.db.get_value( + "Bank Transaction", payment_entry.payment_entry, ("deposit", "withdrawal") + ) + return abs(flt(wth) - flt(dep)) + else: frappe.throw( "Please reconcile {0}: {1} manually".format( @@ -174,18 +316,55 @@ def get_paid_amount(payment_entry, currency, bank_account): ) -@frappe.whitelist() -def unclear_reference_payment(doctype, docname): - if frappe.db.exists(doctype, docname): - doc = frappe.get_doc(doctype, docname) - if doctype == "Sales Invoice": - frappe.db.set_value( - "Sales Invoice Payment", - dict(parenttype=doc.payment_document, parent=doc.payment_entry), - "clearance_date", - None, - ) - else: - frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None) +def set_voucher_clearance(doctype, docname, clearance_date, self): + if doctype in [ + "Payment Entry", + "Journal Entry", + "Purchase Invoice", + "Expense Claim", + "Loan Repayment", + "Loan Disbursement", + ]: + if ( + doctype == "Payment Entry" + and frappe.db.get_value("Payment Entry", docname, "payment_type") == "Internal Transfer" + and len(get_reconciled_bank_transactions(doctype, docname)) < 2 + ): + return + frappe.db.set_value(doctype, docname, "clearance_date", clearance_date) - return doc.payment_entry + elif doctype == "Sales Invoice": + frappe.db.set_value( + "Sales Invoice Payment", + dict(parenttype=doctype, parent=docname), + "clearance_date", + clearance_date, + ) + + elif doctype == "Bank Transaction": + # For when a second bank transaction has fixed another, e.g. refund + bt = frappe.get_doc(doctype, docname) + if clearance_date: + vouchers = [{"payment_doctype": "Bank Transaction", "payment_name": self.name}] + bt.add_payment_entries(vouchers) + else: + for pe in bt.payment_entries: + if pe.payment_document == self.doctype and pe.payment_entry == self.name: + bt.remove(pe) + bt.save() + break + + +def get_reconciled_bank_transactions(doctype, docname): + return frappe.get_all( + "Bank Transaction Payments", + filters={"payment_document": doctype, "payment_entry": docname}, + pluck="parent", + ) + + +@frappe.whitelist() +def unclear_reference_payment(doctype, docname, bt_name): + bt = frappe.get_doc("Bank Transaction", bt_name) + set_voucher_clearance(doctype, docname, None, bt) + return docname diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 21f27aedc51..089f20b467d 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry"); frappe.ui.form.on("Journal Entry", { setup: function(frm) { frm.add_fetch("bank_account", "account", "account"); - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry']; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger"]; }, refresh: function(frm) { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 5b0322af2d2..db399b7bade 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -89,7 +89,13 @@ class JournalEntry(AccountsController): from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries unlink_ref_doc_from_payment_entries(self) - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") + self.ignore_linked_doctypes = ( + "GL Entry", + "Stock Ledger Entry", + "Payment Ledger Entry", + "Repost Payment Ledger", + "Repost Payment Ledger Items", + ) self.make_gl_entries(1) self.update_advance_paid() self.unlink_advance_entry_reference() @@ -238,21 +244,16 @@ class JournalEntry(AccountsController): ): processed_assets.append(d.reference_name) - asset = frappe.db.get_value( - "Asset", d.reference_name, ["calculate_depreciation", "value_after_depreciation"], as_dict=1 - ) + asset = frappe.get_doc("Asset", d.reference_name) if asset.calculate_depreciation: continue depr_value = d.debit or d.credit - frappe.db.set_value( - "Asset", - d.reference_name, - "value_after_depreciation", - asset.value_after_depreciation - depr_value, - ) + asset.db_set("value_after_depreciation", asset.value_after_depreciation - depr_value) + + asset.set_status() def update_inter_company_jv(self): if ( @@ -348,12 +349,9 @@ class JournalEntry(AccountsController): else: depr_value = d.debit or d.credit - frappe.db.set_value( - "Asset", - d.reference_name, - "value_after_depreciation", - asset.value_after_depreciation + depr_value, - ) + asset.db_set("value_after_depreciation", asset.value_after_depreciation + depr_value) + + asset.set_status() def unlink_inter_company_jv(self): if ( diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 6039bdfe95f..91374ae217b 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges"; frappe.ui.form.on('Payment Entry', { onload: function(frm) { - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice']; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Repost Payment Ledger"]; if(frm.doc.__islocal) { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 4a7a57b6275..3927ecae43d 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -239,7 +239,7 @@ "depends_on": "paid_from", "fieldname": "paid_from_account_currency", "fieldtype": "Link", - "label": "Account Currency", + "label": "Account Currency (From)", "options": "Currency", "print_hide": 1, "read_only": 1, @@ -249,7 +249,7 @@ "depends_on": "paid_from", "fieldname": "paid_from_account_balance", "fieldtype": "Currency", - "label": "Account Balance", + "label": "Account Balance (From)", "options": "paid_from_account_currency", "print_hide": 1, "read_only": 1 @@ -272,7 +272,7 @@ "depends_on": "paid_to", "fieldname": "paid_to_account_currency", "fieldtype": "Link", - "label": "Account Currency", + "label": "Account Currency (To)", "options": "Currency", "print_hide": 1, "read_only": 1, @@ -282,7 +282,7 @@ "depends_on": "paid_to", "fieldname": "paid_to_account_balance", "fieldtype": "Currency", - "label": "Account Balance", + "label": "Account Balance (To)", "options": "paid_to_account_currency", "print_hide": 1, "read_only": 1 @@ -304,7 +304,7 @@ { "fieldname": "source_exchange_rate", "fieldtype": "Float", - "label": "Exchange Rate", + "label": "Source Exchange Rate", "precision": "9", "print_hide": 1, "reqd": 1 @@ -334,7 +334,7 @@ { "fieldname": "target_exchange_rate", "fieldtype": "Float", - "label": "Exchange Rate", + "label": "Target Exchange Rate", "precision": "9", "print_hide": 1, "reqd": 1 @@ -633,14 +633,14 @@ "depends_on": "eval:doc.party_type == 'Supplier'", "fieldname": "purchase_taxes_and_charges_template", "fieldtype": "Link", - "label": "Taxes and Charges Template", + "label": "Purchase Taxes and Charges Template", "options": "Purchase Taxes and Charges Template" }, { "depends_on": "eval: doc.party_type == 'Customer'", "fieldname": "sales_taxes_and_charges_template", "fieldtype": "Link", - "label": "Taxes and Charges Template", + "label": "Sales Taxes and Charges Template", "options": "Sales Taxes and Charges Template" }, { @@ -733,7 +733,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-12-08 16:25:43.824051", + "modified": "2023-02-14 04:52:30.478523", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 1cccbd93886..cd5b6d5ce2b 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -92,7 +92,13 @@ class PaymentEntry(AccountsController): self.set_status() def on_cancel(self): - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") + self.ignore_linked_doctypes = ( + "GL Entry", + "Stock Ledger Entry", + "Payment Ledger Entry", + "Repost Payment Ledger", + "Repost Payment Ledger Items", + ) self.make_gl_entries(cancel=1) self.update_outstanding_amounts() self.update_advance_paid() diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 675a3287fa4..e3d9c26b2d1 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -368,6 +368,7 @@ class PaymentReconciliation(Document): "exchange_rate": 1, "cost_center": erpnext.get_default_cost_center(self.company), reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount), + reverse_dr_or_cr: flt(row.difference_amount), } ) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 52eb29b3bbd..2f43914c453 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -495,7 +495,7 @@ def get_amount(ref_doc, payment_account=None): """get amount based on doctype""" dt = ref_doc.doctype if dt in ["Sales Order", "Purchase Order"]: - grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid) + grand_total = flt(ref_doc.rounded_total) - flt(ref_doc.advance_paid) elif dt in ["Sales Invoice", "Purchase Invoice"]: if ref_doc.party_account_currency == ref_doc.currency: diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py index 655c4ec0035..115b415eeda 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py @@ -21,8 +21,24 @@ class POSClosingEntry(StatusUpdater): if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open": frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry")) + self.validate_duplicate_pos_invoices() self.validate_pos_invoices() + def validate_duplicate_pos_invoices(self): + pos_occurences = {} + for idx, inv in enumerate(self.pos_transactions, 1): + pos_occurences.setdefault(inv.pos_invoice, []).append(idx) + + error_list = [] + for key, value in pos_occurences.items(): + if len(value) > 1: + error_list.append( + _("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value))) + ) + + if error_list: + frappe.throw(error_list, title=_("Duplicate POS Invoices found"), as_list=True) + def validate_pos_invoices(self): invalid_rows = [] for d in self.pos_transactions: diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 3a237a43a3f..b1e22087dbd 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -17,6 +17,22 @@ class POSInvoiceMergeLog(Document): def validate(self): self.validate_customer() self.validate_pos_invoice_status() + self.validate_duplicate_pos_invoices() + + def validate_duplicate_pos_invoices(self): + pos_occurences = {} + for idx, inv in enumerate(self.pos_invoices, 1): + pos_occurences.setdefault(inv.pos_invoice, []).append(idx) + + error_list = [] + for key, value in pos_occurences.items(): + if len(value) > 1: + error_list.append( + _("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value))) + ) + + if error_list: + frappe.throw(error_list, title=_("Duplicate POS Invoices found"), as_list=True) def validate_customer(self): if self.merge_invoices_based_on == "Customer Group": @@ -425,6 +441,8 @@ def create_merge_logs(invoice_by_customer, closing_entry=None): if closing_entry: closing_entry.set_status(update=True, status="Failed") + if type(error_message) == list: + error_message = frappe.json.dumps(error_message) closing_entry.db_set("error_message", error_message) raise diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index ce9ce647db0..a63039e0e3a 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -472,7 +472,7 @@ "description": "If rate is zero them item will be treated as \"Free Item\"", "fieldname": "free_item_rate", "fieldtype": "Currency", - "label": "Rate" + "label": "Free Item Rate" }, { "collapsible": 1, @@ -608,7 +608,7 @@ "icon": "fa fa-gift", "idx": 1, "links": [], - "modified": "2022-10-13 19:05:35.056304", + "modified": "2023-02-14 04:53:34.887358", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index a098e8d1db8..e2b4a1ad5be 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. super.onload(); // Ignore linked advances - this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice']; + this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger"]; if(!this.frm.doc.__islocal) { // show credit_to in print format diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 0e9f9761068..21addab240b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -5,6 +5,7 @@ import frappe from frappe import _, throw from frappe.model.mapper import get_mapped_doc +from frappe.query_builder.functions import Sum from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate import erpnext @@ -1416,6 +1417,8 @@ class PurchaseInvoice(BuyingController): "GL Entry", "Stock Ledger Entry", "Repost Item Valuation", + "Repost Payment Ledger", + "Repost Payment Ledger Items", "Payment Ledger Entry", "Tax Withheld Vouchers", ) @@ -1463,19 +1466,16 @@ class PurchaseInvoice(BuyingController): def update_billing_status_in_pr(self, update_modified=True): updated_pr = [] po_details = [] + + pr_details_billed_amt = self.get_pr_details_billed_amt() + for d in self.get("items"): if d.pr_detail: - billed_amt = frappe.db.sql( - """select sum(amount) from `tabPurchase Invoice Item` - where pr_detail=%s and docstatus=1""", - d.pr_detail, - ) - billed_amt = billed_amt and billed_amt[0][0] or 0 frappe.db.set_value( "Purchase Receipt Item", d.pr_detail, "billed_amt", - billed_amt, + flt(pr_details_billed_amt.get(d.pr_detail)), update_modified=update_modified, ) updated_pr.append(d.purchase_receipt) @@ -1491,6 +1491,24 @@ class PurchaseInvoice(BuyingController): pr_doc = frappe.get_doc("Purchase Receipt", pr) 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): self.due_date = None diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 7abf3f31d93..47e3f9b9354 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e super.onload(); this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', - 'POS Closing Entry', 'Journal Entry', 'Payment Entry']; + 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger"]; if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { // show debit_to in print format diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 31cf1206ce3..5cda276087d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -397,6 +397,8 @@ class SalesInvoice(SellingController): "GL Entry", "Stock Ledger Entry", "Repost Item Valuation", + "Repost Payment Ledger", + "Repost Payment Ledger Items", "Payment Ledger Entry", ) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 2c829b258b4..f0146ea70eb 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -278,7 +278,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers) if cint(tax_details.round_off_tax_amount): - tax_amount = round(tax_amount) + tax_amount = normal_round(tax_amount) return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount @@ -603,3 +603,20 @@ def is_valid_certificate( valid = True return valid + + +def normal_round(number): + """ + Rounds a number to the nearest integer. + :param number: The number to round. + """ + decimal_part = number - int(number) + + if decimal_part >= 0.5: + decimal_part = 1 + else: + decimal_part = 0 + + number = int(number) + decimal_part + + return number diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index e23265b5e79..fde4de8402f 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -395,6 +395,7 @@ def get_column_names(): class GrossProfitGenerator(object): def __init__(self, filters=None): + self.sle = {} self.data = [] self.average_buying_rate = {} self.filters = frappe._dict(filters) @@ -404,7 +405,6 @@ class GrossProfitGenerator(object): if filters.group_by == "Invoice": self.group_items_by_invoice() - self.load_stock_ledger_entries() self.load_product_bundle() self.load_non_stock_items() self.get_returned_invoice_items() @@ -633,7 +633,7 @@ class GrossProfitGenerator(object): return flt(row.qty) * item_rate else: - my_sle = self.sle.get((item_code, row.warehouse)) + my_sle = self.get_stock_ledger_entries(item_code, row.warehouse) if (row.update_stock or row.dn_detail) and my_sle: parenttype, parent = row.parenttype, row.parent if row.dn_detail: @@ -651,7 +651,7 @@ class GrossProfitGenerator(object): dn["item_row"], dn["warehouse"], ) - my_sle = self.sle.get((item_code, warehouse)) + my_sle = self.get_stock_ledger_entries(item_code, row.warehouse) return self.calculate_buying_amount_from_sle( row, my_sle, parenttype, parent, item_row, item_code ) @@ -667,15 +667,12 @@ class GrossProfitGenerator(object): def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code): from frappe.query_builder.functions import Sum - delivery_note = frappe.qb.DocType("Delivery Note") delivery_note_item = frappe.qb.DocType("Delivery Note Item") query = ( - frappe.qb.from_(delivery_note) - .inner_join(delivery_note_item) - .on(delivery_note.name == delivery_note_item.parent) + frappe.qb.from_(delivery_note_item) .select(Sum(delivery_note_item.incoming_rate * delivery_note_item.stock_qty)) - .where(delivery_note.docstatus == 1) + .where(delivery_note_item.docstatus == 1) .where(delivery_note_item.item_code == item_code) .where(delivery_note_item.against_sales_order == sales_order) .where(delivery_note_item.so_detail == so_detail) @@ -947,24 +944,36 @@ class GrossProfitGenerator(object): "Item", item_code, ["item_name", "description", "item_group", "brand"] ) - def load_stock_ledger_entries(self): - res = frappe.db.sql( - """select item_code, voucher_type, voucher_no, - voucher_detail_no, stock_value, warehouse, actual_qty as qty - from `tabStock Ledger Entry` - where company=%(company)s and is_cancelled = 0 - order by - item_code desc, warehouse desc, posting_date desc, - posting_time desc, creation desc""", - self.filters, - as_dict=True, - ) - self.sle = {} - for r in res: - if (r.item_code, r.warehouse) not in self.sle: - self.sle[(r.item_code, r.warehouse)] = [] + def get_stock_ledger_entries(self, item_code, warehouse): + if item_code and warehouse: + if (item_code, warehouse) not in self.sle: + sle = qb.DocType("Stock Ledger Entry") + res = ( + qb.from_(sle) + .select( + sle.item_code, + sle.voucher_type, + sle.voucher_no, + sle.voucher_detail_no, + sle.stock_value, + sle.warehouse, + sle.actual_qty.as_("qty"), + ) + .where( + (sle.company == self.filters.company) + & (sle.item_code == item_code) + & (sle.warehouse == warehouse) + & (sle.is_cancelled == 0) + ) + .orderby(sle.item_code) + .orderby(sle.warehouse, sle.posting_date, sle.posting_time, sle.creation, order=Order.desc) + .run(as_dict=True) + ) - self.sle[(r.item_code, r.warehouse)].append(r) + self.sle[(item_code, warehouse)] = res + + return self.sle[(item_code, warehouse)] + return [] def load_product_bundle(self): self.product_bundles = {} diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index a61e8de7481..49513851361 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -258,7 +258,7 @@ frappe.ui.form.on('Asset', { $.each(depr_entries || [], function(i, v) { x_intervals.push(frappe.format(v.posting_date, { fieldtype: 'Date' })); let last_asset_value = asset_values[asset_values.length - 1] - asset_values.push(last_asset_value - v.value); + asset_values.push(flt(last_asset_value - v.value, precision('gross_purchase_amount'))); }); } diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index e00f3a5834f..e1d58a0264c 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -413,11 +413,14 @@ class Asset(AccountsController): if self.journal_entry_for_scrap: status = "Scrapped" - elif self.finance_books: - idx = self.get_default_finance_book_idx() or 0 + else: + expected_value_after_useful_life = 0 + value_after_depreciation = self.value_after_depreciation - expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life - value_after_depreciation = self.finance_books[idx].value_after_depreciation + if self.calculate_depreciation: + idx = self.get_default_finance_book_idx() or 0 + expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life + value_after_depreciation = self.finance_books[idx].value_after_depreciation if flt(value_after_depreciation) <= expected_value_after_useful_life: status = "Fully Depreciated" @@ -463,6 +466,7 @@ class Asset(AccountsController): .where(gle.debit != 0) .where(gle.is_cancelled == 0) .orderby(gle.posting_date) + .orderby(gle.creation) ).run(as_dict=True) return records diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index e7a25321b82..fb6e174fbae 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -168,7 +168,7 @@ def make_depreciation_entry(asset_depr_schedule_name, date=None): row.value_after_depreciation -= d.depreciation_amount row.db_update() - frappe.db.set_value("Asset", asset_name, "depr_entry_posting_status", "Successful") + asset.db_set("depr_entry_posting_status", "Successful") asset.set_status() diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 9a05a74ef9d..a7172a72c6f 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -91,6 +91,9 @@ class AssetRepair(AccountsController): make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes) self.asset_doc.save() + def after_delete(self): + frappe.get_doc("Asset", self.asset).set_status() + def check_repair_status(self): if self.repair_status == "Pending": frappe.throw(_("Please update Repair Status.")) diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index 51a6a86e9f6..59d43b1ea64 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -151,6 +151,7 @@ def prepare_chart_data(data, filters): filters.filter_based_on, "Monthly", company=filters.company, + ignore_fiscal_year=True, ) for d in period_list: diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 34417f7ac3a..652dcf0d43c 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -21,6 +21,7 @@ "allow_multiple_items", "bill_for_rejected_quantity_in_purchase_invoice", "disable_last_purchase_rate", + "show_pay_button", "subcontract", "backflush_raw_materials_of_subcontract_based_on", "column_break_11", @@ -140,6 +141,12 @@ "fieldname": "disable_last_purchase_rate", "fieldtype": "Check", "label": "Disable Last Purchase Rate" + }, + { + "default": "1", + "fieldname": "show_pay_button", + "fieldtype": "Check", + "label": "Show Pay Button in Purchase Order Portal" } ], "icon": "fa fa-cog", @@ -147,7 +154,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-01-09 17:08:28.828173", + "modified": "2023-02-15 14:42:10.200679", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index 66eafe9547a..1bf7f589e23 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -23,7 +23,6 @@ "default_bank_account", "column_break_10", "default_price_list", - "payment_terms", "internal_supplier_section", "is_internal_supplier", "represents_company", @@ -53,6 +52,7 @@ "supplier_primary_address", "primary_address", "accounting_tab", + "payment_terms", "accounts", "settings_tab", "allow_purchase_invoice_creation_without_purchase_order", @@ -457,11 +457,10 @@ "link_fieldname": "party" } ], - "modified": "2022-11-09 18:02:59.075203", + "modified": "2023-02-18 11:05:50.592270", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", - "name_case": "Title Case", "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 6fa44c93c22..3705fcf4990 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -204,6 +204,12 @@ class AccountsController(TransactionBase): validate_einvoice_fields(self) def on_trash(self): + # delete references in 'Repost Payment Ledger' + rpi = frappe.qb.DocType("Repost Payment Ledger Items") + frappe.qb.from_(rpi).delete().where( + (rpi.voucher_type == self.doctype) & (rpi.voucher_no == self.name) + ).run() + # delete sl and gl entries on deletion of transaction if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"): ple = frappe.qb.DocType("Payment Ledger Entry") diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index a9561fe2dac..cc80f6ca984 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -409,7 +409,14 @@ class SubcontractingController(StockController): if self.available_materials.get(key) and self.available_materials[key]["batch_no"]: new_rm_obj = None for batch_no, batch_qty in self.available_materials[key]["batch_no"].items(): - if batch_qty >= qty: + if batch_qty >= qty or ( + rm_obj.consumed_qty == 0 + and self.backflush_based_on == "BOM" + and len(self.available_materials[key]["batch_no"]) == 1 + ): + if rm_obj.consumed_qty == 0: + self.__set_consumed_qty(rm_obj, qty) + self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty) self.available_materials[key]["batch_no"][batch_no] -= qty return diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py index 38d69932f24..f44fad333cf 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py @@ -12,7 +12,7 @@ class PlaidConnector: def __init__(self, access_token=None): self.access_token = access_token self.settings = frappe.get_single("Plaid Settings") - self.products = ["auth", "transactions"] + self.products = ["transactions"] self.client_name = frappe.local.site self.client = plaid.Client( client_id=self.settings.plaid_client_id, diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js index 3740d049839..3ba6bb99873 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js @@ -47,7 +47,7 @@ erpnext.integrations.plaidLink = class plaidLink { } async init_config() { - this.product = ["auth", "transactions"]; + this.product = ["transactions"]; this.plaid_env = this.frm.doc.plaid_env; this.client_name = frappe.boot.sitename; this.token = await this.get_link_token(); diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index 62ea85fc5d2..f3aa6a37935 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -70,7 +70,8 @@ def add_bank_accounts(response, bank, company): except TypeError: pass - bank = json.loads(bank) + if isinstance(bank, str): + bank = json.loads(bank) result = [] default_gl_account = get_default_bank_cash_account(company, "Bank") @@ -177,16 +178,15 @@ def sync_transactions(bank, bank_account): ) result = [] - for transaction in reversed(transactions): - result += new_bank_transaction(transaction) + if transactions: + for transaction in reversed(transactions): + result += new_bank_transaction(transaction) if result: last_transaction_date = frappe.db.get_value("Bank Transaction", result.pop(), "date") frappe.logger().info( - "Plaid added {} new Bank Transactions from '{}' between {} and {}".format( - len(result), bank_account, start_date, end_date - ) + f"Plaid added {len(result)} new Bank Transactions from '{bank_account}' between {start_date} and {end_date}" ) frappe.db.set_value( @@ -230,19 +230,20 @@ def new_bank_transaction(transaction): bank_account = frappe.db.get_value("Bank Account", dict(integration_id=transaction["account_id"])) - if float(transaction["amount"]) >= 0: - debit = 0 - credit = float(transaction["amount"]) + amount = float(transaction["amount"]) + if amount >= 0.0: + deposit = 0.0 + withdrawal = amount else: - debit = abs(float(transaction["amount"])) - credit = 0 + deposit = abs(amount) + withdrawal = 0.0 status = "Pending" if transaction["pending"] == "True" else "Settled" tags = [] try: tags += transaction["category"] - tags += ["Plaid Cat. {}".format(transaction["category_id"])] + tags += [f'Plaid Cat. {transaction["category_id"]}'] except KeyError: pass @@ -254,11 +255,18 @@ def new_bank_transaction(transaction): "date": getdate(transaction["date"]), "status": status, "bank_account": bank_account, - "deposit": debit, - "withdrawal": credit, + "deposit": deposit, + "withdrawal": withdrawal, "currency": transaction["iso_currency_code"], "transaction_id": transaction["transaction_id"], - "reference_number": transaction["payment_meta"]["reference_number"], + "transaction_type": ( + transaction["transaction_code"] or transaction["payment_meta"]["payment_method"] + ), + "reference_number": ( + transaction["check_number"] + or transaction["payment_meta"]["reference_number"] + or transaction["name"] + ), "description": transaction["name"], } ) @@ -271,7 +279,7 @@ def new_bank_transaction(transaction): result.append(new_transaction.name) except Exception: - frappe.throw(title=_("Bank transaction creation error")) + frappe.throw(_("Bank transaction creation error")) return result @@ -300,3 +308,26 @@ def enqueue_synchronization(): def get_link_token_for_update(access_token): plaid = PlaidConnector(access_token) return plaid.get_link_token(update_mode=True) + + +def get_company(bank_account_name): + from frappe.defaults import get_user_default + + company_names = frappe.db.get_all("Company", pluck="name") + if len(company_names) == 1: + return company_names[0] + if frappe.db.exists("Bank Account", bank_account_name): + return frappe.db.get_value("Bank Account", bank_account_name, "company") + company_default = get_user_default("Company") + if company_default: + return company_default + frappe.throw(_("Could not detect the Company for updating Bank Accounts")) + + +@frappe.whitelist() +def update_bank_account_ids(response): + data = json.loads(response) + institution_name = data["institution"]["name"] + bank = frappe.get_doc("Bank", institution_name).as_dict() + bank_account_name = f"{data['account']['name']} - {institution_name}" + return add_bank_accounts(response, bank, get_company(bank_account_name)) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py index e8dc3e258f6..6d34a204cd2 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py @@ -125,6 +125,8 @@ class TestPlaidSettings(unittest.TestCase): "unofficial_currency_code": None, "name": "INTRST PYMNT", "transaction_type": "place", + "transaction_code": "direct debit", + "check_number": "3456789", "amount": -4.22, "location": { "city": None, diff --git a/erpnext/hooks.py b/erpnext/hooks.py index fd19d2585cc..fba886ca2c3 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -311,15 +311,10 @@ doc_events = { "on_submit": [ "erpnext.regional.create_transaction_log", "erpnext.regional.italy.utils.sales_invoice_on_submit", - "erpnext.regional.saudi_arabia.utils.create_qr_code", - ], - "on_cancel": [ - "erpnext.regional.italy.utils.sales_invoice_on_cancel", - "erpnext.regional.saudi_arabia.utils.delete_qr_code_file", ], + "on_cancel": ["erpnext.regional.italy.utils.sales_invoice_on_cancel"], "on_trash": "erpnext.regional.check_deletion_permission", }, - "POS Invoice": {"on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]}, "Purchase Invoice": { "validate": [ "erpnext.regional.united_arab_emirates.utils.update_grand_total_for_rcm", @@ -347,7 +342,6 @@ doc_events = { "Email Unsubscribe": { "after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient" }, - "Company": {"on_trash": ["erpnext.regional.saudi_arabia.utils.delete_vat_settings_for_company"]}, "Integration Request": { "validate": "erpnext.accounts.doctype.payment_request.payment_request.validate_payment" }, diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js index 7beecaceedf..e7f67caf249 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js @@ -25,8 +25,9 @@ frappe.query_reports["BOM Stock Report"] = { ], "formatter": function(value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); + if (column.id == "item") { - if (data["enough_parts_to_build"] > 0) { + if (data["in_stock_qty"] >= data["required_qty"]) { value = `${data['item']}`; } else { value = `${data['item']}`; diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 211f07445a1..2abd65b1b5b 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -250,18 +250,14 @@ erpnext.patches.v13_0.item_naming_series_not_mandatory erpnext.patches.v13_0.update_category_in_ltds_certificate erpnext.patches.v13_0.fetch_thumbnail_in_website_items erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit -erpnext.patches.v13_0.create_ksa_vat_custom_fields # 07-01-2022 erpnext.patches.v14_0.migrate_crm_settings -erpnext.patches.v13_0.rename_ksa_qr_field erpnext.patches.v13_0.wipe_serial_no_field_for_0_qty -erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 erpnext.patches.v13_0.agriculture_deprecation_warning erpnext.patches.v13_0.hospitality_deprecation_warning erpnext.patches.v13_0.update_asset_quantity_field erpnext.patches.v13_0.delete_bank_reconciliation_detail erpnext.patches.v13_0.enable_provisional_accounting erpnext.patches.v13_0.non_profit_deprecation_warning -erpnext.patches.v13_0.enable_ksa_vat_docs #1 erpnext.patches.v13_0.show_india_localisation_deprecation_warning erpnext.patches.v13_0.show_hr_payroll_deprecation_warning erpnext.patches.v13_0.reset_corrupt_defaults @@ -269,6 +265,8 @@ erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair erpnext.patches.v15_0.delete_taxjar_doctypes erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets erpnext.patches.v14_0.update_reference_due_date_in_journal_entry +erpnext.patches.v15_0.saudi_depreciation_warning +erpnext.patches.v15_0.delete_saudi_doctypes [post_model_sync] execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings') @@ -306,7 +304,6 @@ erpnext.patches.v13_0.set_per_billed_in_return_delivery_note execute:frappe.delete_doc("DocType", "Naming Series") erpnext.patches.v13_0.job_card_status_on_hold erpnext.patches.v14_0.copy_is_subcontracted_value_to_is_old_subcontracting_flow -erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.patches.v14_0.crm_ux_cleanup erpnext.patches.v14_0.migrate_existing_lead_notes_as_per_the_new_format erpnext.patches.v14_0.remove_india_localisation # 14-07-2022 @@ -315,7 +312,6 @@ erpnext.patches.v14_0.remove_hr_and_payroll_modules # 20-07-2022 erpnext.patches.v14_0.fix_crm_no_of_employees erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries -erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger erpnext.patches.v13_0.update_schedule_type_in_loans erpnext.patches.v13_0.drop_unused_sle_index_parts erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization @@ -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.set_pick_list_status 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 diff --git a/erpnext/patches/v13_0/create_ksa_vat_custom_fields.py b/erpnext/patches/v13_0/create_ksa_vat_custom_fields.py deleted file mode 100644 index 093463a12e9..00000000000 --- a/erpnext/patches/v13_0/create_ksa_vat_custom_fields.py +++ /dev/null @@ -1,11 +0,0 @@ -import frappe - -from erpnext.regional.saudi_arabia.setup import make_custom_fields - - -def execute(): - company = frappe.get_all("Company", filters={"country": "Saudi Arabia"}) - if not company: - return - - make_custom_fields() diff --git a/erpnext/patches/v13_0/delete_old_purchase_reports.py b/erpnext/patches/v13_0/delete_old_purchase_reports.py index 987f53f37c1..60621fbc9ca 100644 --- a/erpnext/patches/v13_0/delete_old_purchase_reports.py +++ b/erpnext/patches/v13_0/delete_old_purchase_reports.py @@ -17,10 +17,11 @@ def execute(): for report in reports_to_delete: if frappe.db.exists("Report", report): + delete_links_from_desktop_icons(report) delete_auto_email_reports(report) check_and_delete_linked_reports(report) - frappe.delete_doc("Report", report) + frappe.delete_doc("Report", report, force=True) def delete_auto_email_reports(report): @@ -28,3 +29,10 @@ def delete_auto_email_reports(report): auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"]) for auto_email_report in auto_email_reports: frappe.delete_doc("Auto Email Report", auto_email_report[0]) + + +def delete_links_from_desktop_icons(report): + """Check for one or multiple Desktop Icons and delete""" + desktop_icons = frappe.db.get_values("Desktop Icon", {"_report": report}, ["name"]) + for desktop_icon in desktop_icons: + frappe.delete_doc("Desktop Icon", desktop_icon[0], force=True) diff --git a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py deleted file mode 100644 index 84b6c37dd9d..00000000000 --- a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2020, Wahni Green Technologies and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe - -from erpnext.regional.saudi_arabia.setup import add_print_formats - - -def execute(): - company = frappe.get_all("Company", filters={"country": "Saudi Arabia"}) - if company: - add_print_formats() - return - - if frappe.db.exists("DocType", "Print Format"): - frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True) - frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True) - for d in ("KSA VAT Invoice", "KSA POS Invoice"): - frappe.db.set_value("Print Format", d, "disabled", 1) diff --git a/erpnext/patches/v13_0/enable_ksa_vat_docs.py b/erpnext/patches/v13_0/enable_ksa_vat_docs.py deleted file mode 100644 index 4adf4d71db7..00000000000 --- a/erpnext/patches/v13_0/enable_ksa_vat_docs.py +++ /dev/null @@ -1,12 +0,0 @@ -import frappe - -from erpnext.regional.saudi_arabia.setup import add_permissions, add_print_formats - - -def execute(): - company = frappe.get_all("Company", filters={"country": "Saudi Arabia"}) - if not company: - return - - add_print_formats() - add_permissions() diff --git a/erpnext/patches/v13_0/rename_ksa_qr_field.py b/erpnext/patches/v13_0/rename_ksa_qr_field.py deleted file mode 100644 index e4b91412ee1..00000000000 --- a/erpnext/patches/v13_0/rename_ksa_qr_field.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) 2020, Wahni Green Technologies and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe -from frappe.custom.doctype.custom_field.custom_field import create_custom_fields -from frappe.model.utils.rename_field import rename_field - - -def execute(): - company = frappe.get_all("Company", filters={"country": "Saudi Arabia"}) - if not company: - return - - if frappe.db.exists("DocType", "Sales Invoice"): - frappe.reload_doc("accounts", "doctype", "sales_invoice", force=True) - - # rename_field method assumes that the field already exists or the doc is synced - if not frappe.db.has_column("Sales Invoice", "ksa_einv_qr"): - create_custom_fields( - { - "Sales Invoice": [ - dict( - fieldname="ksa_einv_qr", - label="KSA E-Invoicing QR", - fieldtype="Attach Image", - read_only=1, - no_copy=1, - hidden=1, - ) - ] - } - ) - - if frappe.db.has_column("Sales Invoice", "qr_code"): - rename_field("Sales Invoice", "qr_code", "ksa_einv_qr") - frappe.delete_doc_if_exists("Custom Field", "Sales Invoice-qr_code") diff --git a/erpnext/patches/v15_0/delete_saudi_doctypes.py b/erpnext/patches/v15_0/delete_saudi_doctypes.py new file mode 100644 index 00000000000..371e3352908 --- /dev/null +++ b/erpnext/patches/v15_0/delete_saudi_doctypes.py @@ -0,0 +1,25 @@ +import click +import frappe + + +def execute(): + if "ksa" in frappe.get_installed_apps(): + return + + doctypes = ["KSA VAT Setting", "KSA VAT Purchase Account", "KSA VAT Sales Account"] + for doctype in doctypes: + frappe.delete_doc("DocType", doctype, ignore_missing=True) + + print_formats = ["KSA POS Invoice", "KSA VAT Invoice"] + for print_format in print_formats: + frappe.delete_doc("Print Format", print_format, ignore_missing=True, force=True) + + reports = ["KSA VAT"] + for report in reports: + frappe.delete_doc("Report", report, ignore_missing=True, force=True) + + click.secho( + "Region Saudi Arabia(KSA) is moved to a separate app" + "Please install the app to continue using the module: https://github.com/8848digital/KSA", + fg="yellow", + ) diff --git a/erpnext/patches/v15_0/saudi_depreciation_warning.py b/erpnext/patches/v15_0/saudi_depreciation_warning.py new file mode 100644 index 00000000000..6af8efe5ff0 --- /dev/null +++ b/erpnext/patches/v15_0/saudi_depreciation_warning.py @@ -0,0 +1,12 @@ +import click +import frappe + + +def execute(): + if "ksa" in frappe.get_installed_apps(): + return + click.secho( + "Region Saudi Arabia(KSA) is moved to a separate app\n" + "Please install the app to continue using the KSA Features: https://github.com/8848digital/KSA", + fg="yellow", + ) diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 37d98ad8ea1..ba7aa850825 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -408,7 +408,7 @@ "depends_on": "eval:(doc.frequency == \"Daily\" && doc.collect_progress == true)", "fieldname": "daily_time_to_send", "fieldtype": "Time", - "label": "Time to send" + "label": "Daily Time to send" }, { "depends_on": "eval:(doc.frequency == \"Weekly\" && doc.collect_progress == true)", @@ -421,7 +421,7 @@ "depends_on": "eval:(doc.frequency == \"Weekly\" && doc.collect_progress == true)", "fieldname": "weekly_time_to_send", "fieldtype": "Time", - "label": "Time to send" + "label": "Weekly Time to send" }, { "fieldname": "column_break_45", @@ -451,7 +451,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2022-06-23 16:45:06.108499", + "modified": "2023-02-14 04:54:25.819620", "modified_by": "Administrator", "module": "Projects", "name": "Project", @@ -497,4 +497,4 @@ "timeline_field": "customer", "title_field": "project_name", "track_seen": 1 -} +} \ No newline at end of file diff --git a/erpnext/projects/doctype/timesheet/timesheet.json b/erpnext/projects/doctype/timesheet/timesheet.json index 0cce129034e..468300661a0 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.json +++ b/erpnext/projects/doctype/timesheet/timesheet.json @@ -282,21 +282,21 @@ { "fieldname": "base_total_costing_amount", "fieldtype": "Currency", - "label": "Total Costing Amount", + "label": "Base Total Costing Amount", "print_hide": 1, "read_only": 1 }, { "fieldname": "base_total_billable_amount", "fieldtype": "Currency", - "label": "Total Billable Amount", + "label": "Base Total Billable Amount", "print_hide": 1, "read_only": 1 }, { "fieldname": "base_total_billed_amount", "fieldtype": "Currency", - "label": "Total Billed Amount", + "label": "Base Total Billed Amount", "print_hide": 1, "read_only": 1 }, @@ -311,10 +311,11 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2022-06-15 22:08:53.930200", + "modified": "2023-02-14 04:55:41.735991", "modified_by": "Administrator", "module": "Projects", "name": "Timesheet", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -388,5 +389,6 @@ ], "sort_field": "modified", "sort_order": "ASC", + "states": [], "title_field": "title" } \ No newline at end of file diff --git a/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js b/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js index f7c19a1b7ff..0cda93880fa 100644 --- a/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js +++ b/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js @@ -182,6 +182,9 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager { ); } else { this.transactions.splice(transaction_index, 1); + for (const [k, v] of Object.entries(this.transaction_dt_map)) { + if (v > transaction_index) this.transaction_dt_map[k] = v - 1; + } } this.datatable.refresh(this.transactions, this.columns); diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js index 911343d8b64..321b812de21 100644 --- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js +++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js @@ -20,7 +20,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { doctype: "Bank Transaction", filters: { name: this.bank_transaction_name }, fieldname: [ - "date as reference_date", + "date", "deposit", "withdrawal", "currency", @@ -33,6 +33,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { "party", "unallocated_amount", "allocated_amount", + "transaction_type", ], }, callback: (r) => { @@ -41,11 +42,23 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { r.message.payment_entry = 1; r.message.journal_entry = 1; this.dialog.set_values(r.message); + this.copy_data_to_voucher(); this.dialog.show(); } }, }); } + + copy_data_to_voucher() { + let copied = { + reference_number: this.bank_transaction.reference_number || this.bank_transaction.description, + posting_date: this.bank_transaction.date, + reference_date: this.bank_transaction.date, + mode_of_payment: this.bank_transaction.transaction_type, + }; + this.dialog.set_values(copied); + } + get_linked_vouchers(document_types) { frappe.call({ method: @@ -75,10 +88,9 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { row[1], row[2], reference_date, - row[8], format_currency(row[3], row[9]), - row[6], row[4], + row[6], ]); }); this.get_dt_columns(); @@ -104,7 +116,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { { name: __("Document Name"), editable: false, - width: 150, + width: 1, }, { name: __("Reference Date"), @@ -112,25 +124,19 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { width: 120, }, { - name: "Posting Date", - editable: false, - width: 120, - }, - { - name: __("Amount"), + name: __("Remaining"), editable: false, width: 100, }, - { - name: __("Party"), - editable: false, - width: 120, - }, - { name: __("Reference Number"), editable: false, - width: 140, + width: 200, + }, + { + name: __("Party"), + editable: false, + width: 100, }, ]; } @@ -224,6 +230,16 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { fieldname: "exact_match", onchange: () => this.update_options(), }, + { + fieldname: "column_break_5", + fieldtype: "Column Break", + }, + { + fieldtype: "Check", + label: "Bank Transaction", + fieldname: "bank_transaction", + onchange: () => this.update_options(), + }, { fieldtype: "Section Break", fieldname: "section_break_1", @@ -289,7 +305,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { fieldtype: "Column Break", }, { - default: "Journal Entry Type", + default: "Bank Entry", fieldname: "journal_entry_type", fieldtype: "Select", label: "Journal Entry Type", @@ -364,7 +380,12 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { fieldtype: "Section Break", fieldname: "details_section", label: "Transaction Details", - collapsible: 1, + }, + { + fieldname: "date", + fieldtype: "Date", + label: "Date", + read_only: 1, }, { fieldname: "deposit", @@ -381,14 +402,14 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { read_only: 1, }, { - fieldname: "description", - fieldtype: "Small Text", - label: "Description", + fieldname: "column_break_17", + fieldtype: "Column Break", read_only: 1, }, { - fieldname: "column_break_17", - fieldtype: "Column Break", + fieldname: "description", + fieldtype: "Small Text", + label: "Description", read_only: 1, }, { @@ -398,7 +419,6 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { options: "Currency", read_only: 1, }, - { fieldname: "unallocated_amount", fieldtype: "Currency", @@ -593,4 +613,4 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { } } -}; \ No newline at end of file +}; diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 51dcd64d9dd..58aa8d7da23 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -221,9 +221,9 @@ $.extend(erpnext.utils, { callback: function(r) { if (r.message && r.message.length) { r.message.forEach((dimension) => { - let found = filters.some(el => el.fieldname === dimension['fieldname']); + let existing_filter = filters.filter(el => el.fieldname === dimension['fieldname']); - if (!found) { + if (!existing_filter.length) { filters.splice(index, 0, { "fieldname": dimension["fieldname"], "label": __(dimension["doctype"]), @@ -232,6 +232,11 @@ $.extend(erpnext.utils, { return frappe.db.get_link_options(dimension["doctype"], txt); }, }); + } else { + existing_filter[0]['fieldtype'] = "MultiSelectList"; + existing_filter[0]['get_data'] = function(txt) { + return frappe.db.get_link_options(dimension["doctype"], txt); + } } }); } diff --git a/erpnext/regional/doctype/ksa_vat_purchase_account/__init__.py b/erpnext/regional/doctype/ksa_vat_purchase_account/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.json b/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.json deleted file mode 100644 index 89ba3e977af..00000000000 --- a/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "actions": [], - "creation": "2021-07-13 09:17:09.862163", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "title", - "item_tax_template", - "account" - ], - "fields": [ - { - "fieldname": "account", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Account", - "options": "Account", - "reqd": 1 - }, - { - "fieldname": "title", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Title", - "reqd": 1 - }, - { - "fieldname": "item_tax_template", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Item Tax Template", - "options": "Item Tax Template", - "reqd": 1 - } - ], - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2021-08-04 06:42:38.205597", - "modified_by": "Administrator", - "module": "Regional", - "name": "KSA VAT Purchase Account", - "owner": "Administrator", - "permissions": [], - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.py b/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.py deleted file mode 100644 index 3920bc546c1..00000000000 --- a/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2021, Havenir Solutions and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class KSAVATPurchaseAccount(Document): - pass diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/__init__.py b/erpnext/regional/doctype/ksa_vat_sales_account/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.js b/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.js deleted file mode 100644 index 72613f4064f..00000000000 --- a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2021, Havenir Solutions and contributors -// For license information, please see license.txt - -frappe.ui.form.on('KSA VAT Sales Account', { - // refresh: function(frm) { - - // } -}); diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.json b/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.json deleted file mode 100644 index df2747891dc..00000000000 --- a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "actions": [], - "creation": "2021-07-13 08:46:33.820968", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "title", - "item_tax_template", - "account" - ], - "fields": [ - { - "fieldname": "account", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Account", - "options": "Account", - "reqd": 1 - }, - { - "fieldname": "title", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Title", - "reqd": 1 - }, - { - "fieldname": "item_tax_template", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Item Tax Template", - "options": "Item Tax Template", - "reqd": 1 - } - ], - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2021-08-04 06:42:00.081407", - "modified_by": "Administrator", - "module": "Regional", - "name": "KSA VAT Sales Account", - "owner": "Administrator", - "permissions": [], - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.py b/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.py deleted file mode 100644 index 7c2689f530e..00000000000 --- a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2021, Havenir Solutions and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class KSAVATSalesAccount(Document): - pass diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/test_ksa_vat_sales_account.py b/erpnext/regional/doctype/ksa_vat_sales_account/test_ksa_vat_sales_account.py deleted file mode 100644 index 1d6a6a793dc..00000000000 --- a/erpnext/regional/doctype/ksa_vat_sales_account/test_ksa_vat_sales_account.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2021, Havenir Solutions and Contributors -# See license.txt - -# import frappe -import unittest - - -class TestKSAVATSalesAccount(unittest.TestCase): - pass diff --git a/erpnext/regional/doctype/ksa_vat_setting/__init__.py b/erpnext/regional/doctype/ksa_vat_setting/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.js b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.js deleted file mode 100644 index 00b62b9adfb..00000000000 --- a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2021, Havenir Solutions and contributors -// For license information, please see license.txt - -frappe.ui.form.on('KSA VAT Setting', { - onload: function () { - frappe.breadcrumbs.add('Accounts', 'KSA VAT Setting'); - } -}); diff --git a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.json b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.json deleted file mode 100644 index 33619467ed0..00000000000 --- a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "actions": [], - "autoname": "field:company", - "creation": "2021-07-13 08:49:01.100356", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "company", - "ksa_vat_sales_accounts", - "ksa_vat_purchase_accounts" - ], - "fields": [ - { - "fieldname": "company", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Company", - "options": "Company", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "ksa_vat_sales_accounts", - "fieldtype": "Table", - "label": "KSA VAT Sales Accounts", - "options": "KSA VAT Sales Account", - "reqd": 1 - }, - { - "fieldname": "ksa_vat_purchase_accounts", - "fieldtype": "Table", - "label": "KSA VAT Purchase Accounts", - "options": "KSA VAT Purchase Account", - "reqd": 1 - } - ], - "links": [], - "modified": "2021-08-26 04:29:06.499378", - "modified_by": "Administrator", - "module": "Regional", - "name": "KSA VAT Setting", - "owner": "Administrator", - "permissions": [], - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "company", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.py b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.py deleted file mode 100644 index bdae1161fd7..00000000000 --- a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2021, Havenir Solutions and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class KSAVATSetting(Document): - pass diff --git a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting_list.js b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting_list.js deleted file mode 100644 index 269cbec5fb4..00000000000 --- a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting_list.js +++ /dev/null @@ -1,5 +0,0 @@ -frappe.listview_settings['KSA VAT Setting'] = { - onload () { - frappe.breadcrumbs.add('Accounts'); - } -} \ No newline at end of file diff --git a/erpnext/regional/doctype/ksa_vat_setting/test_ksa_vat_setting.py b/erpnext/regional/doctype/ksa_vat_setting/test_ksa_vat_setting.py deleted file mode 100644 index 7207901fd43..00000000000 --- a/erpnext/regional/doctype/ksa_vat_setting/test_ksa_vat_setting.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2021, Havenir Solutions and Contributors -# See license.txt - -# import frappe -import unittest - - -class TestKSAVATSetting(unittest.TestCase): - pass diff --git a/erpnext/regional/print_format/ksa_pos_invoice/__init__.py b/erpnext/regional/print_format/ksa_pos_invoice/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json b/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json deleted file mode 100644 index c2a309231dd..00000000000 --- a/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "absolute_value": 0, - "align_labels_right": 0, - "creation": "2021-12-07 13:25:05.424827", - "css": "", - "custom_format": 1, - "default_print_language": "en", - "disabled": 1, - "doc_type": "POS Invoice", - "docstatus": 0, - "doctype": "Print Format", - "font_size": 0, - "html": "\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n

\n\t{{ doc.company }}
\n\t{{ doc.select_print_heading or _(\"Invoice\") }}
\n\t\n

\n

\n\t{{ _(\"Receipt No\") }}: {{ doc.name }}
\n\t{{ _(\"Cashier\") }}: {{ doc.owner }}
\n\t{{ _(\"Customer\") }}: {{ doc.customer_name }}
\n\t{{ _(\"Date\") }}: {{ doc.get_formatted(\"posting_date\") }}
\n\t{{ _(\"Time\") }}: {{ doc.get_formatted(\"posting_time\") }}
\n

\n\n
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t{%- for item in doc.items -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endfor -%}\n\t\n
{{ _(\"Item\") }}{{ _(\"Qty\") }}{{ _(\"Amount\") }}
\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
{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t
{{ _(\"SR.No\") }}:
\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t
{{ item.qty }}{{ item.get_formatted(\"net_amount\") }}
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- for row in doc.taxes -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endif -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- if doc.rounded_total -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endif -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- if doc.change_amount -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t{%- endif -%}\n\t\n
\n\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t
\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\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t
\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Grand Total\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Rounded Total\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Paid Amount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t
\n\t\t\t\t\t{{ _(\"Change Amount\") }}\n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t
\n
\n

{{ doc.terms or \"\" }}

\n

{{ _(\"Thank you, please visit again.\") }}

", - "idx": 0, - "line_breaks": 0, - "margin_bottom": 0.0, - "margin_left": 0.0, - "margin_right": 0.0, - "margin_top": 0.0, - "modified": "2021-12-08 10:25:01.930885", - "modified_by": "Administrator", - "module": "Regional", - "name": "KSA POS Invoice", - "owner": "Administrator", - "page_number": "Hide", - "print_format_builder": 0, - "print_format_builder_beta": 0, - "print_format_type": "Jinja", - "raw_printing": 0, - "show_section_headings": 0, - "standard": "Yes" -} \ No newline at end of file diff --git a/erpnext/regional/print_format/ksa_vat_invoice/__init__.py b/erpnext/regional/print_format/ksa_vat_invoice/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json deleted file mode 100644 index 6b64d474535..00000000000 --- a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "absolute_value": 0, - "align_labels_right": 0, - "creation": "2021-10-29 22:46:26.039023", - "css": ".qr-code{\n float:right;\n}\n\n.invoice-heading {\n margin: 0;\n}\n\n.ksa-invoice-table {\n border: 1px solid #888a8e;\n border-collapse: collapse;\n width: 100%;\n margin: 20px 0;\n font-size: 16px;\n}\n\n.ksa-invoice-table.two-columns td:nth-child(2) {\n direction: rtl;\n}\n\n.ksa-invoice-table th {\n border: 1px solid #888a8e;\n max-width: 50%;\n padding: 8px;\n}\n\n.ksa-invoice-table td {\n padding: 5px;\n border: 1px solid #888a8e;\n max-width: 50%;\n}\n\n.ksa-invoice-table thead,\n.ksa-invoice-table tfoot {\n text-transform: uppercase;\n}\n\n.qr-rtl {\n direction: rtl;\n}\n\n.qr-flex{\n display: flex;\n justify-content: space-between;\n}", - "custom_format": 1, - "default_print_language": "en", - "disabled": 1, - "doc_type": "Sales Invoice", - "docstatus": 0, - "doctype": "Print Format", - "font_size": 14, - "html": "
\n
\n
\n

TAX INVOICE

\n

\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629

\n
\n \n \n
\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\t\t{% if (company.tax_id) %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n {% if(supplier_address_doc) %}\n \n \n \n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n {% if(customer_address) %}\n \n \n \n \n {% endif %}\n \n {% if(customer_shipping_address) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n \n \n \n \n \n \n \n \n \n {% endif %}\n \n \n \n \n \n \n
{{ company.name }}{{ company.company_name_in_arabic }}
Invoice#: {{doc.name}}\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}
Invoice Date: {{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}
Date of Supply:{{doc.posting_date}}\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}
Supplier:\u0627\u0644\u0645\u0648\u0631\u062f:
Supplier Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:
{{ company.tax_id }}{{ company.tax_id }}
{{ company.name }}{{ company.company_name_in_arabic }}
{{ supplier_address_doc.address_line1}} {{ supplier_address_doc.address_in_arabic}}
Phone: {{ supplier_address_doc.phone }}\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}
Email: {{ supplier_address_doc.email_id }}\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}
CUSTOMER:\u0639\u0645\u064a\u0644:
Customer Tax Identification Number:\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:
{{ customer_tax_id }}{{ customer_tax_id }}
{{ doc.customer }} {{ doc.customer_name_in_arabic }}
{{ customer_address.address_line1}} {{ customer_address.address_in_arabic}}
SHIPPING ADDRESS:\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:
{{ customer_shipping_address.address_line1}} {{ customer_shipping_address.address_in_arabic}}
OTHER INFORMATION\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649
Purchase Order Number: {{ doc.po_no }}\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}
Payment Due Date: {{ doc.due_date}} \u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}
\n\n \n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n \n {% set total = namespace(amount = 0) %}\n \n \n \n \n \n \n \n \n {% for row in doc.taxes %}\n \n {% endfor %}\n \n \n \n \n \n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n \n \n \n \n \n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set key = item.item_code or item.item_name %}\n {% set tax_amount = frappe.utils.flt(data_object[key][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n \n {% endfor %}\n \n \n {%- endfor -%}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Nature of goods or services
\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a
\n Unit price
\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n
\n Quantity
\n \u0627\u0644\u0643\u0645\u064a\u0629\n
\n Taxable Amount
\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n
{{row.description}}\n Total
\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n
{{ item.item_code or item.item_name }}{{ item.get_formatted(\"rate\") }}{{ item.qty }}{{ item.get_formatted(\"amount\") }}\n
\n {%- if(data_object[key][0])-%}\n {{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}\n {%- endif -%}\n \n {%- if(data_object[key][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n
\n
{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n
\n Total (Excluding VAT)\n
\n Total VAT\n
\n {{ doc.get_formatted(\"total\") }}
\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n
{{ doc.get_formatted(\"grand_total\") }}\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642Total Amount Due{{ doc.get_formatted(\"grand_total\") }}
\n\n\t{%- if doc.terms -%}\n

\n {{doc.terms}}\n

\n\t{%- endif -%}\n
\n", - "idx": 0, - "line_breaks": 0, - "margin_bottom": 15.0, - "margin_left": 15.0, - "margin_right": 15.0, - "margin_top": 15.0, - "modified": "2021-12-07 13:43:38.018593", - "modified_by": "Administrator", - "module": "Regional", - "name": "KSA VAT Invoice", - "owner": "Administrator", - "page_number": "Hide", - "print_format_builder": 0, - "print_format_builder_beta": 0, - "print_format_type": "Jinja", - "raw_printing": 0, - "show_section_headings": 0, - "standard": "Yes" -} \ No newline at end of file diff --git a/erpnext/regional/report/ksa_vat/__init__.py b/erpnext/regional/report/ksa_vat/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.js b/erpnext/regional/report/ksa_vat/ksa_vat.js deleted file mode 100644 index 59e72c3e638..00000000000 --- a/erpnext/regional/report/ksa_vat/ksa_vat.js +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) 2016, Havenir Solutions and contributors -// For license information, please see license.txt -/* eslint-disable */ - -frappe.query_reports["KSA VAT"] = { - onload() { - frappe.breadcrumbs.add('Accounts'); - }, - "filters": [ - { - "fieldname": "company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") - }, - { - "fieldname": "from_date", - "label": __("From Date"), - "fieldtype": "Date", - "reqd": 1, - "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), - }, - { - "fieldname": "to_date", - "label": __("To Date"), - "fieldtype": "Date", - "reqd": 1, - "default": frappe.datetime.get_today() - } - ], - "formatter": function(value, row, column, data, default_formatter) { - if (data - && (data.title=='VAT on Sales' || data.title=='VAT on Purchases') - && data.title==value) { - value = $(`${value}`); - var $value = $(value).css("font-weight", "bold"); - value = $value.wrap("

").parent().html(); - return value - }else if (data.title=='Grand Total'){ - if (data.title==value) { - value = $(`${value}`); - var $value = $(value).css("font-weight", "bold"); - value = $value.wrap("

").parent().html(); - return value - }else{ - value = default_formatter(value, row, column, data); - value = $(`${value}`); - var $value = $(value).css("font-weight", "bold"); - value = $value.wrap("

").parent().html(); - return value - } - }else{ - value = default_formatter(value, row, column, data); - return value; - } - }, -}; diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.json b/erpnext/regional/report/ksa_vat/ksa_vat.json deleted file mode 100644 index 036e2603103..00000000000 --- a/erpnext/regional/report/ksa_vat/ksa_vat.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "add_total_row": 0, - "columns": [], - "creation": "2021-07-13 08:54:38.000949", - "disable_prepared_report": 1, - "disabled": 1, - "docstatus": 0, - "doctype": "Report", - "filters": [], - "idx": 0, - "is_standard": "Yes", - "modified": "2021-08-26 04:14:37.202594", - "modified_by": "Administrator", - "module": "Regional", - "name": "KSA VAT", - "owner": "Administrator", - "prepared_report": 1, - "ref_doctype": "GL Entry", - "report_name": "KSA VAT", - "report_type": "Script Report", - "roles": [ - { - "role": "System Manager" - }, - { - "role": "Accounts Manager" - }, - { - "role": "Accounts User" - } - ] -} \ No newline at end of file diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.py b/erpnext/regional/report/ksa_vat/ksa_vat.py deleted file mode 100644 index 15996d2d1f8..00000000000 --- a/erpnext/regional/report/ksa_vat/ksa_vat.py +++ /dev/null @@ -1,231 +0,0 @@ -# Copyright (c) 2013, Havenir Solutions and contributors -# For license information, please see license.txt - - -import json - -import frappe -from frappe import _ -from frappe.utils import get_url_to_list - - -def execute(filters=None): - columns = columns = get_columns() - data = get_data(filters) - return columns, data - - -def get_columns(): - return [ - { - "fieldname": "title", - "label": _("Title"), - "fieldtype": "Data", - "width": 300, - }, - { - "fieldname": "amount", - "label": _("Amount (SAR)"), - "fieldtype": "Currency", - "options": "currency", - "width": 150, - }, - { - "fieldname": "adjustment_amount", - "label": _("Adjustment (SAR)"), - "fieldtype": "Currency", - "options": "currency", - "width": 150, - }, - { - "fieldname": "vat_amount", - "label": _("VAT Amount (SAR)"), - "fieldtype": "Currency", - "options": "currency", - "width": 150, - }, - { - "fieldname": "currency", - "label": _("Currency"), - "fieldtype": "Currency", - "width": 150, - "hidden": 1, - }, - ] - - -def get_data(filters): - data = [] - - # Validate if vat settings exist - company = filters.get("company") - company_currency = frappe.get_cached_value("Company", company, "default_currency") - - if frappe.db.exists("KSA VAT Setting", company) is None: - url = get_url_to_list("KSA VAT Setting") - frappe.msgprint(_('Create KSA VAT Setting for this company').format(url)) - return data - - ksa_vat_setting = frappe.get_doc("KSA VAT Setting", company) - - # Sales Heading - append_data(data, "VAT on Sales", "", "", "", company_currency) - - grand_total_taxable_amount = 0 - grand_total_taxable_adjustment_amount = 0 - grand_total_tax = 0 - - for vat_setting in ksa_vat_setting.ksa_vat_sales_accounts: - ( - total_taxable_amount, - total_taxable_adjustment_amount, - total_tax, - ) = get_tax_data_for_each_vat_setting(vat_setting, filters, "Sales Invoice") - - # Adding results to data - append_data( - data, - vat_setting.title, - total_taxable_amount, - total_taxable_adjustment_amount, - total_tax, - company_currency, - ) - - grand_total_taxable_amount += total_taxable_amount - grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount - grand_total_tax += total_tax - - # Sales Grand Total - append_data( - data, - "Grand Total", - grand_total_taxable_amount, - grand_total_taxable_adjustment_amount, - grand_total_tax, - company_currency, - ) - - # Blank Line - append_data(data, "", "", "", "", company_currency) - - # Purchase Heading - append_data(data, "VAT on Purchases", "", "", "", company_currency) - - grand_total_taxable_amount = 0 - grand_total_taxable_adjustment_amount = 0 - grand_total_tax = 0 - - for vat_setting in ksa_vat_setting.ksa_vat_purchase_accounts: - ( - total_taxable_amount, - total_taxable_adjustment_amount, - total_tax, - ) = get_tax_data_for_each_vat_setting(vat_setting, filters, "Purchase Invoice") - - # Adding results to data - append_data( - data, - vat_setting.title, - total_taxable_amount, - total_taxable_adjustment_amount, - total_tax, - company_currency, - ) - - grand_total_taxable_amount += total_taxable_amount - grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount - grand_total_tax += total_tax - - # Purchase Grand Total - append_data( - data, - "Grand Total", - grand_total_taxable_amount, - grand_total_taxable_adjustment_amount, - grand_total_tax, - company_currency, - ) - - return data - - -def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype): - """ - (KSA, {filters}, 'Sales Invoice') => 500, 153, 10 \n - calculates and returns \n - total_taxable_amount, total_taxable_adjustment_amount, total_tax""" - from_date = filters.get("from_date") - to_date = filters.get("to_date") - - # Initiate variables - total_taxable_amount = 0 - total_taxable_adjustment_amount = 0 - total_tax = 0 - # Fetch All Invoices - invoices = frappe.get_all( - doctype, - filters={"docstatus": 1, "posting_date": ["between", [from_date, to_date]]}, - fields=["name", "is_return"], - ) - - for invoice in invoices: - invoice_items = frappe.get_all( - f"{doctype} Item", - filters={ - "docstatus": 1, - "parent": invoice.name, - "item_tax_template": vat_setting.item_tax_template, - }, - fields=["item_code", "net_amount"], - ) - - for item in invoice_items: - # Summing up total taxable amount - if invoice.is_return == 0: - total_taxable_amount += item.net_amount - - if invoice.is_return == 1: - total_taxable_adjustment_amount += item.net_amount - - # Summing up total tax - total_tax += get_tax_amount(item.item_code, vat_setting.account, doctype, invoice.name) - - return total_taxable_amount, total_taxable_adjustment_amount, total_tax - - -def append_data(data, title, amount, adjustment_amount, vat_amount, company_currency): - """Returns data with appended value.""" - data.append( - { - "title": _(title), - "amount": amount, - "adjustment_amount": adjustment_amount, - "vat_amount": vat_amount, - "currency": company_currency, - } - ) - - -def get_tax_amount(item_code, account_head, doctype, parent): - if doctype == "Sales Invoice": - tax_doctype = "Sales Taxes and Charges" - - elif doctype == "Purchase Invoice": - tax_doctype = "Purchase Taxes and Charges" - - item_wise_tax_detail = frappe.get_value( - tax_doctype, - {"docstatus": 1, "parent": parent, "account_head": account_head}, - "item_wise_tax_detail", - ) - - tax_amount = 0 - if item_wise_tax_detail and len(item_wise_tax_detail) > 0: - item_wise_tax_detail = json.loads(item_wise_tax_detail) - for key, value in item_wise_tax_detail.items(): - if key == item_code: - tax_amount = value[1] - break - - return tax_amount diff --git a/erpnext/regional/saudi_arabia/__init__.py b/erpnext/regional/saudi_arabia/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py deleted file mode 100644 index 7f41c462cce..00000000000 --- a/erpnext/regional/saudi_arabia/setup.py +++ /dev/null @@ -1,173 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe -from frappe.permissions import add_permission, update_permission_property -from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import ( - create_ksa_vat_setting, -) -from frappe.custom.doctype.custom_field.custom_field import create_custom_fields - - -def setup(company=None, patch=True): - add_print_formats() - add_permissions() - make_custom_fields() - - -def add_print_formats(): - frappe.reload_doc("regional", "print_format", "detailed_tax_invoice", force=True) - frappe.reload_doc("regional", "print_format", "simplified_tax_invoice", force=True) - frappe.reload_doc("regional", "print_format", "tax_invoice", force=True) - frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True) - frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True) - - for d in ( - "Simplified Tax Invoice", - "Detailed Tax Invoice", - "Tax Invoice", - "KSA VAT Invoice", - "KSA POS Invoice", - ): - frappe.db.set_value("Print Format", d, "disabled", 0) - - -def add_permissions(): - """Add Permissions for KSA VAT Setting.""" - add_permission("KSA VAT Setting", "All", 0) - for role in ("Accounts Manager", "Accounts User", "System Manager"): - add_permission("KSA VAT Setting", role, 0) - update_permission_property("KSA VAT Setting", role, 0, "write", 1) - update_permission_property("KSA VAT Setting", role, 0, "create", 1) - - """Enable KSA VAT Report""" - frappe.db.set_value("Report", "KSA VAT", "disabled", 0) - - -def make_custom_fields(): - """Create Custom fields - - QR code Image file - - Company Name in Arabic - - Address in Arabic - """ - is_zero_rated = dict( - fieldname="is_zero_rated", - label="Is Zero Rated", - fieldtype="Check", - fetch_from="item_code.is_zero_rated", - insert_after="description", - print_hide=1, - ) - - is_exempt = dict( - fieldname="is_exempt", - label="Is Exempt", - fieldtype="Check", - fetch_from="item_code.is_exempt", - insert_after="is_zero_rated", - print_hide=1, - ) - - purchase_invoice_fields = [ - dict( - fieldname="company_trn", - label="Company TRN", - fieldtype="Read Only", - insert_after="shipping_address", - fetch_from="company.tax_id", - print_hide=1, - ), - dict( - fieldname="supplier_name_in_arabic", - label="Supplier Name in Arabic", - fieldtype="Read Only", - insert_after="supplier_name", - fetch_from="supplier.supplier_name_in_arabic", - print_hide=1, - ), - ] - - sales_invoice_fields = [ - dict( - fieldname="company_trn", - label="Company TRN", - fieldtype="Read Only", - insert_after="company_address", - fetch_from="company.tax_id", - print_hide=1, - ), - dict( - fieldname="customer_name_in_arabic", - label="Customer Name in Arabic", - fieldtype="Read Only", - insert_after="customer_name", - fetch_from="customer.customer_name_in_arabic", - print_hide=1, - ), - dict( - fieldname="ksa_einv_qr", - label="KSA E-Invoicing QR", - fieldtype="Attach Image", - read_only=1, - no_copy=1, - hidden=1, - ), - ] - - custom_fields = { - "Item": [is_zero_rated, is_exempt], - "Customer": [ - dict( - fieldname="customer_name_in_arabic", - label="Customer Name in Arabic", - fieldtype="Data", - insert_after="customer_name", - ), - ], - "Supplier": [ - dict( - fieldname="supplier_name_in_arabic", - label="Supplier Name in Arabic", - fieldtype="Data", - insert_after="supplier_name", - ), - ], - "Purchase Invoice": purchase_invoice_fields, - "Purchase Order": purchase_invoice_fields, - "Purchase Receipt": purchase_invoice_fields, - "Sales Invoice": sales_invoice_fields, - "POS Invoice": sales_invoice_fields, - "Sales Order": sales_invoice_fields, - "Delivery Note": sales_invoice_fields, - "Sales Invoice Item": [is_zero_rated, is_exempt], - "POS Invoice Item": [is_zero_rated, is_exempt], - "Purchase Invoice Item": [is_zero_rated, is_exempt], - "Sales Order Item": [is_zero_rated, is_exempt], - "Delivery Note Item": [is_zero_rated, is_exempt], - "Quotation Item": [is_zero_rated, is_exempt], - "Purchase Order Item": [is_zero_rated, is_exempt], - "Purchase Receipt Item": [is_zero_rated, is_exempt], - "Supplier Quotation Item": [is_zero_rated, is_exempt], - "Address": [ - dict( - fieldname="address_in_arabic", - label="Address in Arabic", - fieldtype="Data", - insert_after="address_line2", - ) - ], - "Company": [ - dict( - fieldname="company_name_in_arabic", - label="Company Name In Arabic", - fieldtype="Data", - insert_after="company_name", - ) - ], - } - - create_custom_fields(custom_fields, ignore_validate=True, update=True) - - -def update_regional_tax_settings(country, company): - create_ksa_vat_setting(company) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py deleted file mode 100644 index cac5ec113e8..00000000000 --- a/erpnext/regional/saudi_arabia/utils.py +++ /dev/null @@ -1,169 +0,0 @@ -import io -import os -from base64 import b64encode - -import frappe -from frappe import _ -from frappe.custom.doctype.custom_field.custom_field import create_custom_fields -from frappe.utils.data import add_to_date, get_time, getdate -from pyqrcode import create as qr_create - -from erpnext import get_region - - -def create_qr_code(doc, method=None): - region = get_region(doc.company) - if region not in ["Saudi Arabia"]: - return - - # if QR Code field not present, create it. Invoices without QR are invalid as per law. - if not hasattr(doc, "ksa_einv_qr"): - create_custom_fields( - { - doc.doctype: [ - dict( - fieldname="ksa_einv_qr", - label="KSA E-Invoicing QR", - fieldtype="Attach Image", - read_only=1, - no_copy=1, - hidden=1, - ) - ] - } - ) - - # Don't create QR Code if it already exists - qr_code = doc.get("ksa_einv_qr") - if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}): - return - - meta = frappe.get_meta(doc.doctype) - - if "ksa_einv_qr" in [d.fieldname for d in meta.get_image_fields()]: - """TLV conversion for - 1. Seller's Name - 2. VAT Number - 3. Time Stamp - 4. Invoice Amount - 5. VAT Amount - """ - tlv_array = [] - # Sellers Name - - seller_name = frappe.db.get_value("Company", doc.company, "company_name_in_arabic") - - if not seller_name: - frappe.throw(_("Arabic name missing for {} in the company document").format(doc.company)) - - tag = bytes([1]).hex() - length = bytes([len(seller_name.encode("utf-8"))]).hex() - value = seller_name.encode("utf-8").hex() - tlv_array.append("".join([tag, length, value])) - - # VAT Number - tax_id = frappe.db.get_value("Company", doc.company, "tax_id") - if not tax_id: - frappe.throw(_("Tax ID missing for {} in the company document").format(doc.company)) - - tag = bytes([2]).hex() - length = bytes([len(tax_id)]).hex() - value = tax_id.encode("utf-8").hex() - tlv_array.append("".join([tag, length, value])) - - # Time Stamp - posting_date = getdate(doc.posting_date) - time = get_time(doc.posting_time) - seconds = time.hour * 60 * 60 + time.minute * 60 + time.second - time_stamp = add_to_date(posting_date, seconds=seconds) - time_stamp = time_stamp.strftime("%Y-%m-%dT%H:%M:%SZ") - - tag = bytes([3]).hex() - length = bytes([len(time_stamp)]).hex() - value = time_stamp.encode("utf-8").hex() - tlv_array.append("".join([tag, length, value])) - - # Invoice Amount - invoice_amount = str(doc.base_grand_total) - tag = bytes([4]).hex() - length = bytes([len(invoice_amount)]).hex() - value = invoice_amount.encode("utf-8").hex() - tlv_array.append("".join([tag, length, value])) - - # VAT Amount - vat_amount = str(get_vat_amount(doc)) - - tag = bytes([5]).hex() - length = bytes([len(vat_amount)]).hex() - value = vat_amount.encode("utf-8").hex() - tlv_array.append("".join([tag, length, value])) - - # Joining bytes into one - tlv_buff = "".join(tlv_array) - - # base64 conversion for QR Code - base64_string = b64encode(bytes.fromhex(tlv_buff)).decode() - - qr_image = io.BytesIO() - url = qr_create(base64_string, error="L") - url.png(qr_image, scale=2, quiet_zone=1) - - name = frappe.generate_hash(doc.name, 5) - - # making file - filename = f"QRCode-{name}.png".replace(os.path.sep, "__") - _file = frappe.get_doc( - { - "doctype": "File", - "file_name": filename, - "is_private": 0, - "content": qr_image.getvalue(), - "attached_to_doctype": doc.get("doctype"), - "attached_to_name": doc.get("name"), - "attached_to_field": "ksa_einv_qr", - } - ) - - _file.save() - - # assigning to document - doc.db_set("ksa_einv_qr", _file.file_url) - doc.notify_update() - - -def get_vat_amount(doc): - vat_settings = frappe.db.get_value("KSA VAT Setting", {"company": doc.company}) - vat_accounts = [] - vat_amount = 0 - - if vat_settings: - vat_settings_doc = frappe.get_cached_doc("KSA VAT Setting", vat_settings) - - for row in vat_settings_doc.get("ksa_vat_sales_accounts"): - vat_accounts.append(row.account) - - for tax in doc.get("taxes"): - if tax.account_head in vat_accounts: - vat_amount += tax.base_tax_amount - - return vat_amount - - -def delete_qr_code_file(doc, method=None): - region = get_region(doc.company) - if region not in ["Saudi Arabia"]: - return - - if hasattr(doc, "ksa_einv_qr"): - if doc.get("ksa_einv_qr"): - file_doc = frappe.get_list("File", {"file_url": doc.get("ksa_einv_qr")}) - if len(file_doc): - frappe.delete_doc("File", file_doc[0].name) - - -def delete_vat_settings_for_company(doc, method=None): - if doc.country != "Saudi Arabia": - return - - if frappe.db.exists("KSA VAT Setting", doc.name): - frappe.delete_doc("KSA VAT Setting", doc.name) diff --git a/erpnext/regional/saudi_arabia/wizard/__init__.py b/erpnext/regional/saudi_arabia/wizard/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/regional/saudi_arabia/wizard/data/__init__.py b/erpnext/regional/saudi_arabia/wizard/data/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/regional/saudi_arabia/wizard/data/ksa_vat_settings.json b/erpnext/regional/saudi_arabia/wizard/data/ksa_vat_settings.json deleted file mode 100644 index 60951a9ceca..00000000000 --- a/erpnext/regional/saudi_arabia/wizard/data/ksa_vat_settings.json +++ /dev/null @@ -1,47 +0,0 @@ -[ - { - "type": "Sales Account", - "accounts": [ - { - "title": "Standard rated Sales", - "item_tax_template": "KSA VAT 5%", - "account": "VAT 5%" - }, - { - "title": "Zero rated domestic sales", - "item_tax_template": "KSA VAT Zero", - "account": "VAT Zero" - }, - { - "title": "Exempted sales", - "item_tax_template": "KSA VAT Exempted", - "account": "VAT Exempted" - } - ] - }, - { - "type": "Purchase Account", - "accounts": [ - { - "title": "Standard rated domestic purchases", - "item_tax_template": "KSA VAT 5%", - "account": "VAT 5%" - }, - { - "title": "Imports subject to VAT paid at customs", - "item_tax_template": "KSA Excise 50%", - "account": "Excise 50%" - }, - { - "title": "Zero rated purchases", - "item_tax_template": "KSA VAT Zero", - "account": "VAT Zero" - }, - { - "title": "Exempted purchases", - "item_tax_template": "KSA VAT Exempted", - "account": "VAT Exempted" - } - ] - } -] \ No newline at end of file diff --git a/erpnext/regional/saudi_arabia/wizard/operations/__init__.py b/erpnext/regional/saudi_arabia/wizard/operations/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py b/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py deleted file mode 100644 index 66d9df224e7..00000000000 --- a/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py +++ /dev/null @@ -1,46 +0,0 @@ -import json -import os - -import frappe - - -def create_ksa_vat_setting(company): - """On creation of first company. Creates KSA VAT Setting""" - - company = frappe.get_doc("Company", company) - - file_path = os.path.join(os.path.dirname(__file__), "..", "data", "ksa_vat_settings.json") - with open(file_path, "r") as json_file: - account_data = json.load(json_file) - - # Creating KSA VAT Setting - ksa_vat_setting = frappe.get_doc({"doctype": "KSA VAT Setting", "company": company.name}) - - for data in account_data: - if data["type"] == "Sales Account": - for row in data["accounts"]: - item_tax_template = row["item_tax_template"] - account = row["account"] - ksa_vat_setting.append( - "ksa_vat_sales_accounts", - { - "title": row["title"], - "item_tax_template": f"{item_tax_template} - {company.abbr}", - "account": f"{account} - {company.abbr}", - }, - ) - - elif data["type"] == "Purchase Account": - for row in data["accounts"]: - item_tax_template = row["item_tax_template"] - account = row["account"] - ksa_vat_setting.append( - "ksa_vat_purchase_accounts", - { - "title": row["title"], - "item_tax_template": f"{item_tax_template} - {company.abbr}", - "account": f"{account} - {company.abbr}", - }, - ) - - ksa_vat_setting.save() diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 7482a33653c..c133cd3152d 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -24,10 +24,10 @@ "account_manager", "image", "defaults_tab", - "default_price_list", + "default_currency", "default_bank_account", "column_break_14", - "default_currency", + "default_price_list", "internal_customer_section", "is_internal_customer", "represents_company", @@ -568,11 +568,10 @@ "link_fieldname": "party" } ], - "modified": "2022-11-08 15:52:34.462657", + "modified": "2023-02-18 11:04:46.343527", "modified_by": "Administrator", "module": "Selling", "name": "Customer", - "name_case": "Title Case", "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ diff --git a/erpnext/selling/doctype/party_specific_item/party_specific_item.json b/erpnext/selling/doctype/party_specific_item/party_specific_item.json index 32b5d478bb5..a1f9902aaee 100644 --- a/erpnext/selling/doctype/party_specific_item/party_specific_item.json +++ b/erpnext/selling/doctype/party_specific_item/party_specific_item.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_import": 1, "creation": "2021-08-27 19:28:07.559978", "doctype": "DocType", "editable_grid": 1, @@ -51,7 +52,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-09-14 13:27:58.612334", + "modified": "2023-02-15 13:00:50.379713", "modified_by": "Administrator", "module": "Selling", "name": "Party Specific Item", @@ -72,6 +73,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "party", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py index 44c4d5497ba..2624db3191d 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py @@ -216,7 +216,7 @@ def get_sales_order_details(company_list, filters): ) if filters.get("item_group"): - query = query.where(db_so_item.item_group == frappe.db.escape(filters.item_group)) + query = query.where(db_so_item.item_group == filters.item_group) if filters.get("from_date"): query = query.where(db_so.transaction_date >= filters.from_date) @@ -225,7 +225,7 @@ def get_sales_order_details(company_list, filters): query = query.where(db_so.transaction_date <= filters.to_date) if filters.get("item_code"): - query = query.where(db_so_item.item_group == frappe.db.escape(filters.item_code)) + query = query.where(db_so_item.item_code == filters.item_code) if filters.get("customer"): query = query.where(db_so.customer == filters.customer) diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 8ff01f5cb4c..5ce6e9c1460 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -418,8 +418,6 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran callback: function(r) { if(r.message) { frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message); - } else { - frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message); } } }); diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index facefa376a5..ece5a7d5544 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -8,7 +8,6 @@ from frappe.permissions import ( get_doc_permissions, has_permission, remove_user_permission, - set_user_permission_if_allowed, ) from frappe.utils import cstr, getdate, today, validate_email_address from frappe.utils.nestedset import NestedSet @@ -96,7 +95,7 @@ class Employee(NestedSet): return add_user_permission("Employee", self.name, self.user_id) - set_user_permission_if_allowed("Company", self.company, self.user_id) + add_user_permission("Company", self.company, self.user_id) def update_user(self): # add employee role if missing diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index 4256a7d8312..481a3a5ebea 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -3,13 +3,17 @@ import frappe -from frappe import _ +from frappe import _, qb from frappe.desk.notifications import clear_notifications from frappe.model.document import Document -from frappe.utils import cint +from frappe.utils import cint, create_batch class TransactionDeletionRecord(Document): + def __init__(self, *args, **kwargs): + super(TransactionDeletionRecord, self).__init__(*args, **kwargs) + self.batch_size = 5000 + def validate(self): frappe.only_for("System Manager") self.validate_doctypes_to_be_ignored() @@ -155,8 +159,9 @@ class TransactionDeletionRecord(Document): "DocField", filters={"fieldtype": "Table", "parent": doctype}, pluck="options" ) - for table in child_tables: - frappe.db.delete(table, {"parent": ["in", parent_docs_to_be_deleted]}) + for batch in create_batch(parent_docs_to_be_deleted, self.batch_size): + for table in child_tables: + frappe.db.delete(table, {"parent": ["in", batch]}) def delete_docs_linked_with_specified_company(self, doctype, company_fieldname): frappe.db.delete(doctype, {company_fieldname: self.company}) @@ -181,13 +186,16 @@ class TransactionDeletionRecord(Document): frappe.db.sql("""update `tabSeries` set current = %s where name=%s""", (last, prefix)) def delete_version_log(self, doctype, company_fieldname): - frappe.db.sql( - """delete from `tabVersion` where ref_doctype=%s and docname in - (select name from `tab{0}` where `{1}`=%s)""".format( - doctype, company_fieldname - ), - (doctype, self.company), - ) + dt = qb.DocType(doctype) + names = qb.from_(dt).select(dt.name).where(dt[company_fieldname] == self.company).run(as_list=1) + names = [x[0] for x in names] + + if names: + versions = qb.DocType("Version") + for batch in create_batch(names, self.batch_size): + qb.from_(versions).delete().where( + (versions.ref_doctype == doctype) & (versions.docname.isin(batch)) + ).run() def delete_communications(self, doctype, company_fieldname): reference_docs = frappe.get_all(doctype, filters={company_fieldname: self.company}) @@ -199,7 +207,8 @@ class TransactionDeletionRecord(Document): ) communication_names = [c.name for c in communications] - frappe.delete_doc("Communication", communication_names, ignore_permissions=True) + for batch in create_batch(communication_names, self.batch_size): + frappe.delete_doc("Communication", batch, ignore_permissions=True) @frappe.whitelist() diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index 45e39c5bd0b..5750914b9ad 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -4015,34 +4015,6 @@ "tax_rate": 18.00 } }, - - "Saudi Arabia": { - "KSA VAT 15%": { - "account_name": "VAT 15%", - "tax_rate": 15.00 - }, - "KSA VAT 5%": { - "account_name": "VAT 5%", - "tax_rate": 5.00 - }, - "KSA VAT Zero": { - "account_name": "VAT Zero", - "tax_rate": 0.00 - }, - "KSA VAT Exempted": { - "account_name": "VAT Exempted", - "tax_rate": 0.00 - }, - "KSA Excise 50%": { - "account_name": "Excise 50%", - "tax_rate": 50.00 - }, - "KSA Excise 100%": { - "account_name": "Excise 100%", - "tax_rate": 100.00 - } - }, - "Serbia": { "Serbia Tax": { "account_name": "VAT", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index ea3cf1948b3..ae56645b730 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -97,12 +97,12 @@ frappe.ui.form.on("Delivery Note", { } if (frm.doc.docstatus == 1 && !frm.doc.inter_company_reference) { - let internal = me.frm.doc.is_internal_customer; + let internal = frm.doc.is_internal_customer; if (internal) { - let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Receipt" : + let button_label = (frm.doc.company === frm.doc.represents_company) ? "Internal Purchase Receipt" : "Inter Company Purchase Receipt"; - me.frm.add_custom_button(button_label, function() { + frm.add_custom_button(__(button_label), function() { frappe.model.open_mapped_doc({ method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_inter_company_purchase_receipt', frm: frm, diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 165a56b7839..0c1f82029e6 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -521,6 +521,7 @@ "allow_bulk_edit": 1, "fieldname": "items", "fieldtype": "Table", + "label": "Delivery Note Item", "oldfieldname": "delivery_note_details", "oldfieldtype": "Table", "options": "Delivery Note Item", @@ -666,6 +667,7 @@ { "fieldname": "taxes", "fieldtype": "Table", + "label": "Sales Taxes and Charges", "oldfieldname": "other_charges", "oldfieldtype": "Table", "options": "Sales Taxes and Charges" @@ -1401,7 +1403,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2022-12-12 18:38:53.067799", + "modified": "2023-02-14 04:45:44.179670", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js index 9e6f3bc9321..6ff3ed3e8e5 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js @@ -14,7 +14,7 @@ frappe.listview_settings['Delivery Note'] = { return [__("Completed"), "green", "per_billed,=,100"]; } }, - onload: function (listview) { + onload: function (doclist) { const action = () => { const selected_docs = doclist.get_checked_items(); const docnames = doclist.get_checked_items(true); @@ -56,14 +56,14 @@ frappe.listview_settings['Delivery Note'] = { // doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false); - listview.page.add_action_item(__('Create Delivery Trip'), action); + doclist.page.add_action_item(__('Create Delivery Trip'), action); - listview.page.add_action_item(__("Sales Invoice"), ()=>{ - erpnext.bulk_transaction_processing.create(listview, "Delivery Note", "Sales Invoice"); + doclist.page.add_action_item(__("Sales Invoice"), ()=>{ + erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Sales Invoice"); }); - listview.page.add_action_item(__("Packaging Slip From Delivery Note"), ()=>{ - erpnext.bulk_transaction_processing.create(listview, "Delivery Note", "Packing Slip"); + doclist.page.add_action_item(__("Packaging Slip From Delivery Note"), ()=>{ + erpnext.bulk_transaction_processing.create(doclist, "Delivery Note", "Packing Slip"); }); } }; diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 629e50efeb9..34adbebc07c 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -706,7 +706,7 @@ "depends_on": "enable_deferred_expense", "fieldname": "no_of_months_exp", "fieldtype": "Int", - "label": "No of Months" + "label": "No of Months (Expense)" }, { "collapsible": 1, @@ -911,7 +911,7 @@ "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2023-01-07 22:45:00.341745", + "modified": "2023-02-14 04:48:26.343620", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 686e6cb047d..c06700a99a1 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -358,7 +358,7 @@ class Item(Document): check_list.append(d.item_tax_template) def validate_barcode(self): - from stdnum import ean + import barcodenumber if len(self.barcodes) > 0: for item_barcode in self.barcodes: @@ -376,19 +376,16 @@ class Item(Document): item_barcode.barcode_type = ( "" if item_barcode.barcode_type not in options else item_barcode.barcode_type ) - if item_barcode.barcode_type and item_barcode.barcode_type.upper() in ( - "EAN", - "UPC-A", - "EAN-13", - "EAN-8", - ): - if not ean.is_valid(item_barcode.barcode): - frappe.throw( - _("Barcode {0} is not a valid {1} code").format( - item_barcode.barcode, item_barcode.barcode_type - ), - InvalidBarcode, - ) + if item_barcode.barcode_type: + barcode_type = convert_erpnext_to_barcodenumber(item_barcode.barcode_type.upper()) + if barcode_type in barcodenumber.barcodes(): + if not barcodenumber.check_code(barcode_type, item_barcode.barcode): + frappe.throw( + _("Barcode {0} is not a valid {1} code").format( + item_barcode.barcode, item_barcode.barcode_type + ), + InvalidBarcode, + ) def validate_warehouse_for_reorder(self): """Validate Reorder level table for duplicate and conditional mandatory""" @@ -985,6 +982,22 @@ 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): frappe.get_doc( { diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 53f6b7f8f17..67ed90d4e75 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -579,6 +579,19 @@ class TestItem(FrappeTestCase): { "barcode": "ARBITRARY_TEXT", }, + {"barcode": "72527273070", "barcode_type": "UPC-A"}, + {"barcode": "123456", "barcode_type": "CODE-39"}, + {"barcode": "401268452363", "barcode_type": "EAN-12"}, + {"barcode": "90311017", "barcode_type": "EAN-8"}, + {"barcode": "0123456789012", "barcode_type": "GS1"}, + {"barcode": "2211564566668", "barcode_type": "GTIN"}, + {"barcode": "0256480249", "barcode_type": "ISBN"}, + {"barcode": "0192552570", "barcode_type": "ISBN-10"}, + {"barcode": "9781234567897", "barcode_type": "ISBN-13"}, + {"barcode": "9771234567898", "barcode_type": "ISSN"}, + {"barcode": "4581171967072", "barcode_type": "JAN"}, + {"barcode": "12345678", "barcode_type": "PZN"}, + {"barcode": "725272730706", "barcode_type": "UPC"}, ] create_item(item_code) for barcode_properties in barcode_properties_list: diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.json b/erpnext/stock/doctype/item_barcode/item_barcode.json index bda1218817c..d9a8347ca0d 100644 --- a/erpnext/stock/doctype/item_barcode/item_barcode.json +++ b/erpnext/stock/doctype/item_barcode/item_barcode.json @@ -25,7 +25,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Barcode Type", - "options": "\nEAN\nUPC-A" + "options": "\nEAN\nUPC-A\nCODE-39\nEAN-12\nEAN-8\nGS1\nGTIN\nISBN\nISBN-10\nISBN-13\nISSN\nJAN\nPZN\nUPC" }, { "fieldname": "uom", diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index b3af309359a..111a0861b71 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -55,7 +55,6 @@ class LandedCostVoucher(Document): self.get_items_from_purchase_receipts() self.set_applicable_charges_on_item() - self.validate_applicable_charges_for_item() def check_mandatory(self): if not self.get("purchase_receipts"): @@ -115,6 +114,13 @@ class LandedCostVoucher(Document): total_item_cost += item.get(based_on_field) for item in self.get("items"): + if not total_item_cost and not item.get(based_on_field): + frappe.throw( + _( + "It's not possible to distribute charges equally when total amount is zero, please set 'Distribute Charges Based On' as 'Quantity'" + ) + ) + item.applicable_charges = flt( flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)), item.precision("applicable_charges"), @@ -162,6 +168,7 @@ class LandedCostVoucher(Document): ) def on_submit(self): + self.validate_applicable_charges_for_item() self.update_landed_cost() def on_cancel(self): diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 979b5c4f838..00fa1686c0d 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -175,6 +175,59 @@ class TestLandedCostVoucher(FrappeTestCase): ) self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0) + def test_landed_cost_voucher_for_zero_purchase_rate(self): + "Test impact of LCV on future stock balances." + from erpnext.stock.doctype.item.test_item import make_item + + item = make_item("LCV Stock Item", {"is_stock_item": 1}) + warehouse = "Stores - _TC" + + pr = make_purchase_receipt( + item_code=item.name, + warehouse=warehouse, + qty=10, + rate=0, + posting_date=add_days(frappe.utils.nowdate(), -2), + ) + + self.assertEqual( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0}, + "stock_value_difference", + ), + 0, + ) + + lcv = make_landed_cost_voucher( + company=pr.company, + receipt_document_type="Purchase Receipt", + receipt_document=pr.name, + charges=100, + distribute_charges_based_on="Distribute Manually", + do_not_save=True, + ) + + lcv.get_items_from_purchase_receipts() + lcv.items[0].applicable_charges = 100 + lcv.save() + lcv.submit() + + self.assertTrue( + frappe.db.exists( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0}, + ) + ) + self.assertEqual( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0}, + "stock_value_difference", + ), + 100, + ) + def test_landed_cost_voucher_against_purchase_invoice(self): pi = make_purchase_invoice( @@ -516,7 +569,7 @@ def make_landed_cost_voucher(**args): lcv = frappe.new_doc("Landed Cost Voucher") lcv.company = args.company or "_Test Company" - lcv.distribute_charges_based_on = "Amount" + lcv.distribute_charges_based_on = args.distribute_charges_based_on or "Amount" lcv.set( "purchase_receipts", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index af0d1483253..bb318f72526 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -887,18 +887,10 @@ def update_billing_percentage(pr_doc, update_modified=True): # Update Billing % based on pending accepted qty total_amount, total_billed_amount = 0, 0 - for item in pr_doc.items: - return_data = frappe.get_all( - "Purchase Receipt", - fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"], - filters=[ - ["Purchase Receipt", "docstatus", "=", 1], - ["Purchase Receipt", "is_return", "=", 1], - ["Purchase Receipt Item", "purchase_receipt_item", "=", item.name], - ], - ) + item_wise_returned_qty = get_item_wise_returned_qty(pr_doc) - returned_qty = return_data[0].qty if return_data else 0 + for item in pr_doc.items: + returned_qty = flt(item_wise_returned_qty.get(item.name)) returned_amount = flt(returned_qty) * flt(item.rate) pending_amount = flt(item.amount) - returned_amount total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt @@ -915,6 +907,27 @@ def update_billing_percentage(pr_doc, update_modified=True): 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() def make_purchase_invoice(source_name, target_doc=None): 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} ) - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][ - "amount" - ] += (account.amount * item.get(based_on_field) / total_item_cost) + if total_item_cost > 0: + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][ + account.expense_account + ]["amount"] += ( + account.amount * item.get(based_on_field) / total_item_cost + ) - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][ - "base_amount" - ] += (account.base_amount * item.get(based_on_field) / total_item_cost) + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][ + account.expense_account + ]["base_amount"] += ( + account.base_amount * item.get(based_on_field) / total_item_cost + ) + else: + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][ + account.expense_account + ]["amount"] += item.applicable_charges + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][ + account.expense_account + ]["base_amount"] += item.applicable_charges return item_account_wise_cost diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 557bb594bf0..7a350b9e446 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -859,7 +859,8 @@ "label": "Purchase Receipt Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "collapsible": 1, @@ -974,7 +975,8 @@ "label": "Purchase Invoice Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "product_bundle", @@ -1010,7 +1012,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-11-02 12:49:28.746701", + "modified": "2023-01-18 15:48:58.114923", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js index 42d0723d427..5f81679bade 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js @@ -2,7 +2,22 @@ // For license information, please see license.txt frappe.ui.form.on('Stock Reposting Settings', { - // refresh: function(frm) { + refresh: function(frm) { + frm.trigger('convert_to_item_based_reposting'); + }, - // } + convert_to_item_based_reposting: function(frm) { + frm.add_custom_button(__('Convert to Item Based Reposting'), function() { + frm.call({ + method: 'convert_to_item_wh_reposting', + frezz: true, + doc: frm.doc, + callback: function(r) { + if (!r.exc) { + frm.reload_doc(); + } + } + }) + }) + } }); diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py index e0c8ed12e7d..51fb5ac4c40 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py @@ -1,6 +1,8 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +import frappe +from frappe import _ from frappe.model.document import Document from frappe.utils import add_to_date, get_datetime, get_time_str, time_diff_in_hours @@ -24,3 +26,62 @@ class StockRepostingSettings(Document): if diff < 10: self.end_time = get_time_str(add_to_date(self.start_time, hours=10, as_datetime=True)) + + @frappe.whitelist() + def convert_to_item_wh_reposting(self): + """Convert Transaction reposting to Item Warehouse based reposting if Item Based Reposting has enabled.""" + + reposting_data = get_reposting_entries() + + vouchers = [d.voucher_no for d in reposting_data] + + item_warehouses = {} + + for ledger in get_stock_ledgers(vouchers): + key = (ledger.item_code, ledger.warehouse) + if key not in item_warehouses: + item_warehouses[key] = ledger.posting_date + elif frappe.utils.getdate(item_warehouses.get(key)) > frappe.utils.getdate(ledger.posting_date): + item_warehouses[key] = ledger.posting_date + + for key, posting_date in item_warehouses.items(): + item_code, warehouse = key + create_repost_item_valuation(item_code, warehouse, posting_date) + + for row in reposting_data: + frappe.db.set_value("Repost Item Valuation", row.name, "status", "Skipped") + + self.db_set("item_based_reposting", 1) + frappe.msgprint(_("Item Warehouse based reposting has been enabled.")) + + +def get_reposting_entries(): + return frappe.get_all( + "Repost Item Valuation", + fields=["voucher_no", "name"], + filters={"status": ("in", ["Queued", "In Progress"]), "docstatus": 1, "based_on": "Transaction"}, + ) + + +def get_stock_ledgers(vouchers): + return frappe.get_all( + "Stock Ledger Entry", + fields=["item_code", "warehouse", "posting_date"], + filters={"voucher_no": ("in", vouchers)}, + ) + + +def create_repost_item_valuation(item_code, warehouse, posting_date): + frappe.get_doc( + { + "doctype": "Repost Item Valuation", + "company": frappe.get_cached_value("Warehouse", warehouse, "company"), + "posting_date": posting_date, + "based_on": "Item and Warehouse", + "posting_time": "00:00:01", + "item_code": item_code, + "warehouse": warehouse, + "allow_negative_stock": True, + "status": "Queued", + } + ).submit() diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 8b63c0f9986..da17cdeb5ae 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -306,7 +306,7 @@ def get_stock_ledger_entries(filters, items): query = query.where(sle.item_code.isin(items)) for field in ["voucher_no", "batch_no", "project", "company"]: - if filters.get(field): + if filters.get(field) and field not in inventory_dimension_fields: query = query.where(sle[field] == filters.get(field)) query = apply_warehouse_filter(query, sle, filters) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index d054ce0f9d4..6a2983faaaf 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -2,6 +2,7 @@ # See license.txt import copy +from collections import defaultdict import frappe from frappe.tests.utils import FrappeTestCase @@ -186,6 +187,40 @@ class TestSubcontractingOrder(FrappeTestCase): ) self.assertEqual(len(ste.items), len(rm_items)) + def test_make_rm_stock_entry_for_batch_items_with_less_transfer(self): + set_backflush_based_on("BOM") + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 4", + "qty": 5, + "rate": 100, + "fg_item": "Subcontracted Item SA4", + "fg_item_qty": 5, + } + ] + + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + itemwise_transfer_qty = defaultdict(int) + for item in rm_items: + item["qty"] -= 1 + itemwise_transfer_qty[item["item_code"]] += item["qty"] + + ste = make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr = make_subcontracting_receipt(sco.name) + + for row in scr.supplied_items: + self.assertEqual(row.consumed_qty, itemwise_transfer_qty.get(row.rm_item_code) + 1) + def test_update_reserved_qty_for_subcontracting(self): # Create RM Material Receipt make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=10, basic_rate=100) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index b6bef8c4a02..3a2c53f4e44 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -51,13 +51,31 @@ frappe.ui.form.on('Subcontracting Receipt', { } })); - frm.set_query("expense_account", "items", function () { + frm.set_query('expense_account', 'items', function () { return { - query: "erpnext.controllers.queries.get_expense_account", + query: 'erpnext.controllers.queries.get_expense_account', filters: { 'company': frm.doc.company } }; }); + frm.set_query('batch_no', 'items', function(doc, cdt, cdn) { + var row = locals[cdt][cdn]; + return { + filters: { + item: row.item_code + } + } + }); + + let batch_no_field = frm.get_docfield("items", "batch_no"); + if (batch_no_field) { + batch_no_field.get_route_options_for_new_doc = function(row) { + return { + "item": row.doc.item_code + } + }; + } + frappe.db.get_single_value('Buying Settings', 'backflush_raw_materials_of_subcontract_based_on').then(val => { if (val == 'Material Transferred for Subcontract') { frm.fields_dict['supplied_items'].grid.grid_rows.forEach((grid_row) => { @@ -73,7 +91,7 @@ frappe.ui.form.on('Subcontracting Receipt', { refresh: (frm) => { if (frm.doc.docstatus > 0) { - frm.add_custom_button(__("Stock Ledger"), function () { + frm.add_custom_button(__('Stock Ledger'), function () { frappe.route_options = { voucher_no: frm.doc.name, from_date: frm.doc.posting_date, @@ -81,8 +99,8 @@ frappe.ui.form.on('Subcontracting Receipt', { company: frm.doc.company, show_cancelled_entries: frm.doc.docstatus === 2 }; - frappe.set_route("query-report", "Stock Ledger"); - }, __("View")); + frappe.set_route('query-report', 'Stock Ledger'); + }, __('View')); frm.add_custom_button(__('Accounting Ledger'), function () { frappe.route_options = { @@ -90,11 +108,11 @@ frappe.ui.form.on('Subcontracting Receipt', { from_date: frm.doc.posting_date, to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), company: frm.doc.company, - group_by: "Group by Voucher (Consolidated)", + group_by: 'Group by Voucher (Consolidated)', show_cancelled_entries: frm.doc.docstatus === 2 }; - frappe.set_route("query-report", "General Ledger"); - }, __("View")); + frappe.set_route('query-report', 'General Ledger'); + }, __('View')); } if (!frm.doc.is_return && frm.doc.docstatus == 1 && frm.doc.per_returned < 100) { @@ -111,25 +129,25 @@ frappe.ui.form.on('Subcontracting Receipt', { frm.add_custom_button(__('Subcontracting Order'), function () { if (!frm.doc.supplier) { frappe.throw({ - title: __("Mandatory"), - message: __("Please Select a Supplier") + title: __('Mandatory'), + message: __('Please Select a Supplier') }); } erpnext.utils.map_current_doc({ method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt', - source_doctype: "Subcontracting Order", + source_doctype: 'Subcontracting Order', target: frm, setters: { supplier: frm.doc.supplier, }, get_query_filters: { docstatus: 1, - per_received: ["<", 100], + per_received: ['<', 100], company: frm.doc.company } }); - }, __("Get Items From")); + }, __('Get Items From')); } }, diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html index 6b354b2fab6..bc34ad5ac50 100644 --- a/erpnext/templates/pages/order.html +++ b/erpnext/templates/pages/order.html @@ -34,16 +34,18 @@ -
-
-

- - {{ _("Pay") }} {{doc.get_formatted("grand_total") }} - -

+ {% if show_pay_button %} + -
+ {% endif %}
{% endblock %} diff --git a/erpnext/templates/pages/order.py b/erpnext/templates/pages/order.py index 185ec6615f6..13772d31295 100644 --- a/erpnext/templates/pages/order.py +++ b/erpnext/templates/pages/order.py @@ -55,6 +55,7 @@ def get_context(context): ) context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points")) + context.show_pay_button = frappe.db.get_single_value("Buying Settings", "show_pay_button") context.show_make_pi_button = False if context.doc.get("supplier"): # show Make Purchase Invoice button based on permission diff --git a/pyproject.toml b/pyproject.toml index 1b342a5fbb3..1c93eed6511 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,8 +10,8 @@ dynamic = ["version"] dependencies = [ # Core dependencies "pycountry~=20.7.3", - "python-stdnum~=1.16", "Unidecode~=1.2.0", + "barcodenumber~=0.5.0", # integration dependencies "gocardless-pro~=1.22.0",