diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 445378300bb..3f1998a3b39 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -99,7 +99,7 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None): if doctype == "Budget": add_dimension_to_budget_doctype(df.copy(), doc) else: - create_custom_field(doctype, df) + create_custom_field(doctype, df, ignore_validate=True) count += 1 @@ -115,7 +115,7 @@ def add_dimension_to_budget_doctype(df, doc): } ) - create_custom_field("Budget", df) + create_custom_field("Budget", df, ignore_validate=True) property_setter = frappe.db.exists("Property Setter", "Budget-budget_against-options") diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 9a35a247ddd..417611fecdd 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -18,7 +18,6 @@ "automatically_fetch_payment_terms", "column_break_17", "enable_common_party_accounting", - "enable_discount_accounting", "report_setting_section", "use_custom_cash_flow", "deferred_accounting_settings_section", @@ -272,13 +271,6 @@ "fieldtype": "Check", "label": "Create Ledger Entries for Change Amount" }, - { - "default": "0", - "description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account", - "fieldname": "enable_discount_accounting", - "fieldtype": "Check", - "label": "Enable Discount Accounting" - }, { "default": "0", "description": "Learn about Common Party", @@ -354,7 +346,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-02-04 12:32:36.805652", + "modified": "2022-04-08 14:45:06.796418", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 835498176c7..3b125a29862 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -28,7 +28,6 @@ class AccountsSettings(Document): self.validate_stale_days() self.enable_payment_schedule_in_print() - self.toggle_discount_accounting_fields() self.validate_pending_reposts() def validate_stale_days(self): @@ -52,74 +51,6 @@ class AccountsSettings(Document): validate_fields_for_doctype=False, ) - def toggle_discount_accounting_fields(self): - enable_discount_accounting = cint(self.enable_discount_accounting) - - for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]: - make_property_setter( - doctype, - "discount_account", - "hidden", - not (enable_discount_accounting), - "Check", - validate_fields_for_doctype=False, - ) - if enable_discount_accounting: - make_property_setter( - doctype, - "discount_account", - "mandatory_depends_on", - "eval: doc.discount_amount", - "Code", - validate_fields_for_doctype=False, - ) - else: - make_property_setter( - doctype, - "discount_account", - "mandatory_depends_on", - "", - "Code", - validate_fields_for_doctype=False, - ) - - for doctype in ["Sales Invoice", "Purchase Invoice"]: - make_property_setter( - doctype, - "additional_discount_account", - "hidden", - not (enable_discount_accounting), - "Check", - validate_fields_for_doctype=False, - ) - if enable_discount_accounting: - make_property_setter( - doctype, - "additional_discount_account", - "mandatory_depends_on", - "eval: doc.discount_amount", - "Code", - validate_fields_for_doctype=False, - ) - else: - make_property_setter( - doctype, - "additional_discount_account", - "mandatory_depends_on", - "", - "Code", - validate_fields_for_doctype=False, - ) - - make_property_setter( - "Item", - "default_discount_account", - "hidden", - not (enable_discount_accounting), - "Check", - validate_fields_for_doctype=False, - ) - def validate_pending_reposts(self): if self.acc_frozen_upto: check_pending_reposting(self.acc_frozen_upto) diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 96779d75be3..0f617b5dda7 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -5,7 +5,10 @@ import frappe from frappe import _, msgprint from frappe.model.document import Document -from frappe.utils import flt, fmt_money, getdate, nowdate +from frappe.query_builder.custom import ConstantColumn +from frappe.utils import flt, fmt_money, getdate + +import erpnext form_grid_templates = {"journal_entries": "templates/form_grid/bank_reconciliation_grid.html"} @@ -76,6 +79,52 @@ class BankClearance(Document): as_dict=1, ) + loan_disbursement = frappe.qb.DocType("Loan Disbursement") + + loan_disbursements = ( + frappe.qb.from_(loan_disbursement) + .select( + ConstantColumn("Loan Disbursement").as_("payment_document"), + loan_disbursement.name.as_("payment_entry"), + loan_disbursement.disbursed_amount.as_("credit"), + ConstantColumn(0).as_("debit"), + loan_disbursement.reference_number.as_("cheque_number"), + loan_disbursement.reference_date.as_("cheque_date"), + loan_disbursement.disbursement_date.as_("posting_date"), + loan_disbursement.applicant.as_("against_account"), + ) + .where(loan_disbursement.docstatus == 1) + .where(loan_disbursement.disbursement_date >= self.from_date) + .where(loan_disbursement.disbursement_date <= self.to_date) + .where(loan_disbursement.clearance_date.isnull()) + .where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account])) + .orderby(loan_disbursement.disbursement_date) + .orderby(loan_disbursement.name, frappe.qb.desc) + ).run(as_dict=1) + + loan_repayment = frappe.qb.DocType("Loan Repayment") + + loan_repayments = ( + frappe.qb.from_(loan_repayment) + .select( + ConstantColumn("Loan Repayment").as_("payment_document"), + loan_repayment.name.as_("payment_entry"), + loan_repayment.amount_paid.as_("debit"), + ConstantColumn(0).as_("credit"), + loan_repayment.reference_number.as_("cheque_number"), + loan_repayment.reference_date.as_("cheque_date"), + loan_repayment.applicant.as_("against_account"), + loan_repayment.posting_date, + ) + .where(loan_repayment.docstatus == 1) + .where(loan_repayment.clearance_date.isnull()) + .where(loan_repayment.posting_date >= self.from_date) + .where(loan_repayment.posting_date <= self.to_date) + .where(loan_repayment.payment_account.isin([self.bank_account, self.account])) + .orderby(loan_repayment.posting_date) + .orderby(loan_repayment.name, frappe.qb.desc) + ).run(as_dict=1) + pos_sales_invoices, pos_purchase_invoices = [], [] if self.include_pos_transactions: pos_sales_invoices = frappe.db.sql( @@ -114,20 +163,29 @@ class BankClearance(Document): entries = sorted( list(payment_entries) - + list(journal_entries + list(pos_sales_invoices) + list(pos_purchase_invoices)), - key=lambda k: k["posting_date"] or getdate(nowdate()), + + list(journal_entries) + + list(pos_sales_invoices) + + list(pos_purchase_invoices) + + list(loan_disbursements) + + list(loan_repayments), + key=lambda k: getdate(k["posting_date"]), ) self.set("payment_entries", []) self.total_amount = 0.0 + default_currency = erpnext.get_default_currency() for d in entries: row = self.append("payment_entries", {}) amount = flt(d.get("debit", 0)) - flt(d.get("credit", 0)) + if not d.get("account_currency"): + d.account_currency = default_currency + formatted_amount = fmt_money(abs(amount), 2, d.account_currency) d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr")) + d.posting_date = getdate(d.posting_date) d.pop("credit") d.pop("debit") diff --git a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py index 706fbbe245c..c1e55f6f723 100644 --- a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py @@ -1,9 +1,96 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe import unittest +import frappe +from frappe.utils import add_months, getdate + +from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.loan_management.doctype.loan.test_loan import ( + create_loan, + create_loan_accounts, + create_loan_type, + create_repayment_entry, + make_loan_disbursement_entry, +) + class TestBankClearance(unittest.TestCase): - pass + @classmethod + def setUpClass(cls): + make_bank_account() + create_loan_accounts() + create_loan_masters() + add_transactions() + + # Basic test case to test if bank clearance tool doesn't break + # Detailed test can be added later + def test_bank_clearance(self): + bank_clearance = frappe.get_doc("Bank Clearance") + bank_clearance.account = "_Test Bank Clearance - _TC" + bank_clearance.from_date = add_months(getdate(), -1) + bank_clearance.to_date = getdate() + bank_clearance.get_payment_entries() + self.assertEqual(len(bank_clearance.payment_entries), 3) + + +def make_bank_account(): + if not frappe.db.get_value("Account", "_Test Bank Clearance - _TC"): + frappe.get_doc( + { + "doctype": "Account", + "account_type": "Bank", + "account_name": "_Test Bank Clearance", + "company": "_Test Company", + "parent_account": "Bank Accounts - _TC", + } + ).insert() + + +def create_loan_masters(): + create_loan_type( + "Clearance Loan", + 2000000, + 13.5, + 25, + 0, + 5, + "Cash", + "_Test Bank Clearance - _TC", + "_Test Bank Clearance - _TC", + "Loan Account - _TC", + "Interest Income Account - _TC", + "Penalty Income Account - _TC", + ) + + +def add_transactions(): + make_payment_entry() + make_loan() + + +def make_loan(): + loan = create_loan( + "_Test Customer", + "Clearance Loan", + 280000, + "Repay Over Number of Periods", + 20, + applicant_type="Customer", + ) + loan.submit() + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=getdate()) + repayment_entry = create_repayment_entry(loan.name, "_Test Customer", getdate(), loan.loan_amount) + repayment_entry.save() + repayment_entry.submit() + + +def make_payment_entry(): + pi = make_purchase_invoice(supplier="_Test Supplier", qty=1, rate=690) + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank Clearance - _TC") + pe.reference_no = "Conrad Oct 18" + pe.reference_date = "2018-10-24" + pe.insert() + pe.submit() diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index a5f9e24e15c..a1d86e2219a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -669,7 +669,7 @@ class PurchaseInvoice(BuyingController): exchange_rate_map, net_rate_map = get_purchase_document_details(self) enable_discount_accounting = cint( - frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") + frappe.db.get_single_value("Buying Settings", "enable_discount_accounting") ) provisional_accounting_for_non_stock_items = cint( frappe.db.get_value( @@ -1159,7 +1159,7 @@ class PurchaseInvoice(BuyingController): # tax table gl entries valuation_tax = {} enable_discount_accounting = cint( - frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") + frappe.db.get_single_value("Buying Settings", "enable_discount_accounting") ) for tax in self.get("taxes"): @@ -1252,7 +1252,7 @@ class PurchaseInvoice(BuyingController): def enable_discount_accounting(self): if not hasattr(self, "_enable_discount_accounting"): self._enable_discount_accounting = cint( - frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") + frappe.db.get_single_value("Buying Settings", "enable_discount_accounting") ) return self._enable_discount_accounting @@ -1369,7 +1369,9 @@ class PurchaseInvoice(BuyingController): if ( not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment ): - round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(self.company) + round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + self.company, "Purchase Invoice", self.name + ) gl_entries.append( self.get_gl_dict( diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 59bd637e412..30d26acf3a2 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -5,6 +5,7 @@ import unittest import frappe +from frappe.tests.utils import change_settings from frappe.utils import add_days, cint, flt, getdate, nowdate, today import erpnext @@ -336,8 +337,8 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) + @change_settings("Buying Settings", {"enable_discount_accounting": 1}) def test_purchase_invoice_with_discount_accounting_enabled(self): - enable_discount_accounting() discount_account = create_account( account_name="Discount Account", @@ -353,10 +354,10 @@ class TestPurchaseInvoice(unittest.TestCase): ] check_gl_entries(self, pi.name, expected_gle, nowdate()) - enable_discount_accounting(enable=0) + @change_settings("Buying Settings", {"enable_discount_accounting": 1}) def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self): - enable_discount_accounting() + additional_discount_account = create_account( account_name="Discount Account", parent_account="Indirect Expenses - _TC", @@ -1588,12 +1589,6 @@ def unlink_payment_on_cancel_of_invoice(enable=1): accounts_settings.save() -def enable_discount_accounting(enable=1): - accounts_settings = frappe.get_doc("Accounts Settings") - accounts_settings.enable_discount_accounting = enable - accounts_settings.save() - - def make_purchase_invoice(**args): pi = frappe.new_doc("Purchase Invoice") args = frappe._dict(args) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 1efd3dca0d3..f0880c19e3c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1051,7 +1051,7 @@ class SalesInvoice(SellingController): def make_tax_gl_entries(self, gl_entries): enable_discount_accounting = cint( - frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") + frappe.db.get_single_value("Selling Settings", "enable_discount_accounting") ) for tax in self.get("taxes"): @@ -1097,7 +1097,7 @@ class SalesInvoice(SellingController): def make_item_gl_entries(self, gl_entries): # income account gl entries enable_discount_accounting = cint( - frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") + frappe.db.get_single_value("Selling Settings", "enable_discount_accounting") ) for item in self.get("items"): @@ -1276,7 +1276,7 @@ class SalesInvoice(SellingController): def enable_discount_accounting(self): if not hasattr(self, "_enable_discount_accounting"): self._enable_discount_accounting = cint( - frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") + frappe.db.get_single_value("Selling Settings", "enable_discount_accounting") ) return self._enable_discount_accounting @@ -1466,7 +1466,9 @@ class SalesInvoice(SellingController): and self.base_rounding_adjustment and not self.is_internal_transfer() ): - round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(self.company) + round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + self.company, "Sales Invoice", self.name + ) gl_entries.append( self.get_gl_dict( diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index caa70d00efb..f2a696d565a 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -7,6 +7,7 @@ import unittest import frappe from frappe.model.dynamic_links import get_dynamic_link_map from frappe.model.naming import make_autoname +from frappe.tests.utils import change_settings from frappe.utils import add_days, flt, getdate, nowdate import erpnext @@ -1977,6 +1978,13 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][2], gle.credit) def test_rounding_adjustment_3(self): + from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( + create_dimension, + disable_dimension, + ) + + create_dimension() + si = create_sales_invoice(do_not_save=True) si.items = [] for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]: @@ -2004,6 +2012,10 @@ class TestSalesInvoice(unittest.TestCase): "included_in_print_rate": 1, }, ) + + si.cost_center = "_Test Cost Center 2 - _TC" + si.location = "Block 1" + si.save() si.submit() self.assertEqual(si.net_total, 4007.16) @@ -2039,6 +2051,18 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(debit_credit_diff, 0) + round_off_gle = frappe.db.get_value( + "GL Entry", + {"voucher_type": "Sales Invoice", "voucher_no": si.name, "account": "Round Off - _TC"}, + ["cost_center", "location"], + as_dict=1, + ) + + self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC") + self.assertEqual(round_off_gle.location, "Block 1") + + disable_dimension() + def test_sales_invoice_with_shipping_rule(self): from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule @@ -2684,12 +2708,8 @@ class TestSalesInvoice(unittest.TestCase): sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC" ) + @change_settings("Selling Settings", {"enable_discount_accounting": 1}) def test_sales_invoice_with_discount_accounting_enabled(self): - from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( - enable_discount_accounting, - ) - - enable_discount_accounting() discount_account = create_account( account_name="Discount Account", @@ -2705,14 +2725,10 @@ class TestSalesInvoice(unittest.TestCase): ] check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) - enable_discount_accounting(enable=0) + @change_settings("Selling Settings", {"enable_discount_accounting": 1}) def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self): - from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( - enable_discount_accounting, - ) - enable_discount_accounting() additional_discount_account = create_account( account_name="Discount Account", parent_account="Indirect Expenses - _TC", @@ -2743,7 +2759,6 @@ class TestSalesInvoice(unittest.TestCase): ] check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) - enable_discount_accounting(enable=0) def test_asset_depreciation_on_sale_with_pro_rata(self): """ diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.py b/erpnext/accounts/doctype/tax_rule/tax_rule.py index 27b78e9fab6..5bfca96bb14 100644 --- a/erpnext/accounts/doctype/tax_rule/tax_rule.py +++ b/erpnext/accounts/doctype/tax_rule/tax_rule.py @@ -163,10 +163,15 @@ def get_party_details(party, party_type, args=None): def get_tax_template(posting_date, args): """Get matching tax rule""" args = frappe._dict(args) + from_date = to_date = posting_date + if not posting_date: + from_date = "1900-01-01" + to_date = "4000-01-01" + conditions = [ """(from_date is null or from_date <= '{0}') - and (to_date is null or to_date >= '{0}')""".format( - posting_date + and (to_date is null or to_date >= '{1}')""".format( + from_date, to_date ) ] diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index f52e517f730..89034ebb8c8 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -355,7 +355,7 @@ def raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_ def make_round_off_gle(gl_map, debit_credit_diff, precision): round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( - gl_map[0].company + gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no ) round_off_account_exists = False round_off_gle = frappe._dict() @@ -392,14 +392,43 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): } ) + update_accounting_dimensions(round_off_gle) + if not round_off_account_exists: gl_map.append(round_off_gle) -def get_round_off_account_and_cost_center(company): +def update_accounting_dimensions(round_off_gle): + dimensions = get_accounting_dimensions() + meta = frappe.get_meta(round_off_gle["voucher_type"]) + has_all_dimensions = True + + for dimension in dimensions: + if not meta.has_field(dimension): + has_all_dimensions = False + + if dimensions and has_all_dimensions: + dimension_values = frappe.db.get_value( + round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions, as_dict=1 + ) + + for dimension in dimensions: + round_off_gle[dimension] = dimension_values.get(dimension) + + +def get_round_off_account_and_cost_center(company, voucher_type, voucher_no): round_off_account, round_off_cost_center = frappe.get_cached_value( "Company", company, ["round_off_account", "round_off_cost_center"] ) or [None, None] + + meta = frappe.get_meta(voucher_type) + + # Give first preference to parent cost center for round off GLE + if meta.has_field("cost_center"): + parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center") + if parent_cost_center: + round_off_cost_center = parent_cost_center + if not round_off_account: frappe.throw(_("Please mention Round Off Account in Company")) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index b0b3049d487..db741d97e10 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -831,9 +831,9 @@ def get_party_shipping_address(doctype, name): "where " "dl.link_doctype=%s " "and dl.link_name=%s " - 'and dl.parenttype="Address" ' + "and dl.parenttype='Address' " "and ifnull(ta.disabled, 0) = 0 and" - '(ta.address_type="Shipping" or ta.is_shipping_address=1) ' + "(ta.address_type='Shipping' or ta.is_shipping_address=1) " "order by ta.is_shipping_address desc, ta.address_type desc limit 1", (doctype, name), ) @@ -881,11 +881,11 @@ def get_default_contact(doctype, name): """ SELECT dl.parent, c.is_primary_contact, c.is_billing_contact FROM `tabDynamic Link` dl - INNER JOIN tabContact c ON c.name = dl.parent + INNER JOIN `tabContact` c ON c.name = dl.parent WHERE dl.link_doctype=%s AND dl.link_name=%s AND - dl.parenttype = "Contact" + dl.parenttype = 'Contact' ORDER BY is_primary_contact DESC, is_billing_contact DESC """, (doctype, name), diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 50321baa2e2..89a94487163 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -20,6 +20,7 @@ "maintain_same_rate", "allow_multiple_items", "bill_for_rejected_quantity_in_purchase_invoice", + "enable_discount_accounting", "subcontract", "backflush_raw_materials_of_subcontract_based_on", "column_break_11", @@ -133,6 +134,13 @@ { "fieldname": "column_break_12", "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account", + "fieldname": "enable_discount_accounting", + "fieldtype": "Check", + "label": "Enable Discount Accounting for Buying" } ], "icon": "fa fa-cog", @@ -140,7 +148,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-01-27 17:57:58.367048", + "modified": "2022-04-14 15:56:42.340223", "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 5507254bbc2..c52b59e4c0c 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.py +++ b/erpnext/buying/doctype/buying_settings/buying_settings.py @@ -5,10 +5,15 @@ import frappe +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.model.document import Document +from frappe.utils import cint class BuyingSettings(Document): + def on_update(self): + self.toggle_discount_accounting_fields() + def validate(self): for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]: frappe.db.set_default(key, self.get(key, "")) @@ -21,3 +26,60 @@ class BuyingSettings(Document): self.get("supp_master_name") == "Naming Series", hide_name_field=False, ) + + def toggle_discount_accounting_fields(self): + enable_discount_accounting = cint(self.enable_discount_accounting) + + make_property_setter( + "Purchase Invoice Item", + "discount_account", + "hidden", + not (enable_discount_accounting), + "Check", + validate_fields_for_doctype=False, + ) + if enable_discount_accounting: + make_property_setter( + "Purchase Invoice Item", + "discount_account", + "mandatory_depends_on", + "eval: doc.discount_amount", + "Code", + validate_fields_for_doctype=False, + ) + else: + make_property_setter( + "Purchase Invoice Item", + "discount_account", + "mandatory_depends_on", + "", + "Code", + validate_fields_for_doctype=False, + ) + + make_property_setter( + "Purchase Invoice", + "additional_discount_account", + "hidden", + not (enable_discount_accounting), + "Check", + validate_fields_for_doctype=False, + ) + if enable_discount_accounting: + make_property_setter( + "Purchase Invoice", + "additional_discount_account", + "mandatory_depends_on", + "eval: doc.discount_amount", + "Code", + validate_fields_for_doctype=False, + ) + else: + make_property_setter( + "Purchase Invoice", + "additional_discount_account", + "mandatory_depends_on", + "", + "Code", + validate_fields_for_doctype=False, + ) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f20df09d36d..78645e0d4ff 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1079,9 +1079,14 @@ class AccountsController(TransactionBase): return amount, base_amount def make_discount_gl_entries(self, gl_entries): - enable_discount_accounting = cint( - frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") - ) + if self.doctype == "Purchase Invoice": + enable_discount_accounting = cint( + frappe.db.get_single_value("Buying Settings", "enable_discount_accounting") + ) + elif self.doctype == "Sales Invoice": + enable_discount_accounting = cint( + frappe.db.get_single_value("Selling Settings", "enable_discount_accounting") + ) if enable_discount_accounting: if self.doctype == "Purchase Invoice": diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 96c730c668c..19b4d68e1cf 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -54,11 +54,11 @@ class Opportunity(TransactionBase): self.calculate_totals() def map_fields(self): - for field in self.meta.fields: - if not self.get(field.fieldname): + for field in self.meta.get_valid_columns(): + if not self.get(field) and frappe.db.field_exists(self.opportunity_from, field): try: - value = frappe.db.get_value(self.opportunity_from, self.party_name, field.fieldname) - frappe.db.set(self, field.fieldname, value) + value = frappe.db.get_value(self.opportunity_from, self.party_name, field) + frappe.db.set(self, field, value) except Exception: continue diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index fefb2e59f01..220ce1dbd89 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -753,7 +753,7 @@ class BOM(WebsiteGenerator): bom_item.include_item_in_manufacturing, bom_item.sourced_by_supplier, bom_item.stock_qty / ifnull(bom.quantity, 1) AS qty_consumed_per_unit - FROM `tabBOM Explosion Item` bom_item, tabBOM bom + FROM `tabBOM Explosion Item` bom_item, `tabBOM` bom WHERE bom_item.parent = bom.name AND bom.name = %s diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 474383dd52c..95be791c963 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -365,5 +365,6 @@ erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022 erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances erpnext.patches.v13_0.create_gst_custom_fields_in_quotation erpnext.patches.v13_0.copy_custom_field_filters_to_website_item +erpnext.patches.v14_0.discount_accounting_separation erpnext.patches.v14_0.delete_employee_transfer_property_doctype erpnext.patches.v13_0.create_accounting_dimensions_in_orders \ No newline at end of file diff --git a/erpnext/patches/v14_0/discount_accounting_separation.py b/erpnext/patches/v14_0/discount_accounting_separation.py new file mode 100644 index 00000000000..fd498052d8c --- /dev/null +++ b/erpnext/patches/v14_0/discount_accounting_separation.py @@ -0,0 +1,9 @@ +import frappe + + +def execute(): + doc = frappe.get_doc("Accounts Settings") + discount_account = doc.enable_discount_accounting + if discount_account: + for doctype in ["Buying Settings", "Selling Settings"]: + frappe.db.set_value(doctype, doctype, "enable_discount_accounting", 1, update_modified=False) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index a3abe8670a1..0f73c5f9bc1 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -553,6 +553,7 @@ def validate_totals(einvoice): + flt(value_details["CgstVal"]) + flt(value_details["SgstVal"]) + flt(value_details["IgstVal"]) + + flt(value_details["CesVal"]) + flt(value_details["OthChrg"]) + flt(value_details["RndOffAmt"]) - flt(value_details["Discount"]) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 446faaa708b..062c2ef5c59 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -1219,7 +1219,7 @@ def make_fixtures(company=None): try: doc = frappe.get_doc(d) doc.flags.ignore_permissions = True - doc.insert() + doc.insert(ignore_if_duplicate=True) except frappe.NameError: frappe.clear_messages() except frappe.DuplicateEntryError: diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 7c4a3f63dcc..005e24cfbe3 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -27,7 +27,8 @@ "column_break_5", "allow_multiple_items", "allow_against_multiple_purchase_orders", - "hide_tax_id" + "hide_tax_id", + "enable_discount_accounting" ], "fields": [ { @@ -164,6 +165,13 @@ "fieldname": "editable_bundle_item_rates", "fieldtype": "Check", "label": "Calculate Product Bundle Price based on Child Items' Rates" + }, + { + "default": "0", + "description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account", + "fieldname": "enable_discount_accounting", + "fieldtype": "Check", + "label": "Enable Discount Accounting for Selling" } ], "icon": "fa fa-cog", @@ -171,7 +179,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-02-04 15:41:59.939261", + "modified": "2022-04-14 16:01:29.405642", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index 29e4712be38..6c098942510 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -14,6 +14,7 @@ class SellingSettings(Document): def on_update(self): self.toggle_hide_tax_id() self.toggle_editable_rate_for_bundle_items() + self.toggle_discount_accounting_fields() def validate(self): for key in [ @@ -58,3 +59,60 @@ class SellingSettings(Document): "Check", validate_fields_for_doctype=False, ) + + def toggle_discount_accounting_fields(self): + enable_discount_accounting = cint(self.enable_discount_accounting) + + make_property_setter( + "Sales Invoice Item", + "discount_account", + "hidden", + not (enable_discount_accounting), + "Check", + validate_fields_for_doctype=False, + ) + if enable_discount_accounting: + make_property_setter( + "Sales Invoice Item", + "discount_account", + "mandatory_depends_on", + "eval: doc.discount_amount", + "Code", + validate_fields_for_doctype=False, + ) + else: + make_property_setter( + "Sales Invoice Item", + "discount_account", + "mandatory_depends_on", + "", + "Code", + validate_fields_for_doctype=False, + ) + + make_property_setter( + "Sales Invoice", + "additional_discount_account", + "hidden", + not (enable_discount_accounting), + "Check", + validate_fields_for_doctype=False, + ) + if enable_discount_accounting: + make_property_setter( + "Sales Invoice", + "additional_discount_account", + "mandatory_depends_on", + "eval: doc.discount_amount", + "Code", + validate_fields_for_doctype=False, + ) + else: + make_property_setter( + "Sales Invoice", + "additional_discount_account", + "mandatory_depends_on", + "", + "Code", + validate_fields_for_doctype=False, + ) diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index 562f7b9e12f..bcd31ada83e 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -5,6 +5,8 @@ import frappe from frappe import _ from frappe.model.document import Document +from frappe.query_builder import Criterion +from frappe.query_builder.functions import Cast_ from frappe.utils import getdate @@ -48,35 +50,57 @@ class ItemPrice(Document): ) def check_duplicates(self): - conditions = ( - """where item_code = %(item_code)s and price_list = %(price_list)s and name != %(name)s""" - ) - for field in [ + item_price = frappe.qb.DocType("Item Price") + + query = ( + frappe.qb.from_(item_price) + .select(item_price.price_list_rate) + .where( + (item_price.item_code == self.item_code) + & (item_price.price_list == self.price_list) + & (item_price.name != self.name) + ) + ) + data_fields = ( "uom", "valid_from", "valid_upto", - "packing_unit", "customer", "supplier", "batch_no", - ]: - if self.get(field): - conditions += " and {0} = %({0})s ".format(field) - else: - conditions += "and (isnull({0}) or {0} = '')".format(field) - - price_list_rate = frappe.db.sql( - """ - select price_list_rate - from `tabItem Price` - {conditions} - """.format( - conditions=conditions - ), - self.as_dict(), ) + number_fields = ["packing_unit"] + + for field in data_fields: + if self.get(field): + query = query.where(item_price[field] == self.get(field)) + else: + query = query.where( + Criterion.any( + [ + item_price[field].isnull(), + Cast_(item_price[field], "varchar") == "", + ] + ) + ) + + for field in number_fields: + if self.get(field): + query = query.where(item_price[field] == self.get(field)) + else: + query = query.where( + Criterion.any( + [ + item_price[field].isnull(), + item_price[field] == 0, + ] + ) + ) + + price_list_rate = query.run(as_dict=True) + if price_list_rate: frappe.throw( _( diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index c4aa8a4711b..27a6eaf08b5 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1167,7 +1167,7 @@ class StockEntry(StockController): from `tabItem` i LEFT JOIN `tabItem Default` id ON i.name=id.parent and id.company=%s where i.name=%s and i.disabled=0 - and (i.end_of_life is null or i.end_of_life='0000-00-00' or i.end_of_life > %s)""", + and (i.end_of_life is null or i.end_of_life<'1900-01-01' or i.end_of_life > %s)""", (self.company, args.get("item_code"), nowdate()), as_dict=1, ) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index b4fac829355..5850ec7be66 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -2,12 +2,12 @@ # See license.txt import json -from datetime import timedelta from uuid import uuid4 import frappe from frappe.core.page.permission_manager.permission_manager import reset from frappe.custom.doctype.property_setter.property_setter import make_property_setter +from frappe.query_builder.functions import CombineDatetime from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, today from frappe.utils.data import add_to_date @@ -1126,6 +1126,63 @@ class TestStockLedgerEntry(FrappeTestCase): # original amount self.assertEqual(50, _get_stock_credit(final_consumption)) + def test_tie_breaking(self): + frappe.flags.dont_execute_stock_reposts = True + self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts") + + item = make_item().name + warehouse = "_Test Warehouse - _TC" + + posting_date = "2022-01-01" + posting_time = "00:00:01" + sle = frappe.qb.DocType("Stock Ledger Entry") + + def ordered_qty_after_transaction(): + return ( + frappe.qb.from_(sle) + .select("qty_after_transaction") + .where((sle.item_code == item) & (sle.warehouse == warehouse) & (sle.is_cancelled == 0)) + .orderby(CombineDatetime(sle.posting_date, sle.posting_time)) + .orderby(sle.creation) + ).run(pluck=True) + + first = make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=10, + posting_time=posting_time, + posting_date=posting_date, + do_not_submit=True, + ) + second = make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=1, + posting_date=posting_date, + posting_time=posting_time, + do_not_submit=True, + ) + + first.submit() + second.submit() + + self.assertEqual([10, 11], ordered_qty_after_transaction()) + + first.cancel() + self.assertEqual([1], ordered_qty_after_transaction()) + + backdated = make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=1, + posting_date="2021-01-01", + posting_time=posting_time, + ) + self.assertEqual([1, 2], ordered_qty_after_transaction()) + + backdated.cancel() + self.assertEqual([1], ordered_qty_after_transaction()) + def create_repack_entry(**args): args = frappe._dict(args) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index a781479cf63..7e5c231d9c4 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -8,9 +8,8 @@ from typing import Optional, Set, Tuple import frappe from frappe import _ from frappe.model.meta import get_field_precision -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import CombineDatetime, Sum from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate -from pypika import CustomFunction import erpnext from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty @@ -1158,16 +1157,15 @@ def get_batch_incoming_rate( item_code, warehouse, batch_no, posting_date, posting_time, creation=None ): - Timestamp = CustomFunction("timestamp", ["date", "time"]) - sle = frappe.qb.DocType("Stock Ledger Entry") - timestamp_condition = Timestamp(sle.posting_date, sle.posting_time) < Timestamp( + timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime( posting_date, posting_time ) if creation: timestamp_condition |= ( - Timestamp(sle.posting_date, sle.posting_time) == Timestamp(posting_date, posting_time) + CombineDatetime(sle.posting_date, sle.posting_time) + == CombineDatetime(posting_date, posting_time) ) & (sle.creation < creation) batch_details = ( diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index d40218e1439..2120304097b 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -7,6 +7,7 @@ from typing import Dict, Optional import frappe from frappe import _ +from frappe.query_builder.functions import CombineDatetime from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime import erpnext @@ -143,12 +144,10 @@ def get_stock_balance( def get_serial_nos_data_after_transactions(args): - from pypika import CustomFunction serial_nos = set() args = frappe._dict(args) sle = frappe.qb.DocType("Stock Ledger Entry") - Timestamp = CustomFunction("timestamp", ["date", "time"]) stock_ledger_entries = ( frappe.qb.from_(sle) @@ -157,7 +156,8 @@ def get_serial_nos_data_after_transactions(args): (sle.item_code == args.item_code) & (sle.warehouse == args.warehouse) & ( - Timestamp(sle.posting_date, sle.posting_time) < Timestamp(args.posting_date, args.posting_time) + CombineDatetime(sle.posting_date, sle.posting_time) + < CombineDatetime(args.posting_date, args.posting_time) ) & (sle.is_cancelled == 0) )