diff --git a/CODEOWNERS b/CODEOWNERS index e406f8f56ee..c4ea16328e6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -4,7 +4,7 @@ # the repo. Unless a later match takes precedence, erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar -erpnext/assets/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar +erpnext/assets/ @anandbaburajan @deepeshgarg007 erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007 erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar @@ -16,6 +16,7 @@ erpnext/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/account/chart_of_accounts/chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py index 75f8f0645c9..9e67c4cf0d0 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py @@ -29,6 +29,7 @@ def create_charts( "root_type", "is_group", "tax_rate", + "account_currency", ]: account_number = cstr(child.get("account_number")).strip() @@ -95,7 +96,17 @@ def identify_is_group(child): is_group = child.get("is_group") elif len( set(child.keys()) - - set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"]) + - set( + [ + "account_name", + "account_type", + "root_type", + "is_group", + "tax_rate", + "account_number", + "account_currency", + ] + ) ): is_group = 1 else: @@ -185,6 +196,7 @@ def get_account_tree_from_existing_company(existing_company): "root_type", "tax_rate", "account_number", + "account_currency", ], order_by="lft, rgt", ) @@ -267,6 +279,7 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals "root_type", "is_group", "tax_rate", + "account_currency", ]: continue 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/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index 220b74727b9..cb7da179012 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -36,7 +36,7 @@ def validate_columns(data): no_of_columns = max([len(d) for d in data]) - if no_of_columns > 7: + if no_of_columns > 8: frappe.throw( _("More columns found than expected. Please compare the uploaded file with standard template"), title=(_("Wrong Template")), @@ -233,6 +233,7 @@ def build_forest(data): is_group, account_type, root_type, + account_currency, ) = i if not account_name: @@ -253,6 +254,8 @@ def build_forest(data): charts_map[account_name]["account_type"] = account_type if root_type: charts_map[account_name]["root_type"] = root_type + if account_currency: + charts_map[account_name]["account_currency"] = account_currency path = return_parent(data, account_name)[::-1] paths.append(path) # List of path is created line_no += 1 @@ -315,6 +318,7 @@ def get_template(template_type): "Is Group", "Account Type", "Root Type", + "Account Currency", ] writer = UnicodeWriter() writer.writerow(fields) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index d67d59b5d45..a4f6a74a5ab 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -211,8 +211,7 @@ class ExchangeRateRevaluation(Document): # Handle Accounts with '0' balance in Account/Base Currency for d in [x for x in account_details if x.zero_balance]: - # TODO: Set new balance in Base/Account currency - if d.balance > 0: + if d.balance != 0: current_exchange_rate = new_exchange_rate = 0 new_balance_in_account_currency = 0 # this will be '0' @@ -399,6 +398,9 @@ class ExchangeRateRevaluation(Document): journal_entry_accounts = [] for d in accounts: + if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")): + continue + dr_or_cr = ( "debit_in_account_currency" if d.get("balance_in_account_currency") > 0 @@ -448,7 +450,13 @@ class ExchangeRateRevaluation(Document): } ) - journal_entry_accounts.append( + journal_entry.set("accounts", journal_entry_accounts) + journal_entry.set_amounts_in_company_currency() + journal_entry.set_total_debit_credit() + + self.gain_loss_unbooked += journal_entry.difference - self.gain_loss_unbooked + journal_entry.append( + "accounts", { "account": unrealized_exchange_gain_loss_account, "balance": get_balance_on(unrealized_exchange_gain_loss_account), @@ -460,10 +468,9 @@ class ExchangeRateRevaluation(Document): "exchange_rate": 1, "reference_type": "Exchange Rate Revaluation", "reference_name": self.name, - } + }, ) - journal_entry.set("accounts", journal_entry_accounts) journal_entry.set_amounts_in_company_currency() journal_entry.set_total_debit_credit() journal_entry.save() 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.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 498fc7c295f..80e72226d3d 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -137,7 +137,8 @@ "fieldname": "finance_book", "fieldtype": "Link", "label": "Finance Book", - "options": "Finance Book" + "options": "Finance Book", + "read_only": 1 }, { "fieldname": "2_add_edit_gl_entries", @@ -538,7 +539,7 @@ "idx": 176, "is_submittable": 1, "links": [], - "modified": "2023-01-17 12:53:53.280620", + "modified": "2023-03-01 14:58:59.286591", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 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 fc837c75a30..7005c17362b 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -45,21 +45,20 @@ class PaymentRequest(Document): frappe.throw(_("To create a Payment Request reference document is required")) def validate_payment_request_amount(self): - existing_payment_request_amount = get_existing_payment_request_amount( - self.reference_doctype, self.reference_name + existing_payment_request_amount = flt( + get_existing_payment_request_amount(self.reference_doctype, self.reference_name) ) - if existing_payment_request_amount: - ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) - if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart": - ref_amount = get_amount(ref_doc, self.payment_account) + ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) + if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart": + ref_amount = get_amount(ref_doc, self.payment_account) - if existing_payment_request_amount + flt(self.grand_total) > ref_amount: - frappe.throw( - _("Total Payment Request amount cannot be greater than {0} amount").format( - self.reference_doctype - ) + if existing_payment_request_amount + flt(self.grand_total) > ref_amount: + frappe.throw( + _("Total Payment Request amount cannot be greater than {0} amount").format( + self.reference_doctype ) + ) def validate_currency(self): ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) @@ -496,26 +495,22 @@ def get_amount(ref_doc, payment_account=None): """get amount based on doctype""" dt = ref_doc.doctype if dt in ["Sales Order", "Purchase Order"]: - grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid) - + grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total) elif dt in ["Sales Invoice", "Purchase Invoice"]: if ref_doc.party_account_currency == ref_doc.currency: grand_total = flt(ref_doc.outstanding_amount) else: grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate - elif dt == "POS Invoice": for pay in ref_doc.payments: if pay.type == "Phone" and pay.account == payment_account: grand_total = pay.amount break - elif dt == "Fees": grand_total = ref_doc.outstanding_amount if grand_total > 0: return grand_total - else: frappe.throw(_("Payment Entry is already created")) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 477c726940c..4279aa4f85c 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -45,7 +45,10 @@ class TestPaymentRequest(unittest.TestCase): frappe.get_doc(method).insert(ignore_permissions=True) def test_payment_request_linkings(self): - so_inr = make_sales_order(currency="INR") + so_inr = make_sales_order(currency="INR", do_not_save=True) + so_inr.disable_rounded_total = 1 + so_inr.save() + pr = make_payment_request( dt="Sales Order", dn=so_inr.name, 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/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index a1239d64a01..b40649bbaec 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -161,7 +161,7 @@ class POSInvoice(SalesInvoice): bold_item_name = frappe.bold(item.item_name) bold_extra_batch_qty_needed = frappe.bold( - abs(available_batch_qty - reserved_batch_qty - item.qty) + abs(available_batch_qty - reserved_batch_qty - item.stock_qty) ) bold_invalid_batch_no = frappe.bold(item.batch_no) @@ -172,7 +172,7 @@ class POSInvoice(SalesInvoice): ).format(item.idx, bold_invalid_batch_no, bold_item_name), title=_("Item Unavailable"), ) - elif (available_batch_qty - reserved_batch_qty - item.qty) < 0: + elif (available_batch_qty - reserved_batch_qty - item.stock_qty) < 0: frappe.throw( _( "Row #{}: Batch No. {} of item {} has less than required stock available, {} more required" @@ -246,7 +246,7 @@ class POSInvoice(SalesInvoice): ), title=_("Item Unavailable"), ) - elif is_stock_item and flt(available_stock) < flt(d.qty): + elif is_stock_item and flt(available_stock) < flt(d.stock_qty): frappe.throw( _( "Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}." @@ -651,7 +651,7 @@ def get_bundle_availability(bundle_item_code, warehouse): item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse) available_qty = item_bin_qty - item_pos_reserved_qty - max_available_bundles = available_qty / item.qty + max_available_bundles = available_qty / item.stock_qty if bundle_bin_qty > max_available_bundles and frappe.get_value( "Item", item.item_code, "is_stock_item" ): 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..b79af71bef3 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) @@ -1485,11 +1485,35 @@ class PurchaseInvoice(BuyingController): if po_details: updated_pr += update_billed_amount_based_on_po(po_details, update_modified) + adjust_incoming_rate = frappe.db.get_single_value( + "Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate" + ) + for pr in set(updated_pr): from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage pr_doc = frappe.get_doc("Purchase Receipt", pr) - update_billing_percentage(pr_doc, update_modified=update_modified) + update_billing_percentage( + pr_doc, update_modified=update_modified, adjust_incoming_rate=adjust_incoming_rate + ) + + def get_pr_details_billed_amt(self): + # Get billed amount based on purchase receipt item reference (pr_detail) in purchase invoice + + pr_details_billed_amt = {} + pr_details = [d.get("pr_detail") for d in self.get("items") if d.get("pr_detail")] + if pr_details: + doctype = frappe.qb.DocType("Purchase Invoice Item") + query = ( + frappe.qb.from_(doctype) + .select(doctype.pr_detail, Sum(doctype.amount)) + .where(doctype.pr_detail.isin(pr_details) & doctype.docstatus == 1) + .groupby(doctype.pr_detail) + ) + + pr_details_billed_amt = frappe._dict(query.run(as_list=1)) + + return pr_details_billed_amt def on_recurring(self, reference_doc, auto_repeat_doc): self.due_date = None diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index f901257ccf6..a6d7df6971f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1523,6 +1523,94 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): company.enable_provisional_accounting_for_non_stock_items = 0 company.save() + def test_adjust_incoming_rate(self): + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) + + frappe.db.set_single_value( + "Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1 + ) + + # Increase the cost of the item + + pr = make_purchase_receipt(qty=1, rate=100) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 100) + + pi = create_purchase_invoice_from_receipt(pr.name) + for row in pi.items: + row.rate = 150 + + pi.save() + pi.submit() + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 150) + + # Reduce the cost of the item + + pr = make_purchase_receipt(qty=1, rate=100) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 100) + + pi = create_purchase_invoice_from_receipt(pr.name) + for row in pi.items: + row.rate = 50 + + pi.save() + pi.submit() + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 50) + + frappe.db.set_single_value( + "Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0 + ) + + # Don't adjust incoming rate + + pr = make_purchase_receipt(qty=1, rate=100) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 100) + + pi = create_purchase_invoice_from_receipt(pr.name) + for row in pi.items: + row.rate = 50 + + pi.save() + pi.submit() + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 100) + + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1) + def test_item_less_defaults(self): pi = frappe.new_doc("Purchase Invoice") 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/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index 43b95dca80e..58276970232 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -135,6 +135,34 @@ def get_assets(filters): where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and ads.asset = a.name and ads.docstatus=1 and ads.name = ds.parent and ifnull(ds.journal_entry, '') != '' group by a.asset_category union + SELECT a.asset_category, + ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then + gle.debit + else + 0 + end), 0) as accumulated_depreciation_as_on_from_date, + ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s + and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then + gle.debit + else + 0 + end), 0) as depreciation_eliminated_during_the_period, + ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s + and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then + gle.debit + else + 0 + end), 0) as depreciation_amount_during_the_period + from `tabGL Entry` gle + join `tabAsset` a on + gle.against_voucher = a.name + join `tabAsset Category Account` aca on + aca.parent = a.asset_category and aca.company_name = %(company)s + join `tabCompany` company on + company.name = %(company)s + where a.docstatus=1 and a.company=%(company)s and a.calculate_depreciation=0 and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) + group by a.asset_category + union SELECT a.asset_category, ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then 0 diff --git a/erpnext/accounts/report/general_ledger/general_ledger.html b/erpnext/accounts/report/general_ledger/general_ledger.html index 475be92add5..2d5ca497654 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.html +++ b/erpnext/accounts/report/general_ledger/general_ledger.html @@ -38,8 +38,11 @@ {% if(data[i].posting_date) { %} {%= frappe.datetime.str_to_user(data[i].posting_date) %} {%= data[i].voucher_type %} -
{%= data[i].voucher_no %} - +
{%= data[i].voucher_no %} + + {% var longest_word = cstr(data[i].remarks).split(" ").reduce((longest, word) => word.length > longest.length ? word : longest, ""); %} + 45 %} class="overflow-wrap-anywhere" {% endif %}> + {% if(!(filters.party || filters.account)) { %} {%= data[i].party || data[i].account %}
@@ -49,11 +52,14 @@ {% if(data[i].bill_no) { %}
{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %} {% } %} - - - {%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %} - - {%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %} +
+ + + {%= format_currency(data[i].debit, filters.presentation_currency) %} + + + {%= format_currency(data[i].credit, filters.presentation_currency) %} + {% } else { %} 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 4ed99f7e496..49513851361 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -209,62 +209,62 @@ frappe.ui.form.on('Asset', { return } - var x_intervals = [frm.doc.purchase_date]; + var x_intervals = [frappe.format(frm.doc.purchase_date, { fieldtype: 'Date' })]; var asset_values = [frm.doc.gross_purchase_amount]; - var last_depreciation_date = frm.doc.purchase_date; - if(frm.doc.opening_accumulated_depreciation) { - last_depreciation_date = frappe.datetime.add_months(frm.doc.next_depreciation_date, - -1*frm.doc.frequency_of_depreciation); - - x_intervals.push(last_depreciation_date); - asset_values.push(flt(frm.doc.gross_purchase_amount) - - flt(frm.doc.opening_accumulated_depreciation)); - } if(frm.doc.calculate_depreciation) { - if (frm.doc.finance_books.length == 1) { - let depr_schedule = (await frappe.call( - "erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_depr_schedule", - { - asset_name: frm.doc.name, - status: frm.doc.docstatus ? "Active" : "Draft", - finance_book: frm.doc.finance_books[0].finance_book || null - } - )).message; - - $.each(depr_schedule || [], function(i, v) { - x_intervals.push(v.schedule_date); - var asset_value = flt(frm.doc.gross_purchase_amount) - flt(v.accumulated_depreciation_amount); - if(v.journal_entry) { - last_depreciation_date = v.schedule_date; - asset_values.push(asset_value); - } else { - if (in_list(["Scrapped", "Sold"], frm.doc.status)) { - asset_values.push(null); - } else { - asset_values.push(asset_value) - } - } - }); + if(frm.doc.opening_accumulated_depreciation) { + var depreciation_date = frappe.datetime.add_months( + frm.doc.finance_books[0].depreciation_start_date, + -1 * frm.doc.finance_books[0].frequency_of_depreciation + ); + x_intervals.push(frappe.format(depreciation_date, { fieldtype: 'Date' })); + asset_values.push(flt(frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, precision('gross_purchase_amount'))); } + + let depr_schedule = (await frappe.call( + "erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_depr_schedule", + { + asset_name: frm.doc.name, + status: frm.doc.docstatus ? "Active" : "Draft", + finance_book: frm.doc.finance_books[0].finance_book || null + } + )).message; + + $.each(depr_schedule || [], function(i, v) { + x_intervals.push(frappe.format(v.schedule_date, { fieldtype: 'Date' })); + var asset_value = flt(frm.doc.gross_purchase_amount - v.accumulated_depreciation_amount, precision('gross_purchase_amount')); + if(v.journal_entry) { + asset_values.push(asset_value); + } else { + if (in_list(["Scrapped", "Sold"], frm.doc.status)) { + asset_values.push(null); + } else { + asset_values.push(asset_value) + } + } + }); } else { + if(frm.doc.opening_accumulated_depreciation) { + x_intervals.push(frappe.format(frm.doc.creation.split(" ")[0], { fieldtype: 'Date' })); + asset_values.push(flt(frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, precision('gross_purchase_amount'))); + } + let depr_entries = (await frappe.call({ method: "get_manual_depreciation_entries", doc: frm.doc, })).message; $.each(depr_entries || [], function(i, v) { - x_intervals.push(v.posting_date); - last_depreciation_date = v.posting_date; + x_intervals.push(frappe.format(v.posting_date, { fieldtype: 'Date' })); let last_asset_value = asset_values[asset_values.length - 1] - asset_values.push(last_asset_value - v.value); + asset_values.push(flt(last_asset_value - v.value, precision('gross_purchase_amount'))); }); } if(in_list(["Scrapped", "Sold"], frm.doc.status)) { - x_intervals.push(frm.doc.disposal_date); + x_intervals.push(frappe.format(frm.doc.disposal_date, { fieldtype: 'Date' })); asset_values.push(0); - last_depreciation_date = frm.doc.disposal_date; } frm.dashboard.render_graph({ diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 4f1cacaad50..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" @@ -429,25 +432,16 @@ class Asset(AccountsController): def get_value_after_depreciation(self, finance_book=None): if not self.calculate_depreciation: - return self.value_after_depreciation + return flt(self.value_after_depreciation, self.precision("gross_purchase_amount")) if not finance_book: - return self.get("finance_books")[0].value_after_depreciation + return flt( + self.get("finance_books")[0].value_after_depreciation, self.precision("gross_purchase_amount") + ) for row in self.get("finance_books"): if finance_book == row.finance_book: - return row.value_after_depreciation - - def _get_value_after_depreciation_for_making_schedule(self, fb_row): - # value_after_depreciation - current Asset value - if self.docstatus == 1 and fb_row.value_after_depreciation: - value_after_depreciation = flt(fb_row.value_after_depreciation) - else: - value_after_depreciation = flt(self.gross_purchase_amount) - flt( - self.opening_accumulated_depreciation - ) - - return value_after_depreciation + return flt(row.value_after_depreciation, self.precision("gross_purchase_amount")) def get_default_finance_book_idx(self): if not self.get("default_finance_book") and self.company: @@ -472,6 +466,7 @@ class Asset(AccountsController): .where(gle.debit != 0) .where(gle.is_cancelled == 0) .orderby(gle.posting_date) + .orderby(gle.creation) ).run(as_dict=True) return records 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_depreciation_schedule/asset_depreciation_schedule.js b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js index c28b2b3b6a3..3d2dff179aa 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js @@ -43,9 +43,9 @@ erpnext.asset.set_accumulated_depreciation = function(frm) { if(frm.doc.depreciation_method != "Manual") return; var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation); - $.each(frm.doc.schedules || [], function(i, row) { + + $.each(frm.doc.depreciation_schedule || [], function(i, row) { accumulated_depreciation += flt(row.depreciation_amount); - frappe.model.set_value(row.doctype, row.name, - "accumulated_depreciation_amount", accumulated_depreciation); + frappe.model.set_value(row.doctype, row.name, "accumulated_depreciation_amount", accumulated_depreciation); }) }; diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json index 898c4820791..d38508d0c42 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json @@ -10,7 +10,9 @@ "asset", "naming_series", "column_break_2", + "gross_purchase_amount", "opening_accumulated_depreciation", + "number_of_depreciations_booked", "finance_book", "finance_book_id", "depreciation_details_section", @@ -148,18 +150,36 @@ "read_only": 1 }, { - "depends_on": "opening_accumulated_depreciation", "fieldname": "opening_accumulated_depreciation", "fieldtype": "Currency", + "hidden": 1, "label": "Opening Accumulated Depreciation", "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "gross_purchase_amount", + "fieldtype": "Currency", + "hidden": 1, + "label": "Gross Purchase Amount", + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "number_of_depreciations_booked", + "fieldtype": "Int", + "hidden": 1, + "label": "Number of Depreciations Booked", + "print_hide": 1, "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-01-16 21:08:21.421260", + "modified": "2023-02-26 16:37:23.734806", "modified_by": "Administrator", "module": "Assets", "name": "Asset Depreciation Schedule", diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index 7615fbc86f9..b75fbcbeb3d 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -4,7 +4,15 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import add_days, add_months, cint, flt, get_last_day, is_last_day_of_the_month +from frappe.utils import ( + add_days, + add_months, + cint, + flt, + get_last_day, + getdate, + is_last_day_of_the_month, +) class AssetDepreciationSchedule(Document): @@ -83,15 +91,58 @@ class AssetDepreciationSchedule(Document): date_of_return=None, update_asset_finance_book_row=True, ): + have_asset_details_been_modified = self.have_asset_details_been_modified(asset_doc) + not_manual_depr_or_have_manual_depr_details_been_modified = ( + self.not_manual_depr_or_have_manual_depr_details_been_modified(row) + ) + self.set_draft_asset_depr_schedule_details(asset_doc, row) - self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row) - self.set_accumulated_depreciation(row, date_of_disposal, date_of_return) + + if self.should_prepare_depreciation_schedule( + have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified + ): + self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row) + self.set_accumulated_depreciation(row, date_of_disposal, date_of_return) + + def have_asset_details_been_modified(self, asset_doc): + return ( + asset_doc.gross_purchase_amount != self.gross_purchase_amount + or asset_doc.opening_accumulated_depreciation != self.opening_accumulated_depreciation + or asset_doc.number_of_depreciations_booked != self.number_of_depreciations_booked + ) + + def not_manual_depr_or_have_manual_depr_details_been_modified(self, row): + return ( + self.depreciation_method != "Manual" + or row.total_number_of_depreciations != self.total_number_of_depreciations + or row.frequency_of_depreciation != self.frequency_of_depreciation + or getdate(row.depreciation_start_date) != self.get("depreciation_schedule")[0].schedule_date + or row.expected_value_after_useful_life != self.expected_value_after_useful_life + ) + + def should_prepare_depreciation_schedule( + self, have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified + ): + if not self.get("depreciation_schedule"): + return True + + old_asset_depr_schedule_doc = self.get_doc_before_save() + + if self.docstatus != 0 and not old_asset_depr_schedule_doc: + return True + + if have_asset_details_been_modified or not_manual_depr_or_have_manual_depr_details_been_modified: + return True + + return False def set_draft_asset_depr_schedule_details(self, asset_doc, row): self.asset = asset_doc.name self.finance_book = row.finance_book self.finance_book_id = row.idx self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation + self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked + self.gross_purchase_amount = asset_doc.gross_purchase_amount self.depreciation_method = row.depreciation_method self.total_number_of_depreciations = row.total_number_of_depreciations self.frequency_of_depreciation = row.frequency_of_depreciation @@ -102,7 +153,7 @@ class AssetDepreciationSchedule(Document): def make_depr_schedule( self, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True ): - if row.depreciation_method != "Manual" and not self.get("depreciation_schedule"): + if not self.get("depreciation_schedule"): self.depreciation_schedule = [] if not asset_doc.available_for_use_date: @@ -134,7 +185,7 @@ class AssetDepreciationSchedule(Document): ): asset_doc.validate_asset_finance_books(row) - value_after_depreciation = asset_doc._get_value_after_depreciation_for_making_schedule(row) + value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row) row.value_after_depreciation = value_after_depreciation if update_asset_finance_book_row: @@ -293,7 +344,9 @@ class AssetDepreciationSchedule(Document): ignore_booked_entry=False, ): straight_line_idx = [ - d.idx for d in self.get("depreciation_schedule") if d.depreciation_method == "Straight Line" + d.idx + for d in self.get("depreciation_schedule") + if d.depreciation_method == "Straight Line" or d.depreciation_method == "Manual" ] accumulated_depreciation = flt(self.opening_accumulated_depreciation) @@ -325,6 +378,17 @@ class AssetDepreciationSchedule(Document): ) +def _get_value_after_depreciation_for_making_schedule(asset_doc, fb_row): + if asset_doc.docstatus == 1 and fb_row.value_after_depreciation: + value_after_depreciation = flt(fb_row.value_after_depreciation) + else: + value_after_depreciation = flt(asset_doc.gross_purchase_amount) - flt( + asset_doc.opening_accumulated_depreciation + ) + + return value_after_depreciation + + def make_draft_asset_depr_schedules_if_not_present(asset_doc): for row in asset_doc.get("finance_books"): draft_asset_depr_schedule_name = get_asset_depr_schedule_name( 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 cead72eae5a..59d43b1ea64 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.query_builder.functions import Sum -from frappe.utils import cstr, formatdate, getdate +from frappe.utils import cstr, flt, formatdate, getdate from erpnext.accounts.report.financial_statements import ( get_fiscal_year_data, @@ -102,13 +102,9 @@ def get_data(filters): ] assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields) - finance_book_filter = ("is", "not set") - if filters.finance_book: - finance_book_filter = ("=", filters.finance_book) - assets_linked_to_fb = frappe.db.get_all( doctype="Asset Finance Book", - filters={"finance_book": finance_book_filter}, + filters={"finance_book": filters.finance_book or ("is", "not set")}, pluck="parent", ) @@ -155,6 +151,7 @@ def prepare_chart_data(data, filters): filters.filter_based_on, "Monthly", company=filters.company, + ignore_fiscal_year=True, ) for d in period_list: @@ -194,7 +191,7 @@ def get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters): else: depr_amount = get_manual_depreciation_amount_of_asset(asset, filters) - return depr_amount + return flt(depr_amount, 2) def get_finance_book_value_map(filters): diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 34417f7ac3a..95857e4604d 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -18,9 +18,11 @@ "pr_required", "column_break_12", "maintain_same_rate", + "set_landed_cost_based_on_purchase_invoice_rate", "allow_multiple_items", "bill_for_rejected_quantity_in_purchase_invoice", "disable_last_purchase_rate", + "show_pay_button", "subcontract", "backflush_raw_materials_of_subcontract_based_on", "column_break_11", @@ -140,6 +142,20 @@ "fieldname": "disable_last_purchase_rate", "fieldtype": "Check", "label": "Disable Last Purchase Rate" + }, + { + "default": "1", + "fieldname": "show_pay_button", + "fieldtype": "Check", + "label": "Show Pay Button in Purchase Order Portal" + }, + { + "default": "0", + "depends_on": "eval: !doc.maintain_same_rate", + "description": "Users can enable the checkbox If they want to adjust the incoming rate (set using purchase receipt) based on the purchase invoice rate.", + "fieldname": "set_landed_cost_based_on_purchase_invoice_rate", + "fieldtype": "Check", + "label": "Set Landed Cost Based on Purchase Invoice Rate" } ], "icon": "fa fa-cog", @@ -147,7 +163,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-01-09 17:08:28.828173", + "modified": "2023-02-28 15:41:32.686805", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.py b/erpnext/buying/doctype/buying_settings/buying_settings.py index be1ebdeb64e..4680a889d3a 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.py +++ b/erpnext/buying/doctype/buying_settings/buying_settings.py @@ -21,3 +21,10 @@ class BuyingSettings(Document): self.get("supp_master_name") == "Naming Series", hide_name_field=False, ) + + def before_save(self): + self.check_maintain_same_rate() + + def check_maintain_same_rate(self): + if self.maintain_same_rate: + self.set_landed_cost_based_on_purchase_invoice_rate = 0 diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index a9f5afb2e98..2f0b7862a82 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -124,12 +124,11 @@ frappe.ui.form.on("Request for Quotation",{ frappe.urllib.get_full_url( "/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?" + new URLSearchParams({ - doctype: frm.doc.doctype, name: frm.doc.name, supplier: data.supplier, print_format: data.print_format || "Standard", language: data.language || frappe.boot.lang, - letter_head: data.letter_head || frm.doc.letter_head || "", + letterhead: data.letter_head || frm.doc.letter_head || "", }).toString() ) ); diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 8e9ded98421..7927beb8233 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -3,6 +3,7 @@ import json +from typing import Optional import frappe from frappe import _ @@ -388,24 +389,26 @@ def create_rfq_items(sq_doc, supplier, data): @frappe.whitelist() -def get_pdf(doctype, name, supplier, print_format=None, language=None, letter_head=None): - # permissions get checked in `download_pdf` - if doc := get_rfq_doc(doctype, name, supplier): - download_pdf( - doctype, - name, - print_format, - doc=doc, - language=language, - letter_head=letter_head or None, - ) - - -def get_rfq_doc(doctype, name, supplier): +def get_pdf( + name: str, + supplier: str, + print_format: Optional[str] = None, + language: Optional[str] = None, + letterhead: Optional[str] = None, +): + doc = frappe.get_doc("Request for Quotation", name) if supplier: - doc = frappe.get_doc(doctype, name) doc.update_supplier_part_no(supplier) - return doc + + # permissions get checked in `download_pdf` + download_pdf( + doc.doctype, + doc.name, + print_format, + doc=doc, + language=language, + letterhead=letterhead or None, + ) @frappe.whitelist() diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index 064b806e953..d250e6f18a9 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -8,6 +8,7 @@ from frappe.utils import nowdate from erpnext.buying.doctype.request_for_quotation.request_for_quotation import ( create_supplier_quotation, + get_pdf, make_supplier_quotation_from_rfq, ) from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq @@ -124,6 +125,11 @@ class TestRequestforQuotation(FrappeTestCase): rfq.status = "Draft" rfq.submit() + def test_get_pdf(self): + rfq = make_request_for_quotation() + get_pdf(rfq.name, rfq.get("suppliers")[0].supplier) + self.assertEqual(frappe.local.response.type, "pdf") + def make_request_for_quotation(**args): """ 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/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js index 6304a0908d0..9db769d59bf 100644 --- a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js +++ b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js @@ -22,14 +22,14 @@ frappe.query_reports["Subcontracted Item To Be Received"] = { fieldname:"from_date", label: __("From Date"), fieldtype: "Date", - default: frappe.datetime.add_months(frappe.datetime.month_start(), -1), + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), reqd: 1 }, { fieldname:"to_date", label: __("To Date"), fieldtype: "Date", - default: frappe.datetime.add_days(frappe.datetime.month_start(),-1), + default: frappe.datetime.get_today(), reqd: 1 }, ] diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js index b6739fe6632..7e5338f353b 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js @@ -22,14 +22,14 @@ frappe.query_reports["Subcontracted Raw Materials To Be Transferred"] = { fieldname:"from_date", label: __("From Date"), fieldtype: "Date", - default: frappe.datetime.add_months(frappe.datetime.month_start(), -1), + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), reqd: 1 }, { fieldname:"to_date", label: __("To Date"), fieldtype: "Date", - default: frappe.datetime.add_days(frappe.datetime.month_start(),-1), + default: frappe.datetime.get_today(), reqd: 1 }, ] 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/buying_controller.py b/erpnext/controllers/buying_controller.py index 4f7d9ad92e8..e15b61287eb 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -265,7 +265,10 @@ class BuyingController(SubcontractingController): ) / qty_in_stock_uom else: item.valuation_rate = ( - item.base_net_amount + item.item_tax_amount + flt(item.landed_cost_voucher_amount) + item.base_net_amount + + item.item_tax_amount + + flt(item.landed_cost_voucher_amount) + + flt(item.get("rate_difference_with_purchase_invoice")) ) / qty_in_stock_uom else: item.valuation_rate = 0.0 diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index fc6793a9bbc..15c270e58ad 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -131,7 +131,7 @@ def validate_returned_items(doc): ) elif ref.serial_no: - if not d.serial_no: + if d.qty and not d.serial_no: frappe.throw(_("Row # {0}: Serial No is mandatory").format(d.idx)) else: serial_nos = get_serial_nos(d.serial_no) @@ -252,7 +252,6 @@ def get_already_returned_items(doc): child.parent = par.name and par.docstatus = 1 and par.is_return = 1 and par.return_against = %s group by item_code - for update """.format( column, doc.doctype, doc.doctype ), @@ -401,6 +400,16 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): if serial_nos: target_doc.serial_no = "\n".join(serial_nos) + if source_doc.get("rejected_serial_no"): + returned_serial_nos = get_returned_serial_nos( + source_doc, source_parent, serial_no_field="rejected_serial_no" + ) + rejected_serial_nos = list( + set(get_serial_nos(source_doc.rejected_serial_no)) - set(returned_serial_nos) + ) + if rejected_serial_nos: + target_doc.rejected_serial_no = "\n".join(rejected_serial_nos) + if doctype in ["Purchase Receipt", "Subcontracting Receipt"]: returned_qty_map = get_returned_qty_map_for_row( source_parent.name, source_parent.supplier, source_doc.name, doctype @@ -611,7 +620,7 @@ def get_filters( return filters -def get_returned_serial_nos(child_doc, parent_doc): +def get_returned_serial_nos(child_doc, parent_doc, serial_no_field="serial_no"): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos return_ref_field = frappe.scrub(child_doc.doctype) @@ -620,7 +629,7 @@ def get_returned_serial_nos(child_doc, parent_doc): serial_nos = [] - fields = ["`{0}`.`serial_no`".format("tab" + child_doc.doctype)] + fields = [f"`{'tab' + child_doc.doctype}`.`{serial_no_field}`"] filters = [ [parent_doc.doctype, "return_against", "=", parent_doc.name], @@ -630,6 +639,6 @@ def get_returned_serial_nos(child_doc, parent_doc): ] for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters): - serial_nos.extend(get_serial_nos(row.serial_no)) + serial_nos.extend(get_serial_nos(row.get(serial_no_field))) return serial_nos diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 8b4d28bc7dd..3ea0216bb10 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -84,6 +84,9 @@ class SellingController(StockController): ) if not self.meta.get_field("sales_team"): party_details.pop("sales_team") + else: + self.set("sales_team", party_details.get("sales_team")) + self.update_if_missing(party_details) elif lead: @@ -136,7 +139,7 @@ class SellingController(StockController): self.in_words = money_in_words(amount, self.currency) def calculate_commission(self): - if not self.meta.get_field("commission_rate"): + if not self.meta.get_field("commission_rate") or self.docstatus.is_submitted(): return self.round_floats_in(self, ("amount_eligible_for_commission", "commission_rate")) 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/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 8c403aa9bfe..1edd7bf85e1 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -24,11 +24,19 @@ class calculate_taxes_and_totals(object): def __init__(self, doc: Document): self.doc = doc frappe.flags.round_off_applicable_accounts = [] + + self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items") + get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts) self.calculate() + def filter_rows(self): + """Exclude rows, that do not fulfill the filter criteria, from totals computation.""" + items = list(filter(lambda item: not item.get("is_alternative"), self.doc.get("items"))) + return items + def calculate(self): - if not len(self.doc.get("items")): + if not len(self._items): return self.discount_amount_applied = False @@ -70,7 +78,7 @@ class calculate_taxes_and_totals(object): if hasattr(self.doc, "tax_withholding_net_total"): sum_net_amount = 0 sum_base_net_amount = 0 - for item in self.doc.get("items"): + for item in self._items: if hasattr(item, "apply_tds") and item.apply_tds: sum_net_amount += item.net_amount sum_base_net_amount += item.base_net_amount @@ -79,7 +87,7 @@ class calculate_taxes_and_totals(object): self.doc.base_tax_withholding_net_total = sum_base_net_amount def validate_item_tax_template(self): - for item in self.doc.get("items"): + for item in self._items: if item.item_code and item.get("item_tax_template"): item_doc = frappe.get_cached_doc("Item", item.item_code) args = { @@ -137,7 +145,7 @@ class calculate_taxes_and_totals(object): return if not self.discount_amount_applied: - for item in self.doc.get("items"): + for item in self._items: self.doc.round_floats_in(item) if item.discount_percentage == 100: @@ -236,7 +244,7 @@ class calculate_taxes_and_totals(object): if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")): return - for item in self.doc.get("items"): + for item in self._items: item_tax_map = self._load_item_tax_rate(item.item_tax_rate) cumulated_tax_fraction = 0 total_inclusive_tax_amount_per_qty = 0 @@ -317,7 +325,7 @@ class calculate_taxes_and_totals(object): self.doc.total ) = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0 - for item in self.doc.get("items"): + for item in self._items: self.doc.total += item.amount self.doc.total_qty += item.qty self.doc.base_total += item.base_amount @@ -354,7 +362,7 @@ class calculate_taxes_and_totals(object): ] ) - for n, item in enumerate(self.doc.get("items")): + for n, item in enumerate(self._items): item_tax_map = self._load_item_tax_rate(item.item_tax_rate) for i, tax in enumerate(self.doc.get("taxes")): # tax_amount represents the amount of tax for the current step @@ -363,7 +371,7 @@ class calculate_taxes_and_totals(object): # Adjust divisional loss to the last item if tax.charge_type == "Actual": actual_tax_dict[tax.idx] -= current_tax_amount - if n == len(self.doc.get("items")) - 1: + if n == len(self._items) - 1: current_tax_amount += actual_tax_dict[tax.idx] # accumulate tax amount into tax.tax_amount @@ -391,7 +399,7 @@ class calculate_taxes_and_totals(object): ) # set precision in the last item iteration - if n == len(self.doc.get("items")) - 1: + if n == len(self._items) - 1: self.round_off_totals(tax) self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"]) @@ -570,7 +578,7 @@ class calculate_taxes_and_totals(object): def calculate_total_net_weight(self): if self.doc.meta.get_field("total_net_weight"): self.doc.total_net_weight = 0.0 - for d in self.doc.items: + for d in self._items: if d.total_weight: self.doc.total_net_weight += d.total_weight @@ -630,7 +638,7 @@ class calculate_taxes_and_totals(object): if total_for_discount_amount: # calculate item amount after Discount Amount - for i, item in enumerate(self.doc.get("items")): + for i, item in enumerate(self._items): distributed_amount = ( flt(self.doc.discount_amount) * item.net_amount / total_for_discount_amount ) @@ -643,7 +651,7 @@ class calculate_taxes_and_totals(object): self.doc.apply_discount_on == "Net Total" or not taxes or total_for_discount_amount == self.doc.net_total - ) and i == len(self.doc.get("items")) - 1: + ) and i == len(self._items) - 1: discount_amount_loss = flt( self.doc.net_total - net_total - self.doc.discount_amount, self.doc.precision("net_total") ) diff --git a/erpnext/crm/doctype/lead_source/lead_source.json b/erpnext/crm/doctype/lead_source/lead_source.json index 723c6d993d7..c3cedcc7a63 100644 --- a/erpnext/crm/doctype/lead_source/lead_source.json +++ b/erpnext/crm/doctype/lead_source/lead_source.json @@ -26,10 +26,11 @@ } ], "links": [], - "modified": "2021-02-08 12:51:48.971517", + "modified": "2023-02-10 00:51:44.973957", "modified_by": "Administrator", "module": "CRM", "name": "Lead Source", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -58,5 +59,7 @@ ], "quick_entry": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [], + "translated_doctype": 1 } \ No newline at end of file diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index 1f76a1ae2eb..b2617955a36 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -19,10 +19,6 @@ frappe.ui.form.on("Opportunity", { } } }); - - if (frm.doc.opportunity_from && frm.doc.party_name){ - frm.trigger('set_contact_link'); - } }, validate: function(frm) { @@ -130,6 +126,10 @@ frappe.ui.form.on("Opportunity", { } else { frappe.contacts.clear_address_and_contact(frm); } + + if (frm.doc.opportunity_from && frm.doc.party_name) { + frm.trigger('set_contact_link'); + } }, set_contact_link: function(frm) { @@ -137,6 +137,8 @@ frappe.ui.form.on("Opportunity", { frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Customer'} } else if(frm.doc.opportunity_from == "Lead" && frm.doc.party_name) { frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Lead'} + } else if (frm.doc.opportunity_from == "Prospect" && frm.doc.party_name) { + frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Prospect'} } }, diff --git a/erpnext/crm/doctype/sales_stage/sales_stage.json b/erpnext/crm/doctype/sales_stage/sales_stage.json index 77aa559b771..caf8ff5b36b 100644 --- a/erpnext/crm/doctype/sales_stage/sales_stage.json +++ b/erpnext/crm/doctype/sales_stage/sales_stage.json @@ -18,10 +18,11 @@ } ], "links": [], - "modified": "2020-05-20 12:22:01.866472", + "modified": "2023-02-10 01:40:23.713390", "modified_by": "Administrator", "module": "CRM", "name": "Sales Stage", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -40,5 +41,7 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1 + "states": [], + "track_changes": 1, + "translated_doctype": 1 } \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py index 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..dbfbcc9b385 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" }, @@ -362,7 +356,7 @@ auto_cancel_exempted_doctypes = [ scheduler_events = { "cron": { - "0/5 * * * *": [ + "0/15 * * * *": [ "erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs", ], "0/30 * * * *": [ diff --git a/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json b/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json index 158f143ae86..ba053555531 100644 --- a/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json +++ b/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json @@ -64,8 +64,6 @@ "fieldtype": "Section Break" }, { - "fetch_from": "prevdoc_detail_docname.sales_person", - "fetch_if_empty": 1, "fieldname": "service_person", "fieldtype": "Link", "in_list_view": 1, @@ -110,13 +108,15 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-05-27 17:47:21.474282", + "modified": "2023-02-27 11:09:33.114458", "modified_by": "Administrator", "module": "Maintenance", "name": "Maintenance Visit Purpose", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index c2b331fcfd1..db699b94d8f 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -289,7 +289,7 @@ { "fieldname": "scrap_items", "fieldtype": "Table", - "label": "Items", + "label": "Scrap Items", "options": "BOM Scrap Item" }, { @@ -605,7 +605,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-01-10 07:47:08.652616", + "modified": "2023-02-13 17:31:37.504565", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index c3f52d45833..51f7b24e745 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -212,7 +212,7 @@ def resume_bom_cost_update_jobs(): ["name", "boms_updated", "status"], ) incomplete_level = any(row.get("status") == "Pending" for row in bom_batches) - if not bom_batches or incomplete_level: + if not bom_batches or not incomplete_level: continue # Prep parent BOMs & updated processed BOMs for next level @@ -252,6 +252,9 @@ def get_processed_current_boms( current_boms = [] for row in bom_batches: + if not row.boms_updated: + continue + boms_updated = json.loads(row.boms_updated) current_boms.extend(boms_updated) boms_updated_dict = {bom: True for bom in boms_updated} diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 3133628cbf2..e82f37977cd 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -561,7 +561,34 @@ class JobCard(Document): ) def set_transferred_qty_in_job_card_item(self, ste_doc): - from frappe.query_builder.functions import Sum + def _get_job_card_items_transferred_qty(ste_doc): + from frappe.query_builder.functions import Sum + + job_card_items_transferred_qty = {} + job_card_items = [ + x.get("job_card_item") for x in ste_doc.get("items") if x.get("job_card_item") + ] + + if job_card_items: + se = frappe.qb.DocType("Stock Entry") + sed = frappe.qb.DocType("Stock Entry Detail") + + query = ( + frappe.qb.from_(sed) + .join(se) + .on(sed.parent == se.name) + .select(sed.job_card_item, Sum(sed.qty)) + .where( + (sed.job_card_item.isin(job_card_items)) + & (se.docstatus == 1) + & (se.purpose == "Material Transfer for Manufacture") + ) + .groupby(sed.job_card_item) + ) + + job_card_items_transferred_qty = frappe._dict(query.run(as_list=True)) + + return job_card_items_transferred_qty def _validate_over_transfer(row, transferred_qty): "Block over transfer of items if not allowed in settings." @@ -578,29 +605,23 @@ class JobCard(Document): exc=JobCardOverTransferError, ) - for row in ste_doc.items: - if not row.job_card_item: - continue - - sed = frappe.qb.DocType("Stock Entry Detail") - se = frappe.qb.DocType("Stock Entry") - transferred_qty = ( - frappe.qb.from_(sed) - .join(se) - .on(sed.parent == se.name) - .select(Sum(sed.qty)) - .where( - (sed.job_card_item == row.job_card_item) - & (se.docstatus == 1) - & (se.purpose == "Material Transfer for Manufacture") - ) - ).run()[0][0] + job_card_items_transferred_qty = _get_job_card_items_transferred_qty(ste_doc) + if job_card_items_transferred_qty: allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer") - if not allow_excess: - _validate_over_transfer(row, transferred_qty) - frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty)) + for row in ste_doc.items: + if not row.job_card_item: + continue + + transferred_qty = flt(job_card_items_transferred_qty.get(row.job_card_item)) + + if not allow_excess: + _validate_over_transfer(row, transferred_qty) + + frappe.db.set_value( + "Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty) + ) def set_transferred_qty(self, update_status=False): "Set total FG Qty in Job Card for which RM was transferred." 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/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py index cdf1541f888..3573a3a93d8 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py @@ -4,7 +4,8 @@ import frappe from frappe import _ -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import Floor, Sum +from frappe.utils import cint from pypika.terms import ExistsCriterion @@ -34,57 +35,55 @@ def get_columns(): def get_bom_stock(filters): - qty_to_produce = filters.get("qty_to_produce") or 1 - if int(qty_to_produce) < 0: - frappe.throw(_("Quantity to Produce can not be less than Zero")) + qty_to_produce = filters.get("qty_to_produce") + if cint(qty_to_produce) <= 0: + frappe.throw(_("Quantity to Produce should be greater than zero.")) if filters.get("show_exploded_view"): bom_item_table = "BOM Explosion Item" else: bom_item_table = "BOM Item" - bin = frappe.qb.DocType("Bin") - bom = frappe.qb.DocType("BOM") - bom_item = frappe.qb.DocType(bom_item_table) - - query = ( - frappe.qb.from_(bom) - .inner_join(bom_item) - .on(bom.name == bom_item.parent) - .left_join(bin) - .on(bom_item.item_code == bin.item_code) - .select( - bom_item.item_code, - bom_item.description, - bom_item.stock_qty, - bom_item.stock_uom, - (bom_item.stock_qty / bom.quantity) * qty_to_produce, - Sum(bin.actual_qty), - Sum(bin.actual_qty) / (bom_item.stock_qty / bom.quantity), - ) - .where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM")) - .groupby(bom_item.item_code) + warehouse_details = frappe.db.get_value( + "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 ) - if filters.get("warehouse"): - warehouse_details = frappe.db.get_value( - "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 - ) + BOM = frappe.qb.DocType("BOM") + BOM_ITEM = frappe.qb.DocType(bom_item_table) + BIN = frappe.qb.DocType("Bin") + WH = frappe.qb.DocType("Warehouse") + CONDITIONS = () - if warehouse_details: - wh = frappe.qb.DocType("Warehouse") - query = query.where( - ExistsCriterion( - frappe.qb.from_(wh) - .select(wh.name) - .where( - (wh.lft >= warehouse_details.lft) - & (wh.rgt <= warehouse_details.rgt) - & (bin.warehouse == wh.name) - ) - ) + if warehouse_details: + CONDITIONS = ExistsCriterion( + frappe.qb.from_(WH) + .select(WH.name) + .where( + (WH.lft >= warehouse_details.lft) + & (WH.rgt <= warehouse_details.rgt) + & (BIN.warehouse == WH.name) ) - else: - query = query.where(bin.warehouse == filters.get("warehouse")) + ) + else: + CONDITIONS = BIN.warehouse == filters.get("warehouse") - return query.run() + QUERY = ( + frappe.qb.from_(BOM) + .inner_join(BOM_ITEM) + .on(BOM.name == BOM_ITEM.parent) + .left_join(BIN) + .on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS)) + .select( + BOM_ITEM.item_code, + BOM_ITEM.description, + BOM_ITEM.stock_qty, + BOM_ITEM.stock_uom, + BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity, + Sum(BIN.actual_qty).as_("actual_qty"), + Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))), + ) + .where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM")) + .groupby(BOM_ITEM.item_code) + ) + + return QUERY.run() diff --git a/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py new file mode 100644 index 00000000000..1c56ebe24d4 --- /dev/null +++ b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py @@ -0,0 +1,108 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + + +import frappe +from frappe.exceptions import ValidationError +from frappe.tests.utils import FrappeTestCase +from frappe.utils import floor + +from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom +from erpnext.manufacturing.report.bom_stock_report.bom_stock_report import ( + get_bom_stock as bom_stock_report, +) +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + +class TestBomStockReport(FrappeTestCase): + def setUp(self): + self.warehouse = "_Test Warehouse - _TC" + self.fg_item, self.rm_items = create_items() + make_stock_entry(target=self.warehouse, item_code=self.rm_items[0], qty=20, basic_rate=100) + make_stock_entry(target=self.warehouse, item_code=self.rm_items[1], qty=40, basic_rate=200) + self.bom = make_bom(item=self.fg_item, quantity=1, raw_materials=self.rm_items, rm_qty=10) + + def test_bom_stock_report(self): + # Test 1: When `qty_to_produce` is 0. + filters = frappe._dict( + { + "bom": self.bom.name, + "warehouse": "Stores - _TC", + "qty_to_produce": 0, + } + ) + self.assertRaises(ValidationError, bom_stock_report, filters) + + # Test 2: When stock is not available. + data = bom_stock_report( + frappe._dict( + { + "bom": self.bom.name, + "warehouse": "Stores - _TC", + "qty_to_produce": 1, + } + ) + ) + expected_data = get_expected_data(self.bom, "Stores - _TC", 1) + self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) + + # Test 3: When stock is available. + data = bom_stock_report( + frappe._dict( + { + "bom": self.bom.name, + "warehouse": self.warehouse, + "qty_to_produce": 1, + } + ) + ) + expected_data = get_expected_data(self.bom, self.warehouse, 1) + self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) + + +def create_items(): + fg_item = make_item(properties={"is_stock_item": 1}).name + rm_item1 = make_item( + properties={ + "is_stock_item": 1, + "standard_rate": 100, + "opening_stock": 100, + "last_purchase_rate": 100, + } + ).name + rm_item2 = make_item( + properties={ + "is_stock_item": 1, + "standard_rate": 200, + "opening_stock": 200, + "last_purchase_rate": 200, + } + ).name + + return fg_item, [rm_item1, rm_item2] + + +def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False): + expected_data = [] + + for item in bom.get("exploded_items") if show_exploded_view else bom.get("items"): + in_stock_qty = frappe.get_cached_value( + "Bin", {"item_code": item.item_code, "warehouse": warehouse}, "actual_qty" + ) + + expected_data.append( + [ + item.item_code, + item.description, + item.stock_qty, + item.stock_uom, + item.stock_qty * qty_to_produce / bom.quantity, + in_stock_qty, + floor(in_stock_qty / (item.stock_qty * qty_to_produce / bom.quantity)) + if in_stock_qty + else None, + ] + ) + + return expected_data diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 52b1b05f029..0a0c7927f38 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 @@ -329,3 +325,6 @@ erpnext.patches.v14_0.set_pick_list_status erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance erpnext.patches.v14_0.update_closing_balances +# below 2 migration patches should always run last +erpnext.patches.v14_0.migrate_gl_to_payment_ledger +erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger diff --git a/erpnext/patches/v11_0/update_sales_partner_type.py b/erpnext/patches/v11_0/update_sales_partner_type.py index 2d37fd69b19..72fd424b245 100644 --- a/erpnext/patches/v11_0/update_sales_partner_type.py +++ b/erpnext/patches/v11_0/update_sales_partner_type.py @@ -1,16 +1,17 @@ import frappe -from frappe import _ def execute(): - from erpnext.setup.setup_wizard.operations.install_fixtures import default_sales_partner_type + from erpnext.setup.setup_wizard.operations.install_fixtures import read_lines frappe.reload_doc("selling", "doctype", "sales_partner_type") frappe.local.lang = frappe.db.get_default("lang") or "en" + default_sales_partner_type = read_lines("sales_partner_type.txt") + for s in default_sales_partner_type: - insert_sales_partner_type(_(s)) + insert_sales_partner_type(s) # get partner type in existing forms (customized) # and create a document if not created diff --git a/erpnext/patches/v13_0/create_ksa_vat_custom_fields.py b/erpnext/patches/v13_0/create_ksa_vat_custom_fields.py deleted file mode 100644 index 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/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py index 371ecbc8c13..5c46bf32807 100644 --- a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py +++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py @@ -27,7 +27,13 @@ def get_details_of_draft_or_submitted_depreciable_assets(): records = ( frappe.qb.from_(asset) - .select(asset.name, asset.opening_accumulated_depreciation, asset.docstatus) + .select( + asset.name, + asset.opening_accumulated_depreciation, + asset.gross_purchase_amount, + asset.number_of_depreciations_booked, + asset.docstatus, + ) .where(asset.calculate_depreciation == 1) .where(asset.docstatus < 2) ).run(as_dict=True) 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/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index a87c3ec9514..d1a55e6f424 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -91,6 +91,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } _calculate_taxes_and_totals() { + const is_quotation = this.frm.doc.doctype == "Quotation"; + this.frm.doc._items = is_quotation ? this.filtered_items() : this.frm.doc.items; + this.validate_conversion_rate(); this.calculate_item_values(); this.initialize_taxes(); @@ -122,7 +125,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { calculate_item_values() { var me = this; if (!this.discount_amount_applied) { - for (const item of this.frm.doc.items || []) { + for (const item of this.frm.doc._items || []) { frappe.model.round_floats_in(item); item.net_rate = item.rate; item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty; @@ -131,8 +134,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item)); } else { - let qty = item.qty || 1; - qty = me.frm.doc.is_return ? -1 * qty : qty; + // allow for '0' qty on Credit/Debit notes + let qty = item.qty || -1 item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item)); } @@ -206,7 +209,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { }); if(has_inclusive_tax==false) return; - $.each(me.frm.doc["items"] || [], function(n, item) { + $.each(me.frm.doc._items || [], function(n, item) { var item_tax_map = me._load_item_tax_rate(item.item_tax_rate); var cumulated_tax_fraction = 0.0; var total_inclusive_tax_amount_per_qty = 0; @@ -277,7 +280,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { var me = this; this.frm.doc.total_qty = this.frm.doc.total = this.frm.doc.base_total = this.frm.doc.net_total = this.frm.doc.base_net_total = 0.0; - $.each(this.frm.doc["items"] || [], function(i, item) { + $.each(this.frm.doc._items || [], function(i, item) { me.frm.doc.total += item.amount; me.frm.doc.total_qty += item.qty; me.frm.doc.base_total += item.base_amount; @@ -330,7 +333,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } }); - $.each(this.frm.doc["items"] || [], function(n, item) { + $.each(this.frm.doc._items || [], function(n, item) { var item_tax_map = me._load_item_tax_rate(item.item_tax_rate); $.each(me.frm.doc["taxes"] || [], function(i, tax) { // tax_amount represents the amount of tax for the current step @@ -339,7 +342,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { // Adjust divisional loss to the last item if (tax.charge_type == "Actual") { actual_tax_dict[tax.idx] -= current_tax_amount; - if (n == me.frm.doc["items"].length - 1) { + if (n == me.frm.doc._items.length - 1) { current_tax_amount += actual_tax_dict[tax.idx]; } } @@ -376,7 +379,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } // set precision in the last item iteration - if (n == me.frm.doc["items"].length - 1) { + if (n == me.frm.doc._items.length - 1) { me.round_off_totals(tax); me.set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"]); @@ -599,10 +602,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { _cleanup() { this.frm.doc.base_in_words = this.frm.doc.in_words = ""; + let items = this.frm.doc._items; - if(this.frm.doc["items"] && this.frm.doc["items"].length) { - if(!frappe.meta.get_docfield(this.frm.doc["items"][0].doctype, "item_tax_amount", this.frm.doctype)) { - $.each(this.frm.doc["items"] || [], function(i, item) { + if(items && items.length) { + if(!frappe.meta.get_docfield(items[0].doctype, "item_tax_amount", this.frm.doctype)) { + $.each(items || [], function(i, item) { delete item["item_tax_amount"]; }); } @@ -655,7 +659,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { var net_total = 0; // calculate item amount after Discount Amount if (total_for_discount_amount) { - $.each(this.frm.doc["items"] || [], function(i, item) { + $.each(this.frm.doc._items || [], function(i, item) { distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount; item.net_amount = flt(item.net_amount - distributed_amount, precision("base_amount", item)); @@ -663,7 +667,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { // discount amount rounding loss adjustment if no taxes if ((!(me.frm.doc.taxes || []).length || total_for_discount_amount==me.frm.doc.net_total || (me.frm.doc.apply_discount_on == "Net Total")) - && i == (me.frm.doc.items || []).length - 1) { + && i == (me.frm.doc._items || []).length - 1) { var discount_amount_loss = flt(me.frm.doc.net_total - net_total - me.frm.doc.discount_amount, precision("net_total")); item.net_amount = flt(item.net_amount + discount_amount_loss, @@ -892,4 +896,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } } + + filtered_items() { + return this.frm.doc.items.filter(item => !item["is_alternative"]); + } }; diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 09f2c5d5cb1..8d69ea0c994 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -488,7 +488,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe () => { var d = locals[cdt][cdn]; me.add_taxes_from_item_tax_template(d.item_tax_rate); - if (d.free_item_data) { + if (d.free_item_data && d.free_item_data.length > 0) { me.apply_product_discount(d); } }, @@ -1884,11 +1884,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe get_advances() { if(!this.frm.is_return) { + var me = this; return this.frm.call({ method: "set_advances", doc: this.frm.doc, callback: function(r, rt) { refresh_field("advances"); + me.frm.dirty(); } }) } 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/industry_type/industry_type.json b/erpnext/selling/doctype/industry_type/industry_type.json index 6c49f0f6dda..3c8ab8e47ae 100644 --- a/erpnext/selling/doctype/industry_type/industry_type.json +++ b/erpnext/selling/doctype/industry_type/industry_type.json @@ -1,123 +1,68 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:industry", - "beta": 0, - "creation": "2012-03-27 14:36:09", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:industry", + "creation": "2012-03-27 14:36:09", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "industry" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "industry", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Industry", - "length": 0, - "no_copy": 0, - "oldfieldname": "industry", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "industry", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Industry", + "oldfieldname": "industry", + "oldfieldtype": "Data", + "reqd": 1, + "unique": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-flag", - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2020-09-18 17:26:09.703215", - "modified_by": "Administrator", - "module": "Selling", - "name": "Industry Type", - "owner": "Administrator", + ], + "icon": "fa fa-flag", + "idx": 1, + "links": [], + "modified": "2023-02-10 03:14:40.735763", + "modified_by": "Administrator", + "module": "Selling", + "name": "Industry Type", + "naming_rule": "By fieldname", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User" + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Master Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Master Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "track_seen": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "translated_doctype": 1 } \ No newline at end of file diff --git a/erpnext/selling/doctype/party_specific_item/party_specific_item.json b/erpnext/selling/doctype/party_specific_item/party_specific_item.json index 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/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index b348bd35754..81ef44d53ed 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -90,7 +90,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. || frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) { this.frm.add_custom_button( __("Sales Order"), - this.frm.cscript["Make Sales Order"], + () => this.make_sales_order(), __("Create") ); } @@ -145,6 +145,20 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. } + make_sales_order() { + var me = this; + + let has_alternative_item = this.frm.doc.items.some((item) => item.is_alternative); + if (has_alternative_item) { + this.show_alternative_items_dialog(); + } else { + frappe.model.open_mapped_doc({ + method: "erpnext.selling.doctype.quotation.quotation.make_sales_order", + frm: me.frm + }); + } + } + set_dynamic_field_label(){ if (this.frm.doc.quotation_to == "Customer") { @@ -220,17 +234,111 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. } }) } + + show_alternative_items_dialog() { + let me = this; + + const table_fields = [ + { + fieldtype:"Data", + fieldname:"name", + label: __("Name"), + read_only: 1, + }, + { + fieldtype:"Link", + fieldname:"item_code", + options: "Item", + label: __("Item Code"), + read_only: 1, + in_list_view: 1, + columns: 2, + formatter: (value, df, options, doc) => { + return doc.is_alternative ? `${value}` : value; + } + }, + { + fieldtype:"Data", + fieldname:"description", + label: __("Description"), + in_list_view: 1, + read_only: 1, + }, + { + fieldtype:"Currency", + fieldname:"amount", + label: __("Amount"), + options: "currency", + in_list_view: 1, + read_only: 1, + }, + { + fieldtype:"Check", + fieldname:"is_alternative", + label: __("Is Alternative"), + read_only: 1, + }]; + + + this.data = this.frm.doc.items.filter( + (item) => item.is_alternative || item.has_alternative_item + ).map((item) => { + return { + "name": item.name, + "item_code": item.item_code, + "description": item.description, + "amount": item.amount, + "is_alternative": item.is_alternative, + } + }); + + const dialog = new frappe.ui.Dialog({ + title: __("Select Alternative Items for Sales Order"), + fields: [ + { + fieldname: "info", + fieldtype: "HTML", + read_only: 1 + }, + { + fieldname: "alternative_items", + fieldtype: "Table", + cannot_add_rows: true, + in_place_edit: true, + reqd: 1, + data: this.data, + description: __("Select an item from each set to be used in the Sales Order."), + get_data: () => { + return this.data; + }, + fields: table_fields + }, + ], + primary_action: function() { + frappe.model.open_mapped_doc({ + method: "erpnext.selling.doctype.quotation.quotation.make_sales_order", + frm: me.frm, + args: { + selected_items: dialog.fields_dict.alternative_items.grid.get_selected_children() + } + }); + dialog.hide(); + }, + primary_action_label: __('Continue') + }); + + dialog.fields_dict.info.$wrapper.html( + `

+ + Alternative Items +

` + ) + dialog.show(); + } }; cur_frm.script_manager.make(erpnext.selling.QuotationController); -cur_frm.cscript['Make Sales Order'] = function() { - frappe.model.open_mapped_doc({ - method: "erpnext.selling.doctype.quotation.quotation.make_sales_order", - frm: cur_frm - }) -} - frappe.ui.form.on("Quotation Item", "items_on_form_rendered", "packed_items_on_form_rendered", function(frm, cdt, cdn) { // enable tax_amount field if Actual }) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 063813b2dc7..fc66db20d29 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -35,6 +35,9 @@ class Quotation(SellingController): make_packing_list(self) + def before_submit(self): + self.set_has_alternative_item() + def validate_valid_till(self): if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date): frappe.throw(_("Valid till date cannot be before transaction date")) @@ -59,7 +62,18 @@ class Quotation(SellingController): title=_("Unpublished Item"), ) + def set_has_alternative_item(self): + """Mark 'Has Alternative Item' for rows.""" + if not any(row.is_alternative for row in self.get("items")): + return + + items_with_alternatives = self.get_rows_with_alternatives() + for row in self.get("items"): + if not row.is_alternative and row.name in items_with_alternatives: + row.has_alternative_item = 1 + def get_ordered_status(self): + status = "Open" ordered_items = frappe._dict( frappe.db.get_all( "Sales Order Item", @@ -70,16 +84,40 @@ class Quotation(SellingController): ) ) - status = "Open" - if ordered_items: + if not ordered_items: + return status + + has_alternatives = any(row.is_alternative for row in self.get("items")) + self._items = self.get_valid_items() if has_alternatives else self.get("items") + + if any(row.qty > ordered_items.get(row.item_code, 0.0) for row in self._items): + status = "Partially Ordered" + else: status = "Ordered" - for item in self.get("items"): - if item.qty > ordered_items.get(item.item_code, 0.0): - status = "Partially Ordered" - return status + def get_valid_items(self): + """ + Filters out items in an alternatives set that were not ordered. + """ + + def is_in_sales_order(row): + in_sales_order = bool( + frappe.db.exists( + "Sales Order Item", {"quotation_item": row.name, "item_code": row.item_code, "docstatus": 1} + ) + ) + return in_sales_order + + def can_map(row) -> bool: + if row.is_alternative or row.has_alternative_item: + return is_in_sales_order(row) + + return True + + return list(filter(can_map, self.get("items"))) + def is_fully_ordered(self): return self.get_ordered_status() == "Ordered" @@ -176,6 +214,22 @@ class Quotation(SellingController): def on_recurring(self, reference_doc, auto_repeat_doc): self.valid_till = None + def get_rows_with_alternatives(self): + rows_with_alternatives = [] + table_length = len(self.get("items")) + + for idx, row in enumerate(self.get("items")): + if row.is_alternative: + continue + + if idx == (table_length - 1): + break + + if self.get("items")[idx + 1].is_alternative: + rows_with_alternatives.append(row.name) + + return rows_with_alternatives + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context @@ -221,6 +275,8 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): ) ) + selected_rows = [x.get("name") for x in frappe.flags.get("args", {}).get("selected_items", [])] + def set_missing_values(source, target): if customer: target.customer = customer.name @@ -244,6 +300,24 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): target.blanket_order = obj.blanket_order target.blanket_order_rate = obj.blanket_order_rate + def can_map_row(item) -> bool: + """ + Row mapping from Quotation to Sales order: + 1. If no selections, map all non-alternative rows (that sum up to the grand total) + 2. If selections: Is Alternative Item/Has Alternative Item: Map if selected and adequate qty + 3. If selections: Simple row: Map if adequate qty + """ + has_qty = item.qty > 0 + + if not selected_rows: + return not item.is_alternative + + if selected_rows and (item.is_alternative or item.has_alternative_item): + return (item.name in selected_rows) and has_qty + + # Simple row + return has_qty + doclist = get_mapped_doc( "Quotation", source_name, @@ -253,7 +327,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): "doctype": "Sales Order Item", "field_map": {"parent": "prevdoc_docname", "name": "quotation_item"}, "postprocess": update_item, - "condition": lambda doc: doc.qty > 0, + "condition": can_map_row, }, "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, @@ -322,7 +396,11 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): source_name, { "Quotation": {"doctype": "Sales Invoice", "validation": {"docstatus": ["=", 1]}}, - "Quotation Item": {"doctype": "Sales Invoice Item", "postprocess": update_item}, + "Quotation Item": { + "doctype": "Sales Invoice Item", + "postprocess": update_item, + "condition": lambda row: not row.is_alternative, + }, "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, }, diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index cdf5f5d00c5..67f6518657e 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -457,6 +457,139 @@ class TestQuotation(FrappeTestCase): expected_index = id + 1 self.assertEqual(item.idx, expected_index) + def test_alternative_items_with_stock_items(self): + """ + Check if taxes & totals considers only non-alternative items with: + - One set of non-alternative & alternative items [first 3 rows] + - One simple stock item + """ + from erpnext.stock.doctype.item.test_item import make_item + + item_list = [] + stock_items = { + "_Test Simple Item 1": 100, + "_Test Alt 1": 120, + "_Test Alt 2": 110, + "_Test Simple Item 2": 200, + } + + for item, rate in stock_items.items(): + make_item(item, {"is_stock_item": 1}) + item_list.append( + { + "item_code": item, + "qty": 1, + "rate": rate, + "is_alternative": bool("Alt" in item), + } + ) + + quotation = make_quotation(item_list=item_list, do_not_submit=1) + quotation.append( + "taxes", + { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 10, + }, + ) + quotation.submit() + + self.assertEqual(quotation.net_total, 300) + self.assertEqual(quotation.grand_total, 330) + + def test_alternative_items_with_service_items(self): + """ + Check if taxes & totals considers only non-alternative items with: + - One set of non-alternative & alternative service items [first 3 rows] + - One simple non-alternative service item + All having the same item code and unique item name/description due to + dynamic services + """ + from erpnext.stock.doctype.item.test_item import make_item + + item_list = [] + service_items = { + "Tiling with Standard Tiles": 100, + "Alt Tiling with Durable Tiles": 150, + "Alt Tiling with Premium Tiles": 180, + "False Ceiling with Material #234": 190, + } + + make_item("_Test Dynamic Service Item", {"is_stock_item": 0}) + + for name, rate in service_items.items(): + item_list.append( + { + "item_code": "_Test Dynamic Service Item", + "item_name": name, + "description": name, + "qty": 1, + "rate": rate, + "is_alternative": bool("Alt" in name), + } + ) + + quotation = make_quotation(item_list=item_list, do_not_submit=1) + quotation.append( + "taxes", + { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 10, + }, + ) + quotation.submit() + + self.assertEqual(quotation.net_total, 290) + self.assertEqual(quotation.grand_total, 319) + + def test_alternative_items_sales_order_mapping_with_stock_items(self): + from erpnext.selling.doctype.quotation.quotation import make_sales_order + from erpnext.stock.doctype.item.test_item import make_item + + frappe.flags.args = frappe._dict() + item_list = [] + stock_items = { + "_Test Simple Item 1": 100, + "_Test Alt 1": 120, + "_Test Alt 2": 110, + "_Test Simple Item 2": 200, + } + + for item, rate in stock_items.items(): + make_item(item, {"is_stock_item": 1}) + item_list.append( + { + "item_code": item, + "qty": 1, + "rate": rate, + "is_alternative": bool("Alt" in item), + "warehouse": "_Test Warehouse - _TC", + } + ) + + quotation = make_quotation(item_list=item_list) + + frappe.flags.args.selected_items = [quotation.items[2]] + sales_order = make_sales_order(quotation.name) + sales_order.delivery_date = add_days(sales_order.transaction_date, 10) + sales_order.save() + + self.assertEqual(sales_order.items[0].item_code, "_Test Alt 2") + self.assertEqual(sales_order.items[1].item_code, "_Test Simple Item 2") + self.assertEqual(sales_order.net_total, 310) + + sales_order.submit() + quotation.reload() + self.assertEqual(quotation.status, "Ordered") + test_records = frappe.get_test_records("Quotation") diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index ca7dfd23378..f2aabc52400 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -49,6 +49,8 @@ "pricing_rules", "stock_uom_rate", "is_free_item", + "is_alternative", + "has_alternative_item", "section_break_43", "valuation_rate", "column_break_45", @@ -643,12 +645,28 @@ "no_copy": 1, "options": "currency", "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_alternative", + "fieldtype": "Check", + "label": "Is Alternative", + "print_hide": 1 + }, + { + "default": "0", + "fieldname": "has_alternative_item", + "fieldtype": "Check", + "hidden": 1, + "label": "Has Alternative Item", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-12-25 02:49:53.926625", + "modified": "2023-02-06 11:00:07.042364", "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item", @@ -656,5 +674,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index fb64772479b..449d461561a 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -275,7 +275,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex if (this.frm.doc.docstatus===0) { this.frm.add_custom_button(__('Quotation'), function() { - erpnext.utils.map_current_doc({ + let d = erpnext.utils.map_current_doc({ method: "erpnext.selling.doctype.quotation.quotation.make_sales_order", source_doctype: "Quotation", target: me.frm, @@ -293,7 +293,16 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex docstatus: 1, status: ["!=", "Lost"] } - }) + }); + + setTimeout(() => { + d.$parent.append(` + + ${__("Note: Please create Sales Orders from individual Quotations to select from among Alternative Items.")} + + `); + }, 200); + }, __("Get Items From")); } @@ -309,9 +318,12 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex make_work_order() { var me = this; - this.frm.call({ - doc: this.frm.doc, - method: 'get_work_order_items', + me.frm.call({ + method: "erpnext.selling.doctype.sales_order.sales_order.get_work_order_items", + args: { + sales_order: this.frm.docname, + }, + freeze: true, callback: function(r) { if(!r.message) { frappe.msgprint({ @@ -321,14 +333,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex }); return; } - else if(!r.message) { - frappe.msgprint({ - title: __('Work Order not created'), - message: __('Work Order already created for all items with BOM'), - indicator: 'orange' - }); - return; - } else { + else { const fields = [{ label: 'Items', fieldtype: 'Table', @@ -429,9 +434,9 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex make_raw_material_request() { var me = this; this.frm.call({ - doc: this.frm.doc, - method: 'get_work_order_items', + method: "erpnext.selling.doctype.sales_order.sales_order.get_work_order_items", args: { + sales_order: this.frm.docname, for_raw_material_request: 1 }, callback: function(r) { @@ -450,6 +455,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex } make_raw_material_request_dialog(r) { + var me = this; var fields = [ {fieldtype:'Check', fieldname:'include_exploded_items', label: __('Include Exploded Items')}, diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index ca6a51a6f36..385d0f3a585 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -6,11 +6,12 @@ import json import frappe import frappe.utils -from frappe import _ +from frappe import _, qb from frappe.contacts.doctype.address.address import get_company_address from frappe.desk.notifications import clear_doctype_notifications from frappe.model.mapper import get_mapped_doc from frappe.model.utils import get_fetch_values +from frappe.query_builder.functions import Sum from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, nowdate, strip_html from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( @@ -414,51 +415,6 @@ class SalesOrder(SellingController): self.indicator_color = "green" self.indicator_title = _("Paid") - @frappe.whitelist() - def get_work_order_items(self, for_raw_material_request=0): - """Returns items with BOM that already do not have a linked work order""" - items = [] - item_codes = [i.item_code for i in self.items] - product_bundle_parents = [ - pb.new_item_code - for pb in frappe.get_all( - "Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"] - ) - ] - - for table in [self.items, self.packed_items]: - for i in table: - bom = get_default_bom(i.item_code) - stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty - - if not for_raw_material_request: - total_work_order_qty = flt( - frappe.db.sql( - """select sum(qty) from `tabWork Order` - where production_item=%s and sales_order=%s and sales_order_item = %s and docstatus<2""", - (i.item_code, self.name, i.name), - )[0][0] - ) - pending_qty = stock_qty - total_work_order_qty - else: - pending_qty = stock_qty - - if pending_qty and i.item_code not in product_bundle_parents: - items.append( - dict( - name=i.name, - item_code=i.item_code, - description=i.description, - bom=bom or "", - warehouse=i.warehouse, - pending_qty=pending_qty, - required_qty=pending_qty if for_raw_material_request else 0, - sales_order_item=i.name, - ) - ) - - return items - def on_recurring(self, reference_doc, auto_repeat_doc): def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date): delivery_date = auto_repeat_doc.get_next_schedule_date(schedule_date=ref_doc_delivery_date) @@ -1350,3 +1306,57 @@ def update_produced_qty_in_so_item(sales_order, sales_order_item): return frappe.db.set_value("Sales Order Item", sales_order_item, "produced_qty", total_produced_qty) + + +@frappe.whitelist() +def get_work_order_items(sales_order, for_raw_material_request=0): + """Returns items with BOM that already do not have a linked work order""" + if sales_order: + so = frappe.get_doc("Sales Order", sales_order) + + wo = qb.DocType("Work Order") + + items = [] + item_codes = [i.item_code for i in so.items] + product_bundle_parents = [ + pb.new_item_code + for pb in frappe.get_all( + "Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"] + ) + ] + + for table in [so.items, so.packed_items]: + for i in table: + bom = get_default_bom(i.item_code) + stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty + + if not for_raw_material_request: + total_work_order_qty = flt( + qb.from_(wo) + .select(Sum(wo.qty)) + .where( + (wo.production_item == i.item_code) + & (wo.sales_order == so.name) * (wo.sales_order_item == i.name) + & (wo.docstatus.lte(2)) + ) + .run()[0][0] + ) + pending_qty = stock_qty - total_work_order_qty + else: + pending_qty = stock_qty + + if pending_qty and i.item_code not in product_bundle_parents: + items.append( + dict( + name=i.name, + item_code=i.item_code, + description=i.description, + bom=bom or "", + warehouse=i.warehouse, + pending_qty=pending_qty, + required_qty=pending_qty if for_raw_material_request else 0, + sales_order_item=i.name, + ) + ) + + return items diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index d4d7c58eb82..627914f0c7e 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1217,6 +1217,8 @@ class TestSalesOrder(FrappeTestCase): self.assertTrue(si.get("payment_schedule")) def test_make_work_order(self): + from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items + # Make a new Sales Order so = make_sales_order( **{ @@ -1230,7 +1232,7 @@ class TestSalesOrder(FrappeTestCase): # Raise Work Orders po_items = [] so_item_name = {} - for item in so.get_work_order_items(): + for item in get_work_order_items(so.name): po_items.append( { "warehouse": item.get("warehouse"), @@ -1448,6 +1450,7 @@ class TestSalesOrder(FrappeTestCase): from erpnext.controllers.item_variant import create_variant from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items make_item( # template item "Test-WO-Tshirt", @@ -1487,7 +1490,7 @@ class TestSalesOrder(FrappeTestCase): ] } ) - wo_items = so.get_work_order_items() + wo_items = get_work_order_items(so.name) self.assertEqual(wo_items[0].get("item_code"), "Test-WO-Tshirt-R") self.assertEqual(wo_items[0].get("bom"), red_var_bom.name) @@ -1497,6 +1500,8 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(wo_items[1].get("bom"), template_bom.name) def test_request_for_raw_materials(self): + from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items + item = make_item( "_Test Finished Item", { @@ -1529,7 +1534,7 @@ class TestSalesOrder(FrappeTestCase): so = make_sales_order(**{"item_list": [{"item_code": item.item_code, "qty": 1, "rate": 1000}]}) so.submit() mr_dict = frappe._dict() - items = so.get_work_order_items(1) + items = get_work_order_items(so.name, 1) mr_dict["items"] = items mr_dict["include_exploded_items"] = 0 mr_dict["ignore_existing_ordered_qty"] = 1 diff --git a/erpnext/selling/doctype/sales_partner_type/sales_partner_type.json b/erpnext/selling/doctype/sales_partner_type/sales_partner_type.json index e7dd0d84a0a..a9b500a625f 100644 --- a/erpnext/selling/doctype/sales_partner_type/sales_partner_type.json +++ b/erpnext/selling/doctype/sales_partner_type/sales_partner_type.json @@ -1,94 +1,47 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:sales_partner_type", - "beta": 0, - "creation": "2018-06-11 13:15:57.404716", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "autoname": "field:sales_partner_type", + "creation": "2018-06-11 13:15:57.404716", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "sales_partner_type" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sales_partner_type", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Sales Partner Type", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "sales_partner_type", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Sales Partner Type", + "reqd": 1, + "unique": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-06-11 13:45:13.554307", - "modified_by": "Administrator", - "module": "Selling", - "name": "Sales Partner Type", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2023-02-10 01:00:20.110800", + "modified_by": "Administrator", + "module": "Selling", + "name": "Sales Partner Type", + "naming_rule": "By fieldname", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "translated_doctype": 1 } \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index c442774d0f7..46320e5538f 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -522,7 +522,7 @@ erpnext.PointOfSale.Controller = class { const from_selector = field === 'qty' && value === "+1"; if (from_selector) - value = flt(item_row.qty) + flt(value); + value = flt(item_row.stock_qty) + flt(value); if (item_row_exists) { if (field === 'qty') diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py index 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/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index 63d339a839d..29691230f22 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -175,7 +175,9 @@ def prepare_data(data, so_elapsed_time, filters): # update existing entry so_row = sales_order_map[so_name] so_row["required_date"] = max(getdate(so_row["delivery_date"]), getdate(row["delivery_date"])) - so_row["delay"] = min(so_row["delay"], row["delay"]) + so_row["delay"] = ( + min(so_row["delay"], row["delay"]) if row["delay"] and so_row["delay"] else so_row["delay"] + ) # sum numeric columns fields = [ diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 8ff01f5cb4c..f1df3a11de4 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -253,7 +253,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran } calculate_commission() { - if(!this.frm.fields_dict.commission_rate) return; + if(!this.frm.fields_dict.commission_rate || this.frm.doc.docstatus === 1) return; if(this.frm.doc.commission_rate > 100) { this.frm.set_value("commission_rate", 100); @@ -418,8 +418,6 @@ 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/designation/designation.json b/erpnext/setup/doctype/designation/designation.json index 2cbbb04ed91..a5b2ac9128a 100644 --- a/erpnext/setup/doctype/designation/designation.json +++ b/erpnext/setup/doctype/designation/designation.json @@ -31,7 +31,7 @@ "icon": "fa fa-bookmark", "idx": 1, "links": [], - "modified": "2022-06-28 17:10:26.853753", + "modified": "2023-02-10 01:53:41.319386", "modified_by": "Administrator", "module": "Setup", "name": "Designation", @@ -58,5 +58,6 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "ASC", - "states": [] + "states": [], + "translated_doctype": 1 } \ No newline at end of file diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 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/setup/setup_wizard/data/designation.txt b/erpnext/setup/setup_wizard/data/designation.txt new file mode 100644 index 00000000000..4c6d7bdea8a --- /dev/null +++ b/erpnext/setup/setup_wizard/data/designation.txt @@ -0,0 +1,31 @@ +Accountant +Administrative Assistant +Administrative Officer +Analyst +Associate +Business Analyst +Business Development Manager +Consultant +Chief Executive Officer +Chief Financial Officer +Chief Operating Officer +Chief Technology Officer +Customer Service Representative +Designer +Engineer +Executive Assistant +Finance Manager +HR Manager +Head of Marketing and Sales +Manager +Managing Director +Marketing Manager +Marketing Specialist +President +Product Manager +Project Manager +Researcher +Sales Representative +Secretary +Software Developer +Vice President diff --git a/erpnext/setup/setup_wizard/data/industry_type.py b/erpnext/setup/setup_wizard/data/industry_type.py deleted file mode 100644 index 0bc3f32eb09..00000000000 --- a/erpnext/setup/setup_wizard/data/industry_type.py +++ /dev/null @@ -1,57 +0,0 @@ -from frappe import _ - - -def get_industry_types(): - return [ - _("Accounting"), - _("Advertising"), - _("Aerospace"), - _("Agriculture"), - _("Airline"), - _("Apparel & Accessories"), - _("Automotive"), - _("Banking"), - _("Biotechnology"), - _("Broadcasting"), - _("Brokerage"), - _("Chemical"), - _("Computer"), - _("Consulting"), - _("Consumer Products"), - _("Cosmetics"), - _("Defense"), - _("Department Stores"), - _("Education"), - _("Electronics"), - _("Energy"), - _("Entertainment & Leisure"), - _("Executive Search"), - _("Financial Services"), - _("Food, Beverage & Tobacco"), - _("Grocery"), - _("Health Care"), - _("Internet Publishing"), - _("Investment Banking"), - _("Legal"), - _("Manufacturing"), - _("Motion Picture & Video"), - _("Music"), - _("Newspaper Publishers"), - _("Online Auctions"), - _("Pension Funds"), - _("Pharmaceuticals"), - _("Private Equity"), - _("Publishing"), - _("Real Estate"), - _("Retail & Wholesale"), - _("Securities & Commodity Exchanges"), - _("Service"), - _("Soap & Detergent"), - _("Software"), - _("Sports"), - _("Technology"), - _("Telecommunications"), - _("Television"), - _("Transportation"), - _("Venture Capital"), - ] diff --git a/erpnext/setup/setup_wizard/data/industry_type.txt b/erpnext/setup/setup_wizard/data/industry_type.txt new file mode 100644 index 00000000000..eadc689e312 --- /dev/null +++ b/erpnext/setup/setup_wizard/data/industry_type.txt @@ -0,0 +1,51 @@ +Accounting +Advertising +Aerospace +Agriculture +Airline +Apparel & Accessories +Automotive +Banking +Biotechnology +Broadcasting +Brokerage +Chemical +Computer +Consulting +Consumer Products +Cosmetics +Defense +Department Stores +Education +Electronics +Energy +Entertainment & Leisure +Executive Search +Financial Services +Food, Beverage & Tobacco +Grocery +Health Care +Internet Publishing +Investment Banking +Legal +Manufacturing +Motion Picture & Video +Music +Newspaper Publishers +Online Auctions +Pension Funds +Pharmaceuticals +Private Equity +Publishing +Real Estate +Retail & Wholesale +Securities & Commodity Exchanges +Service +Soap & Detergent +Software +Sports +Technology +Telecommunications +Television +Transportation +Venture Capital diff --git a/erpnext/setup/setup_wizard/data/lead_source.txt b/erpnext/setup/setup_wizard/data/lead_source.txt new file mode 100644 index 00000000000..00ca1808bb5 --- /dev/null +++ b/erpnext/setup/setup_wizard/data/lead_source.txt @@ -0,0 +1,10 @@ +Existing Customer +Reference +Advertisement +Cold Calling +Exhibition +Supplier Reference +Mass Mailing +Customer's Vendor +Campaign +Walk In diff --git a/erpnext/setup/setup_wizard/data/sales_partner_type.txt b/erpnext/setup/setup_wizard/data/sales_partner_type.txt new file mode 100644 index 00000000000..68e9b9ac732 --- /dev/null +++ b/erpnext/setup/setup_wizard/data/sales_partner_type.txt @@ -0,0 +1,7 @@ +Channel Partner +Distributor +Dealer +Agent +Retailer +Implementation Partner +Reseller diff --git a/erpnext/setup/setup_wizard/data/sales_stage.txt b/erpnext/setup/setup_wizard/data/sales_stage.txt new file mode 100644 index 00000000000..2808ce79855 --- /dev/null +++ b/erpnext/setup/setup_wizard/data/sales_stage.txt @@ -0,0 +1,8 @@ +Prospecting +Qualification +Needs Analysis +Value Proposition +Identifying Decision Makers +Perception Analysis +Proposal/Price Quote +Negotiation/Review diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 1f8c0d6a1ca..6bc17718ae0 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -4,6 +4,7 @@ import json import os +from pathlib import Path import frappe from frappe import _ @@ -16,28 +17,10 @@ from frappe.utils import cstr, getdate from erpnext.accounts.doctype.account.account import RootNotEditable from erpnext.regional.address_template.setup import set_up_address_templates -default_lead_sources = [ - "Existing Customer", - "Reference", - "Advertisement", - "Cold Calling", - "Exhibition", - "Supplier Reference", - "Mass Mailing", - "Customer's Vendor", - "Campaign", - "Walk In", -] -default_sales_partner_type = [ - "Channel Partner", - "Distributor", - "Dealer", - "Agent", - "Retailer", - "Implementation Partner", - "Reseller", -] +def read_lines(filename: str) -> list[str]: + """Return a list of lines from a file in the data directory.""" + return (Path(__file__).parent.parent / "data" / filename).read_text().splitlines() def install(country=None): @@ -85,7 +68,11 @@ def install(country=None): # Stock Entry Type {"doctype": "Stock Entry Type", "name": "Material Issue", "purpose": "Material Issue"}, {"doctype": "Stock Entry Type", "name": "Material Receipt", "purpose": "Material Receipt"}, - {"doctype": "Stock Entry Type", "name": "Material Transfer", "purpose": "Material Transfer"}, + { + "doctype": "Stock Entry Type", + "name": "Material Transfer", + "purpose": "Material Transfer", + }, {"doctype": "Stock Entry Type", "name": "Manufacture", "purpose": "Manufacture"}, {"doctype": "Stock Entry Type", "name": "Repack", "purpose": "Repack"}, { @@ -103,22 +90,6 @@ def install(country=None): "name": "Material Consumption for Manufacture", "purpose": "Material Consumption for Manufacture", }, - # Designation - {"doctype": "Designation", "designation_name": _("CEO")}, - {"doctype": "Designation", "designation_name": _("Manager")}, - {"doctype": "Designation", "designation_name": _("Analyst")}, - {"doctype": "Designation", "designation_name": _("Engineer")}, - {"doctype": "Designation", "designation_name": _("Accountant")}, - {"doctype": "Designation", "designation_name": _("Secretary")}, - {"doctype": "Designation", "designation_name": _("Associate")}, - {"doctype": "Designation", "designation_name": _("Administrative Officer")}, - {"doctype": "Designation", "designation_name": _("Business Development Manager")}, - {"doctype": "Designation", "designation_name": _("HR Manager")}, - {"doctype": "Designation", "designation_name": _("Project Manager")}, - {"doctype": "Designation", "designation_name": _("Head of Marketing and Sales")}, - {"doctype": "Designation", "designation_name": _("Software Developer")}, - {"doctype": "Designation", "designation_name": _("Designer")}, - {"doctype": "Designation", "designation_name": _("Researcher")}, # territory: with two default territories, one for home country and one named Rest of the World { "doctype": "Territory", @@ -291,28 +262,18 @@ def install(country=None): {"doctype": "Market Segment", "market_segment": _("Lower Income")}, {"doctype": "Market Segment", "market_segment": _("Middle Income")}, {"doctype": "Market Segment", "market_segment": _("Upper Income")}, - # Sales Stages - {"doctype": "Sales Stage", "stage_name": _("Prospecting")}, - {"doctype": "Sales Stage", "stage_name": _("Qualification")}, - {"doctype": "Sales Stage", "stage_name": _("Needs Analysis")}, - {"doctype": "Sales Stage", "stage_name": _("Value Proposition")}, - {"doctype": "Sales Stage", "stage_name": _("Identifying Decision Makers")}, - {"doctype": "Sales Stage", "stage_name": _("Perception Analysis")}, - {"doctype": "Sales Stage", "stage_name": _("Proposal/Price Quote")}, - {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")}, # Warehouse Type {"doctype": "Warehouse Type", "name": "Transit"}, ] - from erpnext.setup.setup_wizard.data.industry_type import get_industry_types - - records += [{"doctype": "Industry Type", "industry": d} for d in get_industry_types()] - # records += [{"doctype":"Operation", "operation": d} for d in get_operations()] - records += [{"doctype": "Lead Source", "source_name": _(d)} for d in default_lead_sources] - - records += [ - {"doctype": "Sales Partner Type", "sales_partner_type": _(d)} for d in default_sales_partner_type - ] + for doctype, title_field, filename in ( + ("Designation", "designation_name", "designation.txt"), + ("Sales Stage", "stage_name", "sales_stage.txt"), + ("Industry Type", "industry", "industry_type.txt"), + ("Lead Source", "source_name", "lead_source.txt"), + ("Sales Partner Type", "sales_partner_type", "sales_partner_type.txt"), + ): + records += [{"doctype": doctype, title_field: title} for title in read_lines(filename)] base_path = frappe.get_app_path("erpnext", "stock", "doctype") response = frappe.read_file( @@ -397,7 +358,8 @@ def add_uom_data(): frappe.get_doc({"doctype": "UOM Category", "category_name": _(d.get("category"))}).db_insert() if not frappe.db.exists( - "UOM Conversion Factor", {"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))} + "UOM Conversion Factor", + {"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))}, ): frappe.get_doc( { @@ -535,7 +497,8 @@ def create_bank_account(args): company_name = args.get("company_name") bank_account_group = frappe.db.get_value( - "Account", {"account_type": "Bank", "is_group": 1, "root_type": "Asset", "company": company_name} + "Account", + {"account_type": "Bank", "is_group": 1, "root_type": "Asset", "company": company_name}, ) if bank_account_group: bank_account = frappe.get_doc( diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 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.js b/erpnext/stock/doctype/item/item.js index 5bcb05aa988..9a9ddf44044 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -33,6 +33,9 @@ frappe.ui.form.on("Item", { 'Material Request': () => { open_form(frm, "Material Request", "Material Request Item", "items"); }, + 'Stock Entry': () => { + open_form(frm, "Stock Entry", "Stock Entry Detail", "items"); + }, }; }, @@ -893,6 +896,9 @@ function open_form(frm, doctype, child_doctype, parentfield) { new_child_doc.item_name = frm.doc.item_name; new_child_doc.uom = frm.doc.stock_uom; new_child_doc.description = frm.doc.description; + if (!new_child_doc.qty) { + new_child_doc.qty = 1.0; + } frappe.run_serially([ () => frappe.ui.form.make_quick_entry(doctype, null, null, new_doc), diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 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/item_price/item_price.js b/erpnext/stock/doctype/item_price/item_price.js index 12cf6cf84d5..ce489ff52b4 100644 --- a/erpnext/stock/doctype/item_price/item_price.js +++ b/erpnext/stock/doctype/item_price/item_price.js @@ -2,7 +2,18 @@ // License: GNU General Public License v3. See license.txt frappe.ui.form.on("Item Price", { - onload: function (frm) { + setup(frm) { + frm.set_query("item_code", function() { + return { + filters: { + "disabled": 0, + "has_variants": 0 + } + }; + }); + }, + + onload(frm) { // Fetch price list details frm.add_fetch("price_list", "buying", "buying"); frm.add_fetch("price_list", "selling", "selling"); diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index bcd31ada83e..54d1ae634f5 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -3,7 +3,7 @@ import frappe -from frappe import _ +from frappe import _, bold from frappe.model.document import Document from frappe.query_builder import Criterion from frappe.query_builder.functions import Cast_ @@ -21,6 +21,7 @@ class ItemPrice(Document): self.update_price_list_details() self.update_item_details() self.check_duplicates() + self.validate_item_template() def validate_item(self): if not frappe.db.exists("Item", self.item_code): @@ -49,6 +50,12 @@ class ItemPrice(Document): "Item", self.item_code, ["item_name", "description"] ) + def validate_item_template(self): + if frappe.get_cached_value("Item", self.item_code, "has_variants"): + msg = f"Item Price cannot be created for the template item {bold(self.item_code)}" + + frappe.throw(_(msg)) + def check_duplicates(self): item_price = frappe.qb.DocType("Item Price") diff --git a/erpnext/stock/doctype/item_price/test_item_price.py b/erpnext/stock/doctype/item_price/test_item_price.py index 30d933e247d..8fd4938fa35 100644 --- a/erpnext/stock/doctype/item_price/test_item_price.py +++ b/erpnext/stock/doctype/item_price/test_item_price.py @@ -16,6 +16,28 @@ class TestItemPrice(FrappeTestCase): frappe.db.sql("delete from `tabItem Price`") make_test_records_for_doctype("Item Price", force=True) + def test_template_item_price(self): + from erpnext.stock.doctype.item.test_item import make_item + + item = make_item( + "Test Template Item 1", + { + "has_variants": 1, + "variant_based_on": "Manufacturer", + }, + ) + + doc = frappe.get_doc( + { + "doctype": "Item Price", + "price_list": "_Test Price List", + "item_code": item.name, + "price_list_rate": 100, + } + ) + + self.assertRaises(frappe.ValidationError, doc.save) + def test_duplicate_item(self): doc = frappe.copy_doc(test_records[0]) self.assertRaises(ItemPriceDuplicateItem, doc.save) diff --git a/erpnext/stock/doctype/item_price/test_records.json b/erpnext/stock/doctype/item_price/test_records.json index 0a3d7e81985..afe5ad65b75 100644 --- a/erpnext/stock/doctype/item_price/test_records.json +++ b/erpnext/stock/doctype/item_price/test_records.json @@ -38,5 +38,19 @@ "price_list_rate": 1000, "valid_from": "2017-04-10", "valid_upto": "2017-04-17" + }, + { + "doctype": "Item Price", + "item_code": "_Test Item", + "price_list": "_Test Buying Price List", + "price_list_rate": 100, + "supplier": "_Test Supplier" + }, + { + "doctype": "Item Price", + "item_code": "_Test Item", + "price_list": "_Test Selling Price List", + "price_list_rate": 200, + "customer": "_Test Customer" } ] diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 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/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 156e5917f23..c1f1b0d1352 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -110,8 +110,11 @@ frappe.ui.form.on('Material Request', { if (frm.doc.material_request_type === "Material Transfer") { add_create_pick_list_button(); - frm.add_custom_button(__("Transfer Material"), + frm.add_custom_button(__("Material Transfer"), () => frm.events.make_stock_entry(frm), __('Create')); + + frm.add_custom_button(__("Material Transfer (In Transit)"), + () => frm.events.make_in_transit_stock_entry(frm), __('Create')); } if (frm.doc.material_request_type === "Material Issue") { @@ -333,6 +336,46 @@ frappe.ui.form.on('Material Request', { }); }, + make_in_transit_stock_entry(frm) { + frappe.prompt( + [ + { + label: __('In Transit Warehouse'), + fieldname: 'in_transit_warehouse', + fieldtype: 'Link', + options: 'Warehouse', + reqd: 1, + get_query: () => { + return{ + filters: { + 'company': frm.doc.company, + 'is_group': 0, + 'warehouse_type': 'Transit' + } + } + } + } + ], + (values) => { + frappe.call({ + method: "erpnext.stock.doctype.material_request.material_request.make_in_transit_stock_entry", + args: { + source_name: frm.doc.name, + in_transit_warehouse: values.in_transit_warehouse + }, + callback: function(r) { + if (r.message) { + let doc = frappe.model.sync(r.message); + frappe.set_route('Form', doc[0].doctype, doc[0].name); + } + } + }) + }, + __('In Transit Transfer'), + __("Create Stock Entry") + ) + }, + create_pick_list: (frm) => { frappe.model.open_mapped_doc({ method: "erpnext.stock.doctype.material_request.material_request.create_pick_list", diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 94f63a599b5..8aeb7511f41 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -10,6 +10,7 @@ import json import frappe from frappe import _, msgprint from frappe.model.mapper import get_mapped_doc +from frappe.query_builder.functions import Sum from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items @@ -180,6 +181,34 @@ class MaterialRequest(BuyingController): self.update_requested_qty() self.update_requested_qty_in_production_plan() + def get_mr_items_ordered_qty(self, mr_items): + mr_items_ordered_qty = {} + mr_items = [d.name for d in self.get("items") if d.name in mr_items] + + doctype = qty_field = None + if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"): + doctype = frappe.qb.DocType("Stock Entry Detail") + qty_field = doctype.transfer_qty + elif self.material_request_type == "Manufacture": + doctype = frappe.qb.DocType("Work Order") + qty_field = doctype.qty + + if doctype and qty_field: + query = ( + frappe.qb.from_(doctype) + .select(doctype.material_request_item, Sum(qty_field)) + .where( + (doctype.material_request == self.name) + & (doctype.material_request_item.isin(mr_items)) + & (doctype.docstatus == 1) + ) + .groupby(doctype.material_request_item) + ) + + mr_items_ordered_qty = frappe._dict(query.run()) + + return mr_items_ordered_qty + def update_completed_qty(self, mr_items=None, update_modified=True): if self.material_request_type == "Purchase": return @@ -187,18 +216,13 @@ class MaterialRequest(BuyingController): if not mr_items: mr_items = [d.name for d in self.get("items")] + mr_items_ordered_qty = self.get_mr_items_ordered_qty(mr_items) + mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance") + for d in self.get("items"): if d.name in mr_items: if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"): - d.ordered_qty = flt( - frappe.db.sql( - """select sum(transfer_qty) - from `tabStock Entry Detail` where material_request = %s - and material_request_item = %s and docstatus = 1""", - (self.name, d.name), - )[0][0] - ) - mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance") + d.ordered_qty = flt(mr_items_ordered_qty.get(d.name)) if mr_qty_allowance: allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100)) @@ -217,14 +241,7 @@ class MaterialRequest(BuyingController): ) elif self.material_request_type == "Manufacture": - d.ordered_qty = flt( - frappe.db.sql( - """select sum(qty) - from `tabWork Order` where material_request = %s - and material_request_item = %s and docstatus = 1""", - (self.name, d.name), - )[0][0] - ) + d.ordered_qty = flt(mr_items_ordered_qty.get(d.name)) frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty) @@ -587,6 +604,9 @@ def make_stock_entry(source_name, target_doc=None): def set_missing_values(source, target): target.purpose = source.material_request_type + target.from_warehouse = source.set_from_warehouse + target.to_warehouse = source.set_warehouse + if source.job_card: target.purpose = "Material Transfer for Manufacture" @@ -716,3 +736,15 @@ def create_pick_list(source_name, target_doc=None): doc.set_item_locations() return doc + + +@frappe.whitelist() +def make_in_transit_stock_entry(source_name, in_transit_warehouse): + ste_doc = make_stock_entry(source_name) + ste_doc.add_to_transit = 1 + ste_doc.to_warehouse = in_transit_warehouse + + for row in ste_doc.items: + row.t_warehouse = in_transit_warehouse + + return ste_doc diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index f0a94997fe8..a707c74c7db 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -11,6 +11,7 @@ from frappe.utils import flt, today from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.material_request.material_request import ( + make_in_transit_stock_entry, make_purchase_order, make_stock_entry, make_supplier_quotation, @@ -56,6 +57,22 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(se.doctype, "Stock Entry") self.assertEqual(len(se.get("items")), len(mr.get("items"))) + def test_in_transit_make_stock_entry(self): + mr = frappe.copy_doc(test_records[0]).insert() + + self.assertRaises(frappe.ValidationError, make_stock_entry, mr.name) + + mr = frappe.get_doc("Material Request", mr.name) + mr.material_request_type = "Material Transfer" + mr.submit() + + in_transit_warehouse = get_in_transit_warehouse(mr.company) + se = make_in_transit_stock_entry(mr.name, in_transit_warehouse) + + self.assertEqual(se.doctype, "Stock Entry") + for row in se.get("items"): + self.assertEqual(row.t_warehouse, in_transit_warehouse) + def _insert_stock_entry(self, qty1, qty2, warehouse=None): se = frappe.get_doc( { @@ -742,6 +759,36 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(existing_requested_qty, current_requested_qty) +def get_in_transit_warehouse(company): + if not frappe.db.exists("Warehouse Type", "Transit"): + frappe.get_doc( + { + "doctype": "Warehouse Type", + "name": "Transit", + } + ).insert() + + in_transit_warehouse = frappe.db.exists( + "Warehouse", {"warehouse_type": "Transit", "company": company} + ) + + if not in_transit_warehouse: + in_transit_warehouse = ( + frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": "Transit", + "warehouse_type": "Transit", + "company": company, + } + ) + .insert() + .name + ) + + return in_transit_warehouse + + def make_material_request(**args): args = frappe._dict(args) mr = frappe.new_doc("Material Request") diff --git a/erpnext/stock/doctype/price_list/test_records.json b/erpnext/stock/doctype/price_list/test_records.json index 7ca949c4026..e02a7adbd8b 100644 --- a/erpnext/stock/doctype/price_list/test_records.json +++ b/erpnext/stock/doctype/price_list/test_records.json @@ -31,5 +31,21 @@ "enabled": 1, "price_list_name": "_Test Price List Rest of the World", "selling": 1 + }, + { + "buying": 0, + "currency": "USD", + "doctype": "Price List", + "enabled": 1, + "price_list_name": "_Test Selling Price List", + "selling": 1 + }, + { + "buying": 1, + "currency": "USD", + "doctype": "Price List", + "enabled": 1, + "price_list_name": "_Test Buying Price List", + "selling": 0 } ] diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index af0d1483253..c1abd31bcc1 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -293,6 +293,7 @@ class PurchaseReceipt(BuyingController): get_purchase_document_details, ) + stock_rbnb = None if erpnext.is_perpetual_inventory_enabled(self.company): stock_rbnb = self.get_company_default("stock_received_but_not_billed") landed_cost_entries = get_item_account_wise_additional_cost(self.name) @@ -450,6 +451,21 @@ class PurchaseReceipt(BuyingController): item=d, ) + if d.rate_difference_with_purchase_invoice and stock_rbnb: + account_currency = get_account_currency(stock_rbnb) + self.add_gl_entry( + gl_entries=gl_entries, + account=stock_rbnb, + cost_center=d.cost_center, + debit=0.0, + credit=flt(d.rate_difference_with_purchase_invoice), + remarks=_("Adjustment based on Purchase Invoice rate"), + against_account=warehouse_account_name, + account_currency=account_currency, + project=d.project, + item=d, + ) + # sub-contracting warehouse if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse): self.add_gl_entry( @@ -470,10 +486,11 @@ class PurchaseReceipt(BuyingController): + flt(d.landed_cost_voucher_amount) + flt(d.rm_supp_cost) + flt(d.item_tax_amount) + + flt(d.rate_difference_with_purchase_invoice) ) divisional_loss = flt( - valuation_amount_as_per_doc - stock_value_diff, d.precision("base_net_amount") + valuation_amount_as_per_doc - flt(stock_value_diff), d.precision("base_net_amount") ) if divisional_loss: @@ -765,7 +782,7 @@ class PurchaseReceipt(BuyingController): updated_pr += update_billed_amount_based_on_po(po_details, update_modified) for pr in set(updated_pr): - pr_doc = self if (pr == self.name) else frappe.get_cached_doc("Purchase Receipt", pr) + pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr) update_billing_percentage(pr_doc, update_modified=update_modified) self.load_from_db() @@ -881,30 +898,28 @@ def get_billed_amount_against_po(po_items): return {d.po_detail: flt(d.billed_amt) for d in query} -def update_billing_percentage(pr_doc, update_modified=True): +def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False): # Reload as billed amount was set in db directly pr_doc.load_from_db() # Update Billing % based on pending accepted qty total_amount, total_billed_amount = 0, 0 - for item in pr_doc.items: - return_data = frappe.get_all( - "Purchase Receipt", - fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"], - filters=[ - ["Purchase Receipt", "docstatus", "=", 1], - ["Purchase Receipt", "is_return", "=", 1], - ["Purchase Receipt Item", "purchase_receipt_item", "=", item.name], - ], - ) + item_wise_returned_qty = get_item_wise_returned_qty(pr_doc) - returned_qty = return_data[0].qty if return_data else 0 + for item in pr_doc.items: + returned_qty = flt(item_wise_returned_qty.get(item.name)) returned_amount = flt(returned_qty) * flt(item.rate) pending_amount = flt(item.amount) - returned_amount total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt total_amount += total_billable_amount total_billed_amount += flt(item.billed_amt) + if adjust_incoming_rate: + adjusted_amt = 0.0 + if item.billed_amt and item.amount: + adjusted_amt = flt(item.billed_amt) - flt(item.amount) + + item.db_set("rate_difference_with_purchase_invoice", adjusted_amt, update_modified=False) percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6) pr_doc.db_set("per_billed", percent_billed) @@ -914,6 +929,47 @@ def update_billing_percentage(pr_doc, update_modified=True): pr_doc.set_status(update=True) pr_doc.notify_update() + if adjust_incoming_rate: + adjust_incoming_rate_for_pr(pr_doc) + + +def adjust_incoming_rate_for_pr(doc): + doc.update_valuation_rate(reset_outgoing_rate=False) + + for item in doc.get("items"): + item.db_update() + + doc.docstatus = 2 + doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True) + doc.make_gl_entries_on_cancel() + + # update stock & gl entries for submit state of PR + doc.docstatus = 1 + doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True) + doc.make_gl_entries() + doc.repost_future_sle_and_gle() + + +def get_item_wise_returned_qty(pr_doc): + items = [d.name for d in pr_doc.items] + + return frappe._dict( + frappe.get_all( + "Purchase Receipt", + fields=[ + "`tabPurchase Receipt Item`.purchase_receipt_item", + "sum(abs(`tabPurchase Receipt Item`.qty)) as qty", + ], + filters=[ + ["Purchase Receipt", "docstatus", "=", 1], + ["Purchase Receipt", "is_return", "=", 1], + ["Purchase Receipt Item", "purchase_receipt_item", "in", items], + ], + group_by="`tabPurchase Receipt Item`.purchase_receipt_item", + as_list=1, + ) + ) + @frappe.whitelist() def make_purchase_invoice(source_name, target_doc=None): @@ -1121,13 +1177,25 @@ 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..cd320fdfcd0 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -69,6 +69,7 @@ "item_tax_amount", "rm_supp_cost", "landed_cost_voucher_amount", + "rate_difference_with_purchase_invoice", "billed_amt", "warehouse_and_reference", "warehouse", @@ -859,7 +860,8 @@ "label": "Purchase Receipt Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "collapsible": 1, @@ -974,7 +976,8 @@ "label": "Purchase Invoice Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "product_bundle", @@ -1005,12 +1008,20 @@ "fieldtype": "Check", "label": "Has Item Scanned", "read_only": 1 + }, + { + "fieldname": "rate_difference_with_purchase_invoice", + "fieldtype": "Currency", + "label": "Rate Difference with Purchase Invoice", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-11-02 12:49:28.746701", + "modified": "2023-02-28 15:43:04.470104", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 2a9f091bd09..9673c81e553 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -6,7 +6,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc -from frappe.utils import cint, cstr, flt +from frappe.utils import cint, cstr, flt, get_number_format_info from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template import ( get_template_details, @@ -156,7 +156,9 @@ class QualityInspection(Document): for i in range(1, 11): reading_value = reading.get("reading_" + str(i)) if reading_value is not None and reading_value.strip(): - result = flt(reading.get("min_value")) <= flt(reading_value) <= flt(reading.get("max_value")) + result = ( + flt(reading.get("min_value")) <= parse_float(reading_value) <= flt(reading.get("max_value")) + ) if not result: return False return True @@ -196,7 +198,7 @@ class QualityInspection(Document): # numeric readings for i in range(1, 11): field = "reading_" + str(i) - data[field] = flt(reading.get(field)) + data[field] = parse_float(reading.get(field)) data["mean"] = self.calculate_mean(reading) return data @@ -210,7 +212,7 @@ class QualityInspection(Document): for i in range(1, 11): reading_value = reading.get("reading_" + str(i)) if reading_value is not None and reading_value.strip(): - readings_list.append(flt(reading_value)) + readings_list.append(parse_float(reading_value)) actual_mean = mean(readings_list) if readings_list else 0 return actual_mean @@ -324,3 +326,19 @@ def make_quality_inspection(source_name, target_doc=None): ) return doc + + +def parse_float(num: str) -> float: + """Since reading_# fields are `Data` field they might contain number which + is representation in user's prefered number format instead of machine + readable format. This function converts them to machine readable format.""" + + number_format = frappe.db.get_default("number_format") or "#,###.##" + decimal_str, comma_str, _number_format_precision = get_number_format_info(number_format) + + if decimal_str == "," and comma_str == ".": + num = num.replace(",", "#$") + num = num.replace(".", ",") + num = num.replace("#$", ".") + + return flt(num) diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 4f19643ad52..9d2e1396226 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -2,7 +2,7 @@ # See license.txt import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import nowdate from erpnext.controllers.stock_controller import ( @@ -216,6 +216,40 @@ class TestQualityInspection(FrappeTestCase): qa.save() self.assertEqual(qa.status, "Accepted") + @change_settings("System Settings", {"number_format": "#.###,##"}) + def test_diff_number_format(self): + self.assertEqual(frappe.db.get_default("number_format"), "#.###,##") # sanity check + + # Test QI based on acceptance values (Non formula) + dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) + readings = [ + { + "specification": "Iron Content", # numeric reading + "min_value": 60, + "max_value": 100, + "reading_1": "70,000", + }, + { + "specification": "Iron Content", # numeric reading + "min_value": 60, + "max_value": 100, + "reading_1": "1.100,00", + }, + ] + + qa = create_quality_inspection( + reference_type="Delivery Note", reference_name=dn.name, readings=readings, do_not_save=True + ) + + qa.save() + + # status must be auto set as per formula + self.assertEqual(qa.readings[0].status, "Accepted") + self.assertEqual(qa.readings[1].status, "Rejected") + + qa.delete() + dn.delete() + def create_quality_inspection(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 398b3c98e38..3f6a2c881b8 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -397,6 +397,7 @@ class StockReconciliation(StockController): "voucher_type": self.doctype, "voucher_no": self.name, "voucher_detail_no": row.name, + "actual_qty": 0, "company": self.company, "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"), "is_cancelled": 1 if self.docstatus == 2 else 0, @@ -423,6 +424,8 @@ class StockReconciliation(StockController): data.valuation_rate = flt(row.valuation_rate) data.stock_value_difference = -1 * flt(row.amount_difference) + self.update_inventory_dimensions(row, data) + return data def make_sle_on_cancel(self): diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js index 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/get_item_details.py b/erpnext/stock/get_item_details.py index 5af144110f0..489ec6ebecc 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -8,6 +8,7 @@ import frappe from frappe import _, throw from frappe.model import child_table_fields, default_fields from frappe.model.meta import get_field_precision +from frappe.query_builder.functions import CombineDatetime, IfNull, Sum from frappe.utils import add_days, add_months, cint, cstr, flt, getdate from erpnext import get_company_currency @@ -88,8 +89,15 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru update_party_blanket_order(args, out) + # Never try to find a customer price if customer is set in these Doctype + current_customer = args.customer + if args.get("doctype") in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]: + args.customer = None + out.update(get_price_list_rate(args, item)) + args.customer = current_customer + if args.customer and cint(args.is_pos): out.update(get_pos_profile_item_details(args.company, args, update_data=True)) @@ -519,12 +527,8 @@ def get_barcode_data(items_list): itemwise_barcode = {} for item in items_list: - barcodes = frappe.db.sql( - """ - select barcode from `tabItem Barcode` where parent = %s - """, - item.item_code, - as_dict=1, + barcodes = frappe.db.get_all( + "Item Barcode", filters={"parent": item.item_code}, fields="barcode" ) for barcode in barcodes: @@ -884,34 +888,36 @@ def get_item_price(args, item_code, ignore_party=False): :param item_code: str, Item Doctype field item_code """ - args["item_code"] = item_code - - conditions = """where item_code=%(item_code)s - and price_list=%(price_list)s - and ifnull(uom, '') in ('', %(uom)s)""" - - conditions += "and ifnull(batch_no, '') in ('', %(batch_no)s)" + ip = frappe.qb.DocType("Item Price") + query = ( + frappe.qb.from_(ip) + .select(ip.name, ip.price_list_rate, ip.uom) + .where( + (ip.item_code == item_code) + & (ip.price_list == args.get("price_list")) + & (IfNull(ip.uom, "").isin(["", args.get("uom")])) + & (IfNull(ip.batch_no, "").isin(["", args.get("batch_no")])) + ) + .orderby(ip.valid_from, order=frappe.qb.desc) + .orderby(IfNull(ip.batch_no, ""), order=frappe.qb.desc) + .orderby(ip.uom, order=frappe.qb.desc) + ) if not ignore_party: if args.get("customer"): - conditions += " and customer=%(customer)s" + query = query.where(ip.customer == args.get("customer")) elif args.get("supplier"): - conditions += " and supplier=%(supplier)s" + query = query.where(ip.supplier == args.get("supplier")) else: - conditions += "and (customer is null or customer = '') and (supplier is null or supplier = '')" + query = query.where((IfNull(ip.customer, "") == "") & (IfNull(ip.supplier, "") == "")) if args.get("transaction_date"): - conditions += """ and %(transaction_date)s between - ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')""" + query = query.where( + (IfNull(ip.valid_from, "2000-01-01") <= args["transaction_date"]) + & (IfNull(ip.valid_upto, "2500-12-31") >= args["transaction_date"]) + ) - return frappe.db.sql( - """ select name, price_list_rate, uom - from `tabItem Price` {conditions} - order by valid_from desc, ifnull(batch_no, '') desc, uom desc """.format( - conditions=conditions - ), - args, - ) + return query.run() def get_price_list_rate_for(args, item_code): @@ -1084,91 +1090,68 @@ def get_pos_profile(company, pos_profile=None, user=None): if not user: user = frappe.session["user"] - condition = "pfu.user = %(user)s AND pfu.default=1" - if user and company: - condition = "pfu.user = %(user)s AND pf.company = %(company)s AND pfu.default=1" + pf = frappe.qb.DocType("POS Profile") + pfu = frappe.qb.DocType("POS Profile User") - pos_profile = frappe.db.sql( - """SELECT pf.* - FROM - `tabPOS Profile` pf LEFT JOIN `tabPOS Profile User` pfu - ON - pf.name = pfu.parent - WHERE - {cond} AND pf.disabled = 0 - """.format( - cond=condition - ), - {"user": user, "company": company}, - as_dict=1, + query = ( + frappe.qb.from_(pf) + .left_join(pfu) + .on(pf.name == pfu.parent) + .select(pf.star) + .where((pfu.user == user) & (pfu.default == 1)) ) + if company: + query = query.where(pf.company == company) + + pos_profile = query.run(as_dict=True) + if not pos_profile and company: - pos_profile = frappe.db.sql( - """SELECT pf.* - FROM - `tabPOS Profile` pf LEFT JOIN `tabPOS Profile User` pfu - ON - pf.name = pfu.parent - WHERE - pf.company = %(company)s AND pf.disabled = 0 - """, - {"company": company}, - as_dict=1, - ) + pos_profile = ( + frappe.qb.from_(pf) + .left_join(pfu) + .on(pf.name == pfu.parent) + .select(pf.star) + .where((pf.company == company) & (pf.disabled == 0)) + ).run(as_dict=True) return pos_profile and pos_profile[0] or None def get_serial_nos_by_fifo(args, sales_order=None): if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"): - return "\n".join( - frappe.db.sql_list( - """select name from `tabSerial No` - where item_code=%(item_code)s and warehouse=%(warehouse)s and - sales_order=IF(%(sales_order)s IS NULL, sales_order, %(sales_order)s) - order by timestamp(purchase_date, purchase_time) - asc limit %(qty)s""", - { - "item_code": args.item_code, - "warehouse": args.warehouse, - "qty": abs(cint(args.stock_qty)), - "sales_order": sales_order, - }, - ) + sn = frappe.qb.DocType("Serial No") + query = ( + frappe.qb.from_(sn) + .select(sn.name) + .where((sn.item_code == args.item_code) & (sn.warehouse == args.warehouse)) + .orderby(CombineDatetime(sn.purchase_date, sn.purchase_time)) + .limit(abs(cint(args.stock_qty))) ) + if sales_order: + query = query.where(sn.sales_order == sales_order) + if args.batch_no: + query = query.where(sn.batch_no == args.batch_no) -def get_serial_no_batchwise(args, sales_order=None): - if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"): - return "\n".join( - frappe.db.sql_list( - """select name from `tabSerial No` - where item_code=%(item_code)s and warehouse=%(warehouse)s and - sales_order=IF(%(sales_order)s IS NULL, sales_order, %(sales_order)s) - and batch_no=IF(%(batch_no)s IS NULL, batch_no, %(batch_no)s) order - by timestamp(purchase_date, purchase_time) asc limit %(qty)s""", - { - "item_code": args.item_code, - "warehouse": args.warehouse, - "batch_no": args.batch_no, - "qty": abs(cint(args.stock_qty)), - "sales_order": sales_order, - }, - ) - ) + serial_nos = query.run(as_list=True) + serial_nos = [s[0] for s in serial_nos] + + return "\n".join(serial_nos) @frappe.whitelist() def get_conversion_factor(item_code, uom): variant_of = frappe.db.get_value("Item", item_code, "variant_of", cache=True) filters = {"parent": item_code, "uom": uom} + if variant_of: filters["parent"] = ("in", (item_code, variant_of)) conversion_factor = frappe.db.get_value("UOM Conversion Detail", filters, "conversion_factor") if not conversion_factor: stock_uom = frappe.db.get_value("Item", item_code, "stock_uom") conversion_factor = get_uom_conv_factor(uom, stock_uom) + return {"conversion_factor": conversion_factor or 1.0} @@ -1210,12 +1193,16 @@ def get_bin_details(item_code, warehouse, company=None, include_child_warehouses def get_company_total_stock(item_code, company): - return frappe.db.sql( - """SELECT sum(actual_qty) from - (`tabBin` INNER JOIN `tabWarehouse` ON `tabBin`.warehouse = `tabWarehouse`.name) - WHERE `tabWarehouse`.company = %s and `tabBin`.item_code = %s""", - (company, item_code), - )[0][0] + bin = frappe.qb.DocType("Bin") + wh = frappe.qb.DocType("Warehouse") + + return ( + frappe.qb.from_(bin) + .inner_join(wh) + .on(bin.warehouse == wh.name) + .select(Sum(bin.actual_qty)) + .where((wh.company == company) & (bin.item_code == item_code)) + ).run()[0][0] @frappe.whitelist() @@ -1224,6 +1211,7 @@ def get_serial_no_details(item_code, warehouse, stock_qty, serial_no): {"item_code": item_code, "warehouse": warehouse, "stock_qty": stock_qty, "serial_no": serial_no} ) serial_no = get_serial_no(args) + return {"serial_no": serial_no} @@ -1243,6 +1231,7 @@ def get_bin_details_and_serial_nos( bin_details_and_serial_nos.update( get_serial_no_details(item_code, warehouse, stock_qty, serial_no) ) + return bin_details_and_serial_nos @@ -1257,6 +1246,7 @@ def get_batch_qty_and_serial_no(batch_no, stock_qty, warehouse, item_code, has_s ) serial_no = get_serial_no(args) batch_qty_and_serial_no.update({"serial_no": serial_no}) + return batch_qty_and_serial_no @@ -1329,7 +1319,6 @@ def apply_price_list(args, as_doc=False): def apply_price_list_on_item(args): item_doc = frappe.db.get_value("Item", args.item_code, ["name", "variant_of"], as_dict=1) item_details = get_price_list_rate(args, item_doc) - item_details.update(get_pricing_rule_for_item(args)) return item_details @@ -1413,12 +1402,12 @@ def get_valuation_rate(item_code, company, warehouse=None): ) or {"valuation_rate": 0} elif not item.get("is_stock_item"): - valuation_rate = frappe.db.sql( - """select sum(base_net_amount) / sum(qty*conversion_factor) - from `tabPurchase Invoice Item` - where item_code = %s and docstatus=1""", - item_code, - ) + pi_item = frappe.qb.DocType("Purchase Invoice Item") + valuation_rate = ( + frappe.qb.from_(pi_item) + .select((Sum(pi_item.base_net_amount) / Sum(pi_item.qty * pi_item.conversion_factor))) + .where((pi_item.docstatus == 1) & (pi_item.item_code == item_code)) + ).run() if valuation_rate: return {"valuation_rate": valuation_rate[0][0] or 0.0} @@ -1444,7 +1433,7 @@ def get_serial_no(args, serial_nos=None, sales_order=None): if args.get("warehouse") and args.get("stock_qty") and args.get("item_code"): has_serial_no = frappe.get_value("Item", {"item_code": args.item_code}, "has_serial_no") if args.get("batch_no") and has_serial_no == 1: - return get_serial_no_batchwise(args, sales_order) + return get_serial_nos_by_fifo(args, sales_order) elif has_serial_no == 1: args = json.dumps( { @@ -1476,31 +1465,35 @@ def get_blanket_order_details(args): args = frappe._dict(json.loads(args)) blanket_order_details = None - condition = "" - if args.item_code: - if args.customer and args.doctype == "Sales Order": - condition = " and bo.customer=%(customer)s" - elif args.supplier and args.doctype == "Purchase Order": - condition = " and bo.supplier=%(supplier)s" - if args.blanket_order: - condition += " and bo.name =%(blanket_order)s" - if args.transaction_date: - condition += " and bo.to_date>=%(transaction_date)s" - blanket_order_details = frappe.db.sql( - """ - select boi.rate as blanket_order_rate, bo.name as blanket_order - from `tabBlanket Order` bo, `tabBlanket Order Item` boi - where bo.company=%(company)s and boi.item_code=%(item_code)s - and bo.docstatus=1 and bo.name = boi.parent {0} - """.format( - condition - ), - args, - as_dict=True, + if args.item_code: + bo = frappe.qb.DocType("Blanket Order") + bo_item = frappe.qb.DocType("Blanket Order Item") + + query = ( + frappe.qb.from_(bo) + .from_(bo_item) + .select(bo_item.rate.as_("blanket_order_rate"), bo.name.as_("blanket_order")) + .where( + (bo.company == args.company) + & (bo_item.item_code == args.item_code) + & (bo.docstatus == 1) + & (bo.name == bo_item.parent) + ) ) + if args.customer and args.doctype == "Sales Order": + query = query.where(bo.customer == args.customer) + elif args.supplier and args.doctype == "Purchase Order": + query = query.where(bo.supplier == args.supplier) + if args.blanket_order: + query = query.where(bo.name == args.blanket_order) + if args.transaction_date: + query = query.where(bo.to_date >= args.transaction_date) + + blanket_order_details = query.run(as_dict=True) blanket_order_details = blanket_order_details[0] if blanket_order_details else "" + return blanket_order_details @@ -1510,10 +1503,10 @@ def get_so_reservation_for_item(args): if get_reserved_qty_for_so(args.get("against_sales_order"), args.get("item_code")): reserved_so = args.get("against_sales_order") elif args.get("against_sales_invoice"): - sales_order = frappe.db.sql( - """select sales_order from `tabSales Invoice Item` where - parent=%s and item_code=%s""", - (args.get("against_sales_invoice"), args.get("item_code")), + sales_order = frappe.db.get_all( + "Sales Invoice Item", + filters={"parent": args.get("against_sales_invoice"), "item_code": args.get("item_code")}, + fields="sales_order", ) if sales_order and sales_order[0]: if get_reserved_qty_for_so(sales_order[0][0], args.get("item_code")): @@ -1525,13 +1518,14 @@ def get_so_reservation_for_item(args): def get_reserved_qty_for_so(sales_order, item_code): - reserved_qty = frappe.db.sql( - """select sum(qty) from `tabSales Order Item` - where parent=%s and item_code=%s and ensure_delivery_based_on_produced_serial_no=1 - """, - (sales_order, item_code), + reserved_qty = frappe.db.get_value( + "Sales Order Item", + filters={ + "parent": sales_order, + "item_code": item_code, + "ensure_delivery_based_on_produced_serial_no": 1, + }, + fieldname="sum(qty)", ) - if reserved_qty and reserved_qty[0][0]: - return reserved_qty[0][0] - else: - return 0 + + return reserved_qty or 0 diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 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/stock/stock_balance.py b/erpnext/stock/stock_balance.py index 14cedd2e8a9..439ed7a8e09 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -121,7 +121,7 @@ def get_reserved_qty(item_code, warehouse): and parenttype='Sales Order' and item_code != parent_item and exists (select * from `tabSales Order` so - where name = dnpi_in.parent and docstatus = 1 and status != 'Closed') + where name = dnpi_in.parent and docstatus = 1 and status not in ('On Hold', 'Closed')) ) dnpi) union (select stock_qty as dnpi_qty, qty as so_item_qty, @@ -131,7 +131,7 @@ def get_reserved_qty(item_code, warehouse): and (so_item.delivered_by_supplier is null or so_item.delivered_by_supplier = 0) and exists(select * from `tabSales Order` so where so.name = so_item.parent and so.docstatus = 1 - and so.status != 'Closed')) + and so.status not in ('On Hold', 'Closed'))) ) tab where so_item_qty >= so_item_delivered_qty diff --git a/erpnext/stock/tests/test_get_item_details.py b/erpnext/stock/tests/test_get_item_details.py new file mode 100644 index 00000000000..b53e29e9e8e --- /dev/null +++ b/erpnext/stock/tests/test_get_item_details.py @@ -0,0 +1,40 @@ +import json + +import frappe +from frappe.test_runner import make_test_records +from frappe.tests.utils import FrappeTestCase + +from erpnext.stock.get_item_details import get_item_details + +test_ignore = ["BOM"] +test_dependencies = ["Customer", "Supplier", "Item", "Price List", "Item Price"] + + +class TestGetItemDetail(FrappeTestCase): + def setUp(self): + make_test_records("Price List") + super().setUp() + + def test_get_item_detail_purchase_order(self): + + args = frappe._dict( + { + "item_code": "_Test Item", + "company": "_Test Company", + "customer": "_Test Customer", + "conversion_rate": 1.0, + "price_list_currency": "USD", + "plc_conversion_rate": 1.0, + "doctype": "Purchase Order", + "name": None, + "supplier": "_Test Supplier", + "transaction_date": None, + "conversion_rate": 1.0, + "price_list": "_Test Buying Price List", + "is_subcontracted": 0, + "ignore_pricing_rule": 1, + "qty": 1, + } + ) + details = get_item_details(args) + self.assertEqual(details.get("price_list_rate"), 100) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index 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/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index f4fd4de169d..95dbc83bf80 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -191,14 +191,17 @@ class SubcontractingReceipt(SubcontractingController): def validate_available_qty_for_consumption(self): for item in self.get("supplied_items"): + precision = item.precision("consumed_qty") if ( - item.available_qty_for_consumption and item.available_qty_for_consumption < item.consumed_qty + item.available_qty_for_consumption + and flt(item.available_qty_for_consumption, precision) - flt(item.consumed_qty, precision) < 0 ): - frappe.throw( - _( - "Row {0}: Consumed Qty must be less than or equal to Available Qty For Consumption in Consumed Items Table." - ).format(item.idx) - ) + msg = f"""Row {item.idx}: Consumed Qty {flt(item.consumed_qty, precision)} + must be less than or equal to Available Qty For Consumption + {flt(item.available_qty_for_consumption, precision)} + in Consumed Items Table.""" + + frappe.throw(_(msg)) def validate_items_qty(self): for item in self.items: diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 7f4e9efa948..2a078c4395b 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -13,8 +13,8 @@ from frappe.utils import ( get_datetime, get_datetime_str, get_link_to_form, + get_system_timezone, get_time, - get_time_zone, get_weekdays, getdate, nowdate, @@ -981,7 +981,7 @@ def convert_utc_to_user_timezone(utc_timestamp, user): def get_tz(user): - return frappe.db.get_value("User", user, "time_zone") or get_time_zone() + return frappe.db.get_value("User", user, "time_zone") or get_system_timezone() @frappe.whitelist() diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html index 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/erpnext/translations/de.csv b/erpnext/translations/de.csv index 5a0a863a47e..bec3ce242b0 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -9916,3 +9916,5 @@ Cost and Freight,Kosten und Fracht, Delivered at Place,Geliefert benannter Ort, Delivered at Place Unloaded,Geliefert benannter Ort entladen, Delivered Duty Paid,Geliefert verzollt, +Discount Validity,Frist für den Rabatt, +Discount Validity Based On,Frist für den Rabatt berechnet sich nach, diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py index 13b7877b21d..62033a5e18e 100644 --- a/erpnext/utilities/doctype/video/video.py +++ b/erpnext/utilities/doctype/video/video.py @@ -10,6 +10,7 @@ import pytz from frappe import _ from frappe.model.document import Document from frappe.utils import cint +from frappe.utils.data import get_system_timezone from pyyoutube import Api @@ -64,7 +65,7 @@ def update_youtube_data(): frequency = get_frequency(frequency) time = datetime.now() - timezone = pytz.timezone(frappe.utils.get_time_zone()) + timezone = pytz.timezone(get_system_timezone()) site_time = time.astimezone(timezone) if frequency == 30: diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py index dfca9465ed1..f50c207ab98 100644 --- a/erpnext/www/book_appointment/index.py +++ b/erpnext/www/book_appointment/index.py @@ -4,6 +4,7 @@ import json import frappe import pytz from frappe import _ +from frappe.utils.data import get_system_timezone WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] @@ -125,7 +126,7 @@ def filter_timeslots(date, timeslots): def convert_to_guest_timezone(guest_tz, datetimeobject): guest_tz = pytz.timezone(guest_tz) - local_timezone = pytz.timezone(frappe.utils.get_time_zone()) + local_timezone = pytz.timezone(get_system_timezone()) datetimeobject = local_timezone.localize(datetimeobject) datetimeobject = datetimeobject.astimezone(guest_tz) return datetimeobject @@ -134,7 +135,7 @@ def convert_to_guest_timezone(guest_tz, datetimeobject): def convert_to_system_timezone(guest_tz, datetimeobject): guest_tz = pytz.timezone(guest_tz) datetimeobject = guest_tz.localize(datetimeobject) - system_tz = pytz.timezone(frappe.utils.get_time_zone()) + system_tz = pytz.timezone(get_system_timezone()) datetimeobject = datetimeobject.astimezone(system_tz) return datetimeobject diff --git a/pyproject.toml b/pyproject.toml index 1b342a5fbb3..0718e5b4a16 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", @@ -28,9 +28,6 @@ dependencies = [ requires = ["flit_core >=3.4,<4"] build-backend = "flit_core.buildapi" -[tool.bench.dev-dependencies] -hypothesis = "~=6.31.0" - [tool.black] line-length = 99