diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 5df8269f754..22c81ddd463 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -374,12 +374,13 @@ def make_gl_entries(doc, credit_account, debit_account, against, frappe.db.commit() except Exception as e: if frappe.flags.in_test: + traceback = frappe.get_traceback() + frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) raise e else: frappe.db.rollback() traceback = frappe.get_traceback() - frappe.log_error(message=traceback) - + frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) frappe.flags.deferred_accounting_error = True def send_mail(deferred_process): @@ -446,10 +447,12 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against, if submit: journal_entry.submit() + + frappe.db.commit() except Exception: frappe.db.rollback() traceback = frappe.get_traceback() - frappe.log_error(message=traceback) + frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback) frappe.flags.deferred_accounting_error = True diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index e904768b386..6b4b43d30b7 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -68,10 +68,12 @@ def _get_party_details(party=None, account=None, party_type="Customer", company= party_details["tax_category"] = get_address_tax_category(party.get("tax_category"), party_address, shipping_address if party_type != "Supplier" else party_address) - if not party_details.get("taxes_and_charges"): - party_details["taxes_and_charges"] = set_taxes(party.name, party_type, posting_date, company, - customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category, - billing_address=party_address, shipping_address=shipping_address) + tax_template = set_taxes(party.name, party_type, posting_date, company, + customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category, + billing_address=party_address, shipping_address=shipping_address) + + if tax_template: + party_details['taxes_and_charges'] = tax_template if cint(fetch_payment_terms_template): party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 88bcdad7102..353f9087f1b 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -109,7 +109,11 @@ class ReceivablePayableReport(object): invoiced = 0.0, paid = 0.0, credit_note = 0.0, - outstanding = 0.0 + outstanding = 0.0, + invoiced_in_account_currency = 0.0, + paid_in_account_currency = 0.0, + credit_note_in_account_currency = 0.0, + outstanding_in_account_currency = 0.0 ) self.get_invoices(gle) @@ -150,21 +154,28 @@ class ReceivablePayableReport(object): # gle_balance will be the total "debit - credit" for receivable type reports and # and vice-versa for payable type reports gle_balance = self.get_gle_balance(gle) + gle_balance_in_account_currency = self.get_gle_balance_in_account_currency(gle) + if gle_balance > 0: if gle.voucher_type in ('Journal Entry', 'Payment Entry') and gle.against_voucher: # debit against sales / purchase invoice row.paid -= gle_balance + row.paid_in_account_currency -= gle_balance_in_account_currency else: # invoice row.invoiced += gle_balance + row.invoiced_in_account_currency += gle_balance_in_account_currency else: # payment or credit note for receivables if self.is_invoice(gle): # stand alone debit / credit note row.credit_note -= gle_balance + row.credit_note_in_account_currency -= gle_balance_in_account_currency else: # advance / unlinked payment or other adjustment row.paid -= gle_balance + row.paid_in_account_currency -= gle_balance_in_account_currency + if gle.cost_center: row.cost_center = str(gle.cost_center) @@ -216,8 +227,13 @@ class ReceivablePayableReport(object): # as we can use this to filter out invoices without outstanding for key, row in self.voucher_balance.items(): row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision) + row.outstanding_in_account_currency = flt(row.invoiced_in_account_currency - row.paid_in_account_currency - \ + row.credit_note_in_account_currency, self.currency_precision) + row.invoice_grand_total = row.invoiced - if abs(row.outstanding) > 1.0/10 ** self.currency_precision: + + if (abs(row.outstanding) > 1.0/10 ** self.currency_precision) and \ + (abs(row.outstanding_in_account_currency) > 1.0/10 ** self.currency_precision): # non-zero oustanding, we must consider this row if self.is_invoice(row) and self.filters.based_on_payment_terms: @@ -583,12 +599,14 @@ class ReceivablePayableReport(object): else: select_fields = "debit, credit" + doc_currency_fields = "debit_in_account_currency, credit_in_account_currency" + remarks = ", remarks" if self.filters.get("show_remarks") else "" self.gl_entries = frappe.db.sql(""" select name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, - against_voucher_type, against_voucher, account_currency, {0} {remarks} + against_voucher_type, against_voucher, account_currency, {0}, {1} {remarks} from `tabGL Entry` where @@ -596,8 +614,8 @@ class ReceivablePayableReport(object): and is_cancelled = 0 and party_type=%s and (party is not null and party != '') - {1} {2} {3}""" - .format(select_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True) + {2} {3} {4}""" + .format(select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True) def get_sales_invoices_or_customers_based_on_sales_person(self): if self.filters.get("sales_person"): @@ -718,6 +736,13 @@ class ReceivablePayableReport(object): # get the balance of the GL (debit - credit) or reverse balance based on report type return gle.get(self.dr_or_cr) - self.get_reverse_balance(gle) + def get_gle_balance_in_account_currency(self, gle): + # get the balance of the GL (debit - credit) or reverse balance based on report type + return gle.get(self.dr_or_cr + '_in_account_currency') - self.get_reverse_balance_in_account_currency(gle) + + def get_reverse_balance_in_account_currency(self, gle): + return gle.get('debit_in_account_currency' if self.dr_or_cr=='credit' else 'credit_in_account_currency') + def get_reverse_balance(self, gle): # get "credit" balance if report type is "debit" and vice versa return gle.get('debit' if self.dr_or_cr=='credit' else 'credit') diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 9fa50a9aded..153f5c537a2 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -80,20 +80,20 @@ frappe.ui.form.on('Asset', { if (frm.doc.docstatus==1) { if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) { - frm.add_custom_button("Transfer Asset", function() { + frm.add_custom_button(__("Transfer Asset"), function() { erpnext.asset.transfer_asset(frm); }, __("Manage")); - frm.add_custom_button("Scrap Asset", function() { + frm.add_custom_button(__("Scrap Asset"), function() { erpnext.asset.scrap_asset(frm); }, __("Manage")); - frm.add_custom_button("Sell Asset", function() { + frm.add_custom_button(__("Sell Asset"), function() { frm.trigger("make_sales_invoice"); }, __("Manage")); } else if (frm.doc.status=='Scrapped') { - frm.add_custom_button("Restore Asset", function() { + frm.add_custom_button(__("Restore Asset"), function() { erpnext.asset.restore_asset(frm); }, __("Manage")); } @@ -121,7 +121,7 @@ frappe.ui.form.on('Asset', { } if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) { - frm.add_custom_button("View General Ledger", function() { + frm.add_custom_button(__("View General Ledger"), function() { frappe.route_options = { "voucher_no": frm.doc.name, "from_date": frm.doc.available_for_use_date, diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index f2b02cb9c56..5a9f1c25421 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -192,8 +192,7 @@ class Asset(AccountsController): # value_after_depreciation - current Asset value if self.docstatus == 1 and d.value_after_depreciation: - value_after_depreciation = (flt(d.value_after_depreciation) - - flt(self.opening_accumulated_depreciation)) + value_after_depreciation = flt(d.value_after_depreciation) else: value_after_depreciation = (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)) @@ -241,7 +240,7 @@ class Asset(AccountsController): break # For first row - if has_pro_rata and n==0: + if has_pro_rata and not self.opening_accumulated_depreciation and n==0: depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, self.available_for_use_date, d.depreciation_start_date) @@ -254,7 +253,7 @@ class Asset(AccountsController): if not self.flags.increase_in_asset_life: # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission self.to_date = add_months(self.available_for_use_date, - n * cint(d.frequency_of_depreciation)) + (n + self.number_of_depreciations_booked) * cint(d.frequency_of_depreciation)) depreciation_amount_without_pro_rata = depreciation_amount @@ -354,7 +353,12 @@ class Asset(AccountsController): # if it returns True, depreciation_amount will not be equal for the first and last rows def check_is_pro_rata(self, row): has_pro_rata = False - days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1 + + # if not existing asset, from_date = available_for_use_date + # otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12 + # from_date = 01/01/2022 + from_date = self.get_modified_available_for_use_date(row) + days = date_diff(row.depreciation_start_date, from_date) + 1 # if frequency_of_depreciation is 12 months, total_days = 365 total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation) @@ -364,6 +368,9 @@ class Asset(AccountsController): return has_pro_rata + def get_modified_available_for_use_date(self, row): + return add_months(self.available_for_use_date, (self.number_of_depreciations_booked * row.frequency_of_depreciation)) + def validate_asset_finance_books(self, row): if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount") @@ -402,10 +409,11 @@ class Asset(AccountsController): # to ensure that final accumulated depreciation amount is accurate def get_adjusted_depreciation_amount(self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row, finance_book): - depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book) + if not self.opening_accumulated_depreciation: + depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book) - if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata: - depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row + if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata: + depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row return depreciation_amount_for_last_row @@ -850,13 +858,11 @@ def get_total_days(date, frequency): @erpnext.allow_regional def get_depreciation_amount(asset, depreciable_value, row): - depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked) - if row.depreciation_method in ("Straight Line", "Manual"): # if the Depreciation Schedule is being prepared for the first time if not asset.flags.increase_in_asset_life: - depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) - - flt(row.expected_value_after_useful_life)) / depreciation_left + depreciation_amount = (flt(asset.gross_purchase_amount) - + flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations) # if the Depreciation Schedule is being modified after Asset Repair else: diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index ca10b1db19a..874fb630f87 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -57,8 +57,10 @@ def make_depreciation_entry(asset_name, date=None): je.finance_book = d.finance_book je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount) + credit_account, debit_account = get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account) + credit_entry = { - "account": accumulated_depreciation_account, + "account": credit_account, "credit_in_account_currency": d.depreciation_amount, "reference_type": "Asset", "reference_name": asset.name, @@ -66,7 +68,7 @@ def make_depreciation_entry(asset_name, date=None): } debit_entry = { - "account": depreciation_expense_account, + "account": debit_account, "debit_in_account_currency": d.depreciation_amount, "reference_type": "Asset", "reference_name": asset.name, @@ -132,6 +134,20 @@ def get_depreciation_accounts(asset): return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account +def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account): + root_type = frappe.get_value("Account", depreciation_expense_account, "root_type") + + if root_type == "Expense": + credit_account = accumulated_depreciation_account + debit_account = depreciation_expense_account + elif root_type == "Income": + credit_account = depreciation_expense_account + debit_account = accumulated_depreciation_account + else: + frappe.throw(_("Depreciation Expense Account should be an Income or Expense Account.")) + + return credit_account, debit_account + @frappe.whitelist() def scrap_asset(asset_name): asset = frappe.get_doc("Asset", asset_name) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index d1d4527ec70..ce2cb01ab2e 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -409,19 +409,18 @@ class TestDepreciationMethods(AssetSetup): calculate_depreciation = 1, available_for_use_date = "2030-06-06", is_existing_asset = 1, - number_of_depreciations_booked = 1, - opening_accumulated_depreciation = 40000, + number_of_depreciations_booked = 2, + opening_accumulated_depreciation = 47095.89, expected_value_after_useful_life = 10000, - depreciation_start_date = "2030-12-31", + depreciation_start_date = "2032-12-31", total_number_of_depreciations = 3, frequency_of_depreciation = 12 ) self.assertEqual(asset.status, "Draft") expected_schedules = [ - ["2030-12-31", 14246.58, 54246.58], - ["2031-12-31", 25000.00, 79246.58], - ["2032-06-06", 10753.42, 90000.00] + ["2032-12-31", 30000.0, 77095.89], + ["2033-06-06", 12904.11, 90000.0] ] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] for d in asset.get("schedules")] @@ -869,6 +868,72 @@ class TestDepreciationBasics(AssetSetup): self.assertFalse(asset.schedules[1].journal_entry) self.assertFalse(asset.schedules[2].journal_entry) + def test_depr_entry_posting_when_depr_expense_account_is_an_expense_account(self): + """Tests if the Depreciation Expense Account gets debited and the Accumulated Depreciation Account gets credited when the former's an Expense Account.""" + + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + depreciation_start_date = "2020-12-31", + frequency_of_depreciation = 12, + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + submit = 1 + ) + + post_depreciation_entries(date="2021-06-01") + asset.load_from_db() + + je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry) + accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts] + + for entry in accounting_entries: + if entry["account"] == "_Test Depreciations - _TC": + self.assertTrue(entry["debit"]) + self.assertFalse(entry["credit"]) + else: + self.assertTrue(entry["credit"]) + self.assertFalse(entry["debit"]) + + def test_depr_entry_posting_when_depr_expense_account_is_an_income_account(self): + """Tests if the Depreciation Expense Account gets credited and the Accumulated Depreciation Account gets debited when the former's an Income Account.""" + + depr_expense_account = frappe.get_doc("Account", "_Test Depreciations - _TC") + depr_expense_account.root_type = "Income" + depr_expense_account.parent_account = "Income - _TC" + depr_expense_account.save() + + asset = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + depreciation_start_date = "2020-12-31", + frequency_of_depreciation = 12, + total_number_of_depreciations = 3, + expected_value_after_useful_life = 10000, + submit = 1 + ) + + post_depreciation_entries(date="2021-06-01") + asset.load_from_db() + + je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry) + accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts] + + for entry in accounting_entries: + if entry["account"] == "_Test Depreciations - _TC": + self.assertTrue(entry["credit"]) + self.assertFalse(entry["debit"]) + else: + self.assertTrue(entry["debit"]) + self.assertFalse(entry["credit"]) + + # resetting + depr_expense_account.root_type = "Expense" + depr_expense_account.parent_account = "Expenses - _TC" + depr_expense_account.save() + def test_clear_depreciation_schedule(self): """Tests if clear_depreciation_schedule() works as expected.""" diff --git a/erpnext/assets/doctype/asset_category/asset_category.js b/erpnext/assets/doctype/asset_category/asset_category.js index 51ce157a81c..c702687072d 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.js +++ b/erpnext/assets/doctype/asset_category/asset_category.js @@ -33,7 +33,7 @@ frappe.ui.form.on('Asset Category', { var d = locals[cdt][cdn]; return { "filters": { - "root_type": "Expense", + "root_type": ["in", ["Expense", "Income"]], "is_group": 0, "company": d.company_name } diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index e2f3ca318f6..bd573bf479d 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -42,10 +42,10 @@ class AssetCategory(Document): def validate_account_types(self): account_type_map = { - 'fixed_asset_account': { 'account_type': 'Fixed Asset' }, - 'accumulated_depreciation_account': { 'account_type': 'Accumulated Depreciation' }, - 'depreciation_expense_account': { 'root_type': 'Expense' }, - 'capital_work_in_progress_account': { 'account_type': 'Capital Work in Progress' } + 'fixed_asset_account': {'account_type': ['Fixed Asset']}, + 'accumulated_depreciation_account': {'account_type': ['Accumulated Depreciation']}, + 'depreciation_expense_account': {'root_type': ['Expense', 'Income']}, + 'capital_work_in_progress_account': {'account_type': ['Capital Work in Progress']} } for d in self.accounts: for fieldname in account_type_map.keys(): @@ -53,11 +53,11 @@ class AssetCategory(Document): selected_account = d.get(fieldname) key_to_match = next(iter(account_type_map.get(fieldname))) # acount_type or root_type selected_key_type = frappe.db.get_value('Account', selected_account, key_to_match) - expected_key_type = account_type_map[fieldname][key_to_match] + expected_key_types = account_type_map[fieldname][key_to_match] - if selected_key_type != expected_key_type: + if selected_key_type not in expected_key_types: frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.") - .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)), + .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_types)), title=_("Invalid Account")) def valide_cwip_account(self): diff --git a/erpnext/crm/doctype/crm_settings/__init__.py b/erpnext/crm/doctype/crm_settings/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.js b/erpnext/crm/doctype/crm_settings/crm_settings.js new file mode 100644 index 00000000000..c6569d8122e --- /dev/null +++ b/erpnext/crm/doctype/crm_settings/crm_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('CRM Settings', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.json b/erpnext/crm/doctype/crm_settings/crm_settings.json new file mode 100644 index 00000000000..95b19fa9822 --- /dev/null +++ b/erpnext/crm/doctype/crm_settings/crm_settings.json @@ -0,0 +1,114 @@ +{ + "actions": [], + "creation": "2021-09-09 17:03:22.754446", + "description": "Settings for Selling Module", + "doctype": "DocType", + "document_type": "Other", + "engine": "InnoDB", + "field_order": [ + "section_break_5", + "campaign_naming_by", + "allow_lead_duplication_based_on_emails", + "column_break_4", + "create_event_on_next_contact_date", + "auto_creation_of_contact", + "opportunity_section", + "close_opportunity_after_days", + "column_break_9", + "create_event_on_next_contact_date_opportunity", + "quotation_section", + "default_valid_till" + ], + "fields": [ + { + "fieldname": "campaign_naming_by", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Campaign Naming By", + "options": "Campaign Name\nNaming Series" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "default_valid_till", + "fieldtype": "Data", + "label": "Default Quotation Validity Days" + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "label": "Lead" + }, + { + "default": "0", + "fieldname": "allow_lead_duplication_based_on_emails", + "fieldtype": "Check", + "label": "Allow Lead Duplication based on Emails" + }, + { + "default": "1", + "fieldname": "auto_creation_of_contact", + "fieldtype": "Check", + "label": "Auto Creation of Contact" + }, + { + "default": "1", + "fieldname": "create_event_on_next_contact_date", + "fieldtype": "Check", + "label": "Create Event on Next Contact Date" + }, + { + "fieldname": "opportunity_section", + "fieldtype": "Section Break", + "label": "Opportunity" + }, + { + "default": "15", + "description": "Auto close Opportunity Replied after the no. of days mentioned above", + "fieldname": "close_opportunity_after_days", + "fieldtype": "Int", + "label": "Close Replied Opportunity After Days" + }, + { + "default": "1", + "fieldname": "create_event_on_next_contact_date_opportunity", + "fieldtype": "Check", + "label": "Create Event on Next Contact Date" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "quotation_section", + "fieldtype": "Section Break", + "label": "Quotation" + } + ], + "icon": "fa fa-cog", + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "migration_hash": "3ae78b12dd1c64d551736c6e82092f90", + "modified": "2021-11-03 09:00:36.883496", + "modified_by": "Administrator", + "module": "CRM", + "name": "CRM Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.py b/erpnext/crm/doctype/crm_settings/crm_settings.py new file mode 100644 index 00000000000..bde52547c95 --- /dev/null +++ b/erpnext/crm/doctype/crm_settings/crm_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class CRMSettings(Document): + pass diff --git a/erpnext/crm/doctype/crm_settings/test_crm_settings.py b/erpnext/crm/doctype/crm_settings/test_crm_settings.py new file mode 100644 index 00000000000..3372c5deb4b --- /dev/null +++ b/erpnext/crm/doctype/crm_settings/test_crm_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + + +class TestCRMSettings(unittest.TestCase): + pass diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index c590523a4f8..9adbe8b6f1d 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -11,6 +11,7 @@ from frappe.utils import ( cint, comma_and, cstr, + get_link_to_form, getdate, has_gravatar, nowdate, @@ -91,13 +92,14 @@ class Lead(SellingController): self.contact_doc.save() def add_calendar_event(self, opts=None, force=False): - super(Lead, self).add_calendar_event({ - "owner": self.lead_owner, - "starts_on": self.contact_date, - "ends_on": self.ends_on or "", - "subject": ('Contact ' + cstr(self.lead_name)), - "description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '') - }, force) + if frappe.db.get_single_value('CRM Settings', 'create_event_on_next_contact_date'): + super(Lead, self).add_calendar_event({ + "owner": self.lead_owner, + "starts_on": self.contact_date, + "ends_on": self.ends_on or "", + "subject": ('Contact ' + cstr(self.lead_name)), + "description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '') + }, force) def update_prospects(self): prospects = frappe.get_all('Prospect Lead', filters={'lead': self.name}, fields=['parent']) @@ -108,12 +110,13 @@ class Lead(SellingController): def check_email_id_is_unique(self): if self.email_id: # validate email is unique - duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]}) - duplicate_leads = [lead.name for lead in duplicate_leads] + if not frappe.db.get_single_value('CRM Settings', 'allow_lead_duplication_based_on_emails'): + duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]}) + duplicate_leads = [frappe.bold(get_link_to_form('Lead', lead.name)) for lead in duplicate_leads] - if duplicate_leads: - frappe.throw(_("Email Address must be unique, already exists for {0}") - .format(comma_and(duplicate_leads)), frappe.DuplicateEntryError) + if duplicate_leads: + frappe.throw(_("Email Address must be unique, already exists for {0}") + .format(comma_and(duplicate_leads)), frappe.DuplicateEntryError) def on_trash(self): frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name) @@ -172,41 +175,42 @@ class Lead(SellingController): self.title = self.company_name or self.lead_name def create_contact(self): - if not self.lead_name: - self.set_full_name() - self.set_lead_name() + if frappe.db.get_single_value('CRM Settings', 'auto_creation_of_contact'): + if not self.lead_name: + self.set_full_name() + self.set_lead_name() - contact = frappe.new_doc("Contact") - contact.update({ - "first_name": self.first_name or self.lead_name, - "last_name": self.last_name, - "salutation": self.salutation, - "gender": self.gender, - "designation": self.designation, - "company_name": self.company_name, - }) - - if self.email_id: - contact.append("email_ids", { - "email_id": self.email_id, - "is_primary": 1 + contact = frappe.new_doc("Contact") + contact.update({ + "first_name": self.first_name or self.lead_name, + "last_name": self.last_name, + "salutation": self.salutation, + "gender": self.gender, + "designation": self.designation, + "company_name": self.company_name, }) - if self.phone: - contact.append("phone_nos", { - "phone": self.phone, - "is_primary_phone": 1 - }) + if self.email_id: + contact.append("email_ids", { + "email_id": self.email_id, + "is_primary": 1 + }) - if self.mobile_no: - contact.append("phone_nos", { - "phone": self.mobile_no, - "is_primary_mobile_no":1 - }) + if self.phone: + contact.append("phone_nos", { + "phone": self.phone, + "is_primary_phone": 1 + }) - contact.insert(ignore_permissions=True) + if self.mobile_no: + contact.append("phone_nos", { + "phone": self.mobile_no, + "is_primary_mobile_no":1 + }) - return contact + contact.insert(ignore_permissions=True) + + return contact @frappe.whitelist() def make_customer(source_name, target_doc=None): diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 0bef80a749d..fcbd4ded398 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -8,6 +8,7 @@ import frappe from frappe import _ from frappe.email.inbox import link_communication_to_document from frappe.model.mapper import get_mapped_doc +from frappe.query_builder import DocType from frappe.utils import cint, cstr, flt, get_fullname from erpnext.setup.utils import get_exchange_rate @@ -28,7 +29,6 @@ class Opportunity(TransactionBase): }) self.make_new_lead_if_required() - self.validate_item_details() self.validate_uom_is_integer("uom", "qty") self.validate_cust_name() @@ -70,21 +70,21 @@ class Opportunity(TransactionBase): """Set lead against new opportunity""" if (not self.get("party_name")) and self.contact_email: # check if customer is already created agains the self.contact_email - customer = frappe.db.sql("""select - distinct `tabDynamic Link`.link_name as customer - from - `tabContact`, - `tabDynamic Link` - where `tabContact`.email_id='{0}' - and - `tabContact`.name=`tabDynamic Link`.parent - and - ifnull(`tabDynamic Link`.link_name, '')<>'' - and - `tabDynamic Link`.link_doctype='Customer' - """.format(self.contact_email), as_dict=True) - if customer and customer[0].customer: - self.party_name = customer[0].customer + dynamic_link, contact = DocType("Dynamic Link"), DocType("Contact") + customer = frappe.qb.from_( + dynamic_link + ).join( + contact + ).on( + (contact.name == dynamic_link.parent) + & (dynamic_link.link_doctype == "Customer") + & (contact.email_id == self.contact_email) + ).select( + dynamic_link.link_name + ).distinct().run(as_dict=True) + + if customer and customer[0].link_name: + self.party_name = customer[0].link_name self.opportunity_from = "Customer" return @@ -191,30 +191,31 @@ class Opportunity(TransactionBase): self.add_calendar_event() def add_calendar_event(self, opts=None, force=False): - if not opts: - opts = frappe._dict() + if frappe.db.get_single_value('CRM Settings', 'create_event_on_next_contact_date_opportunity'): + if not opts: + opts = frappe._dict() - opts.description = "" - opts.contact_date = self.contact_date + opts.description = "" + opts.contact_date = self.contact_date - if self.party_name and self.opportunity_from == 'Customer': - if self.contact_person: - opts.description = 'Contact '+cstr(self.contact_person) - else: - opts.description = 'Contact customer '+cstr(self.party_name) - elif self.party_name and self.opportunity_from == 'Lead': - if self.contact_display: - opts.description = 'Contact '+cstr(self.contact_display) - else: - opts.description = 'Contact lead '+cstr(self.party_name) + if self.party_name and self.opportunity_from == 'Customer': + if self.contact_person: + opts.description = 'Contact '+cstr(self.contact_person) + else: + opts.description = 'Contact customer '+cstr(self.party_name) + elif self.party_name and self.opportunity_from == 'Lead': + if self.contact_display: + opts.description = 'Contact '+cstr(self.contact_display) + else: + opts.description = 'Contact lead '+cstr(self.party_name) - opts.subject = opts.description - opts.description += '. By : ' + cstr(self.contact_by) + opts.subject = opts.description + opts.description += '. By : ' + cstr(self.contact_by) - if self.to_discuss: - opts.description += ' To Discuss : ' + cstr(self.to_discuss) + if self.to_discuss: + opts.description += ' To Discuss : ' + cstr(self.to_discuss) - super(Opportunity, self).add_calendar_event(opts, force) + super(Opportunity, self).add_calendar_event(opts, force) def validate_item_details(self): if not self.get('items'): @@ -363,7 +364,7 @@ def set_multiple_status(names, status): def auto_close_opportunity(): """ auto close the `Replied` Opportunities after 7 days """ - auto_close_after_days = frappe.db.get_single_value("Selling Settings", "close_opportunity_after_days") or 15 + auto_close_after_days = frappe.db.get_single_value("CRM Settings", "close_opportunity_after_days") or 15 opportunities = frappe.db.sql(""" select name from tabOpportunity where status='Replied' and modified=0""" - if self.batch_no and not self.get("allow_negative_stock"): - batch_bal_after_transaction = flt(frappe.db.sql("""select sum(actual_qty) - from `tabStock Ledger Entry` - where is_cancelled =0 and warehouse=%s and item_code=%s and batch_no=%s""", - (self.warehouse, self.item_code, self.batch_no))[0][0]) - - if batch_bal_after_transaction < 0: - frappe.throw(_("Stock balance in Batch {0} will become negative {1} for Item {2} at Warehouse {3}") - .format(self.batch_no, batch_bal_after_transaction, self.item_code, self.warehouse)) - def validate_mandatory(self): mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company'] for k in mandatory: diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index ca92936a1dc..26db2642e4b 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -33,65 +33,6 @@ class TestWarehouse(ERPNextTestCase): self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse) self.assertEqual(child_warehouse.is_group, 0) - def test_warehouse_renaming(self): - create_warehouse("Test Warehouse for Renaming 1", company="_Test Company with perpetual inventory") - account = get_inventory_account("_Test Company with perpetual inventory", "Test Warehouse for Renaming 1 - TCP1") - self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": account})) - - # Rename with abbr - if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - TCP1"): - frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1") - frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - TCP1", "Test Warehouse for Renaming 2 - TCP1") - - self.assertTrue(frappe.db.get_value("Warehouse", - filters={"account": "Test Warehouse for Renaming 1 - TCP1"})) - - # Rename without abbr - if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - TCP1"): - frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1") - - frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1", "Test Warehouse for Renaming 3") - - self.assertTrue(frappe.db.get_value("Warehouse", - filters={"account": "Test Warehouse for Renaming 1 - TCP1"})) - - # Another rename with multiple dashes - if frappe.db.exists("Warehouse", "Test - Warehouse - Company - TCP1"): - frappe.delete_doc("Warehouse", "Test - Warehouse - Company - TCP1") - frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1", "Test - Warehouse - Company") - - def test_warehouse_merging(self): - company = "_Test Company with perpetual inventory" - create_warehouse("Test Warehouse for Merging 1", company=company, - properties={"parent_warehouse": "All Warehouses - TCP1"}) - create_warehouse("Test Warehouse for Merging 2", company=company, - properties={"parent_warehouse": "All Warehouses - TCP1"}) - - make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - TCP1", - qty=1, rate=100, company=company) - make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - TCP1", - qty=1, rate=100, company=company) - - existing_bin_qty = ( - cint(frappe.db.get_value("Bin", - {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - TCP1"}, "actual_qty")) - + cint(frappe.db.get_value("Bin", - {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty")) - ) - - frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - TCP1", - "Test Warehouse for Merging 2 - TCP1", merge=True) - - self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - TCP1")) - - bin_qty = frappe.db.get_value("Bin", - {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty") - - self.assertEqual(bin_qty, existing_bin_qty) - - self.assertTrue(frappe.db.get_value("Warehouse", - filters={"account": "Test Warehouse for Merging 2 - TCP1"})) - def test_unlinking_warehouse_from_item_defaults(self): company = "_Test Company" diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json index 9b9093261c2..05076b51a3e 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.json +++ b/erpnext/stock/doctype/warehouse/warehouse.json @@ -1,7 +1,6 @@ { "actions": [], "allow_import": 1, - "allow_rename": 1, "creation": "2013-03-07 18:50:32", "description": "A logical Warehouse against which stock entries are made.", "doctype": "DocType", @@ -245,7 +244,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2021-04-09 19:54:56.263965", + "modified": "2021-12-03 04:40:06.414630", "modified_by": "Administrator", "module": "Stock", "name": "Warehouse", diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index b9dbc388805..9cfad86f142 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -10,7 +10,6 @@ from frappe.contacts.address_and_contact import load_address_and_contact from frappe.utils import cint, flt from frappe.utils.nestedset import NestedSet -import erpnext from erpnext.stock import get_warehouse_account @@ -68,57 +67,6 @@ class Warehouse(NestedSet): return frappe.db.sql("""select name from `tabWarehouse` where parent_warehouse = %s limit 1""", self.name) - def before_rename(self, old_name, new_name, merge=False): - super(Warehouse, self).before_rename(old_name, new_name, merge) - - # Add company abbr if not provided - new_warehouse = erpnext.encode_company_abbr(new_name, self.company) - - if merge: - if not frappe.db.exists("Warehouse", new_warehouse): - frappe.throw(_("Warehouse {0} does not exist").format(new_warehouse)) - - if self.company != frappe.db.get_value("Warehouse", new_warehouse, "company"): - frappe.throw(_("Both Warehouse must belong to same Company")) - - return new_warehouse - - def after_rename(self, old_name, new_name, merge=False): - super(Warehouse, self).after_rename(old_name, new_name, merge) - - new_warehouse_name = self.get_new_warehouse_name_without_abbr(new_name) - self.db_set("warehouse_name", new_warehouse_name) - - if merge: - self.recalculate_bin_qty(new_name) - - def get_new_warehouse_name_without_abbr(self, name): - company_abbr = frappe.get_cached_value('Company', self.company, "abbr") - parts = name.rsplit(" - ", 1) - - if parts[-1].lower() == company_abbr.lower(): - name = parts[0] - - return name - - def recalculate_bin_qty(self, new_name): - from erpnext.stock.stock_balance import repost_stock - frappe.db.auto_commit_on_many_writes = 1 - existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) - - repost_stock_for_items = frappe.db.sql_list("""select distinct item_code - from tabBin where warehouse=%s""", new_name) - - # Delete all existing bins to avoid duplicate bins for the same item and warehouse - frappe.db.sql("delete from `tabBin` where warehouse=%s", new_name) - - for item_code in repost_stock_for_items: - repost_stock(item_code, new_name) - - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) - frappe.db.auto_commit_on_many_writes = 0 - def convert_to_group_or_ledger(self): if self.is_group: self.convert_to_ledger() diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 9d409827db1..e95c0fcd23b 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -7,9 +7,10 @@ import json import frappe from frappe import _ from frappe.model.meta import get_field_precision -from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now +from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate import erpnext +from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty from erpnext.stock.utils import ( get_incoming_outgoing_rate_for_cancel, get_or_make_bin, @@ -17,19 +18,15 @@ from erpnext.stock.utils import ( ) -# future reposting class NegativeStockError(frappe.ValidationError): pass class SerialNoExistsInFutureTransaction(frappe.ValidationError): pass _exceptions = frappe.local('stockledger_exceptions') -# _exceptions = [] def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): from erpnext.controllers.stock_controller import future_sle_exists if sl_entries: - from erpnext.stock.utils import update_bin - cancel = sl_entries[0].get("is_cancelled") if cancel: validate_cancellation(sl_entries) @@ -64,7 +61,38 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc # preserve previous_qty_after_transaction for qty reposting args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction") - update_bin(args, allow_negative_stock, via_landed_cost_voucher) + is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') + if is_stock_item: + bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse")) + update_bin_qty(bin_name, args) + repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher) + else: + frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code"))) + +def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False): + if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation": + if not args.get("posting_date"): + args["posting_date"] = nowdate() + + if args.get("is_cancelled") and via_landed_cost_voucher: + return + + # Reposts only current voucher SL Entries + # Updates valuation rate, stock value, stock queue for current transaction + update_entries_after({ + "item_code": args.get('item_code'), + "warehouse": args.get('warehouse'), + "posting_date": args.get("posting_date"), + "posting_time": args.get("posting_time"), + "voucher_type": args.get("voucher_type"), + "voucher_no": args.get("voucher_no"), + "sle_id": args.get('name'), + "creation": args.get('creation') + }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) + + # update qty in future sle and Validate negative qty + update_qty_in_future_sle(args, allow_negative_stock) + def get_args_for_future_sle(row): return frappe._dict({ @@ -803,9 +831,9 @@ class update_entries_after(object): def update_bin(self): # update bin for each warehouse for warehouse, data in self.data.items(): - bin_record = get_or_make_bin(self.item_code, warehouse) + bin_name = get_or_make_bin(self.item_code, warehouse) - frappe.db.set_value('Bin', bin_record, { + frappe.db.set_value('Bin', bin_name, { "valuation_rate": data.valuation_rate, "actual_qty": data.qty_after_transaction, "stock_value": data.stock_value @@ -1061,17 +1089,36 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): allow_negative_stock = cint(allow_negative_stock) \ or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) - if (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation") and not allow_negative_stock: - sle = get_future_sle_with_negative_qty(args) - if sle: - message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( - abs(sle[0]["qty_after_transaction"]), - frappe.get_desk_link('Item', args.item_code), - frappe.get_desk_link('Warehouse', args.warehouse), - sle[0]["posting_date"], sle[0]["posting_time"], - frappe.get_desk_link(sle[0]["voucher_type"], sle[0]["voucher_no"])) + if allow_negative_stock: + return + if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"): + return + + neg_sle = get_future_sle_with_negative_qty(args) + if neg_sle: + message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( + abs(neg_sle[0]["qty_after_transaction"]), + frappe.get_desk_link('Item', args.item_code), + frappe.get_desk_link('Warehouse', args.warehouse), + neg_sle[0]["posting_date"], neg_sle[0]["posting_time"], + frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"])) + + frappe.throw(message, NegativeStockError, title='Insufficient Stock') + + + if not args.batch_no: + return + + neg_batch_sle = get_future_sle_with_negative_batch_qty(args) + if neg_batch_sle: + message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( + abs(neg_batch_sle[0]["cumulative_total"]), + frappe.get_desk_link('Batch', args.batch_no), + frappe.get_desk_link('Warehouse', args.warehouse), + neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"], + frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"])) + frappe.throw(message, NegativeStockError, title="Insufficient Stock for Batch") - frappe.throw(message, NegativeStockError, title='Insufficient Stock') def get_future_sle_with_negative_qty(args): return frappe.db.sql(""" @@ -1090,6 +1137,29 @@ def get_future_sle_with_negative_qty(args): limit 1 """, args, as_dict=1) + +def get_future_sle_with_negative_batch_qty(args): + return frappe.db.sql(""" + with batch_ledger as ( + select + posting_date, posting_time, voucher_type, voucher_no, + sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total + from `tabStock Ledger Entry` + where + item_code = %(item_code)s + and warehouse = %(warehouse)s + and batch_no=%(batch_no)s + and is_cancelled = 0 + order by posting_date, posting_time, creation + ) + select * from batch_ledger + where + cumulative_total < 0.0 + and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) + limit 1 + """, args, as_dict=1) + + def _round_off_if_near_zero(number: float, precision: int = 6) -> float: """ Rounds off the number to zero only if number is close to zero for decimal specified in precision. Precision defaults to 6. diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 8031c58b812..72d8098d440 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -187,7 +187,7 @@ def get_bin(item_code, warehouse): bin_obj.flags.ignore_permissions = True return bin_obj -def get_or_make_bin(item_code, warehouse) -> str: +def get_or_make_bin(item_code: str , warehouse: str) -> str: bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse}) if not bin_record: @@ -203,11 +203,12 @@ def get_or_make_bin(item_code, warehouse) -> str: return bin_record def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False): + """WARNING: This function is deprecated. Inline this function instead of using it.""" from erpnext.stock.doctype.bin.bin import update_stock is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') if is_stock_item: - bin_record = get_or_make_bin(args.get("item_code"), args.get("warehouse")) - update_stock(bin_record, args, allow_negative_stock, via_landed_cost_voucher) + bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse")) + update_stock(bin_name, args, allow_negative_stock, via_landed_cost_voucher) else: frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code")))