diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 1987c8340d..7227b95818 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -366,7 +366,7 @@ def update_outstanding_amt( if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]: ref_doc = frappe.get_doc(against_voucher_type, against_voucher) - # Didn't use db_set for optimisation purpose + # Didn't use db_set for optimization purpose ref_doc.outstanding_amount = bal frappe.db.set_value(against_voucher_type, against_voucher, "outstanding_amount", bal) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 48edda9032..4618d0807c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -181,7 +181,11 @@ class PaymentEntry(AccountsController): frappe.throw(_("Party is mandatory")) _party_name = "title" if self.party_type == "Shareholder" else self.party_type.lower() + "_name" - self.party_name = frappe.db.get_value(self.party_type, self.party, _party_name) + + if frappe.db.has_column(self.party_type, _party_name): + self.party_name = frappe.db.get_value(self.party_type, self.party, _party_name) + else: + self.party_name = frappe.db.get_value(self.party_type, self.party, "name") if self.party: if not self.party_balance: @@ -295,6 +299,9 @@ class PaymentEntry(AccountsController): def validate_reference_documents(self): valid_reference_doctypes = self.get_valid_reference_doctypes() + if not valid_reference_doctypes: + return + for d in self.get("references"): if not d.allocated_amount: continue @@ -362,7 +369,7 @@ class PaymentEntry(AccountsController): if not d.allocated_amount: continue - if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Fees"): + if d.reference_doctype in ("Sales Invoice", "Purchase Invoice"): outstanding_amount, is_return = frappe.get_cached_value( d.reference_doctype, d.reference_name, ["outstanding_amount", "is_return"] ) @@ -1201,7 +1208,7 @@ def get_outstanding_reference_documents(args): party_account_currency = get_account_currency(args.get("party_account")) company_currency = frappe.get_cached_value("Company", args.get("company"), "default_currency") - # Get positive outstanding sales /purchase invoices/ Fees + # Get positive outstanding sales /purchase invoices condition = "" if args.get("voucher_type") and args.get("voucher_no"): condition = " and voucher_type={0} and voucher_no={1}".format( @@ -1597,10 +1604,11 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre elif reference_doctype != "Journal Entry": if not total_amount: if party_account_currency == company_currency: - total_amount = ref_doc.base_grand_total + # for handling cases that don't have multi-currency (base field) + total_amount = ref_doc.get("grand_total") or ref_doc.get("base_grand_total") exchange_rate = 1 else: - total_amount = ref_doc.grand_total + total_amount = ref_doc.get("grand_total") if not exchange_rate: # Get the exchange rate from the original ref doc # or get it based on the posting date of the ref doc. @@ -1611,7 +1619,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre if reference_doctype in ("Sales Invoice", "Purchase Invoice"): outstanding_amount = ref_doc.get("outstanding_amount") else: - outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid) + outstanding_amount = flt(total_amount) - flt(ref_doc.get("advance_paid")) else: # Get the exchange rate based on the posting date of the ref doc. @@ -1629,16 +1637,23 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre @frappe.whitelist() -def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=None): +def get_payment_entry( + dt, dn, party_amount=None, bank_account=None, bank_amount=None, party_type=None, payment_type=None +): reference_doc = None doc = frappe.get_doc(dt, dn) if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0: frappe.throw(_("Can only make payment against unbilled {0}").format(dt)) - party_type = set_party_type(dt) + if not party_type: + party_type = set_party_type(dt) + party_account = set_party_account(dt, dn, doc, party_type) party_account_currency = set_party_account_currency(dt, party_account, doc) - payment_type = set_payment_type(dt, doc) + + if not payment_type: + payment_type = set_payment_type(dt, doc) + grand_total, outstanding_amount = set_grand_total_and_outstanding_amount( party_amount, dt, party_account_currency, doc ) @@ -1788,8 +1803,6 @@ def set_party_account(dt, dn, doc, party_type): party_account = get_party_account_based_on_invoice_discounting(dn) or doc.debit_to elif dt == "Purchase Invoice": party_account = doc.credit_to - elif dt == "Fees": - party_account = doc.receivable_account else: party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company) return party_account @@ -1805,8 +1818,7 @@ def set_party_account_currency(dt, party_account, doc): def set_payment_type(dt, doc): if ( - dt == "Sales Order" - or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0) + dt == "Sales Order" or (dt in ("Sales Invoice", "Dunning") and doc.outstanding_amount > 0) ) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0): payment_type = "Receive" else: @@ -1824,18 +1836,15 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre else: grand_total = doc.rounded_total or doc.grand_total outstanding_amount = doc.outstanding_amount - elif dt == "Fees": - grand_total = doc.grand_total - outstanding_amount = doc.outstanding_amount elif dt == "Dunning": grand_total = doc.grand_total outstanding_amount = doc.grand_total else: if party_account_currency == doc.company_currency: - grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total) + grand_total = flt(doc.get("base_rounded_total") or doc.get("base_grand_total")) else: - grand_total = flt(doc.get("rounded_total") or doc.grand_total) - outstanding_amount = grand_total - flt(doc.advance_paid) + grand_total = flt(doc.get("rounded_total") or doc.get("grand_total")) + outstanding_amount = doc.get("outstanding_amount") or (grand_total - flt(doc.advance_paid)) return grand_total, outstanding_amount diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js index 98f3420d87..1d596c1bfb 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -36,6 +36,15 @@ frappe.ui.form.on('POS Closing Entry', { }); set_html_data(frm); + + if (frm.doc.docstatus == 1) { + if (!frm.doc.posting_date) { + frm.set_value("posting_date", frappe.datetime.nowdate()); + } + if (!frm.doc.posting_time) { + frm.set_value("posting_time", frappe.datetime.now_time()); + } + } }, refresh: function(frm) { diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json index d6e35c6a50..9d15e6cf35 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json @@ -11,6 +11,7 @@ "period_end_date", "column_break_3", "posting_date", + "posting_time", "pos_opening_entry", "status", "section_break_5", @@ -51,7 +52,6 @@ "fieldtype": "Datetime", "in_list_view": 1, "label": "Period End Date", - "read_only": 1, "reqd": 1 }, { @@ -219,6 +219,13 @@ "fieldtype": "Small Text", "label": "Error", "read_only": 1 + }, + { + "fieldname": "posting_time", + "fieldtype": "Time", + "label": "Posting Time", + "no_copy": 1, + "reqd": 1 } ], "is_submittable": 1, @@ -228,10 +235,11 @@ "link_fieldname": "pos_closing_entry" } ], - "modified": "2021-10-20 16:19:25.340565", + "modified": "2022-08-01 11:37:14.991228", "modified_by": "Administrator", "module": "Accounts", "name": "POS Closing Entry", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -278,5 +286,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file 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 49aab0d0bb..655c4ec003 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py @@ -15,6 +15,9 @@ from erpnext.controllers.status_updater import StatusUpdater class POSClosingEntry(StatusUpdater): def validate(self): + self.posting_date = self.posting_date or frappe.utils.nowdate() + self.posting_time = self.posting_time or frappe.utils.nowtime() + 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")) diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json index d762087078..a059455647 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "posting_date", + "posting_time", "merge_invoices_based_on", "column_break_3", "pos_closing_entry", @@ -105,12 +106,19 @@ "label": "Customer Group", "mandatory_depends_on": "eval:doc.merge_invoices_based_on == 'Customer Group'", "options": "Customer Group" + }, + { + "fieldname": "posting_time", + "fieldtype": "Time", + "label": "Posting Time", + "no_copy": 1, + "reqd": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-09-14 11:17:19.001142", + "modified": "2022-08-01 11:36:42.456429", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Merge Log", @@ -173,5 +181,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file 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 5003a1d6a8..81a234a20a 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 @@ -9,7 +9,7 @@ from frappe import _ from frappe.core.page.background_jobs.background_jobs import get_info from frappe.model.document import Document from frappe.model.mapper import map_child_doc, map_doc -from frappe.utils import cint, flt, getdate, nowdate +from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime from frappe.utils.background_jobs import enqueue from frappe.utils.scheduler import is_scheduler_inactive @@ -99,6 +99,7 @@ class POSInvoiceMergeLog(Document): sales_invoice.is_consolidated = 1 sales_invoice.set_posting_time = 1 sales_invoice.posting_date = getdate(self.posting_date) + sales_invoice.posting_time = get_time(self.posting_time) sales_invoice.save() sales_invoice.submit() @@ -115,6 +116,7 @@ class POSInvoiceMergeLog(Document): credit_note.is_consolidated = 1 credit_note.set_posting_time = 1 credit_note.posting_date = getdate(self.posting_date) + credit_note.posting_time = get_time(self.posting_time) # TODO: return could be against multiple sales invoice which could also have been consolidated? # credit_note.return_against = self.consolidated_invoice credit_note.save() @@ -402,6 +404,9 @@ def create_merge_logs(invoice_by_customer, closing_entry=None): merge_log.posting_date = ( getdate(closing_entry.get("posting_date")) if closing_entry else nowdate() ) + merge_log.posting_time = ( + get_time(closing_entry.get("posting_time")) if closing_entry else nowtime() + ) merge_log.customer = customer merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index de3927e45b..fea81e9c27 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -575,7 +575,6 @@ class PurchaseInvoice(BuyingController): self.make_supplier_gl_entry(gl_entries) self.make_item_gl_entries(gl_entries) - self.make_discount_gl_entries(gl_entries) if self.check_asset_cwip_enabled(): self.get_asset_gl_entry(gl_entries) @@ -807,7 +806,7 @@ class PurchaseInvoice(BuyingController): ) if not item.is_fixed_asset: - dummy, amount = self.get_amount_and_base_amount(item, enable_discount_accounting) + dummy, amount = self.get_amount_and_base_amount(item, None) else: amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount")) @@ -1165,7 +1164,7 @@ class PurchaseInvoice(BuyingController): ) for tax in self.get("taxes"): - amount, base_amount = self.get_tax_amounts(tax, enable_discount_accounting) + amount, base_amount = self.get_tax_amounts(tax, None) if tax.category in ("Total", "Valuation and Total") and flt(base_amount): account_currency = get_account_currency(tax.account_head) @@ -1791,4 +1790,6 @@ def make_purchase_receipt(source_name, target_doc=None): target_doc, ) + doc.set_onload("ignore_price_list", True) + return doc diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index e55d3a70af..0a4f25b876 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -338,59 +338,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): 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): - - discount_account = create_account( - account_name="Discount Account", - parent_account="Indirect Expenses - _TC", - company="_Test Company", - ) - pi = make_purchase_invoice(discount_account=discount_account, rate=45) - - expected_gle = [ - ["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()], - ["Creditors - _TC", 0.0, 225.0, nowdate()], - ["Discount Account - _TC", 0.0, 25.0, nowdate()], - ] - - check_gl_entries(self, pi.name, expected_gle, nowdate()) - - @change_settings("Buying Settings", {"enable_discount_accounting": 1}) - def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self): - - additional_discount_account = create_account( - account_name="Discount Account", - parent_account="Indirect Expenses - _TC", - company="_Test Company", - ) - - pi = make_purchase_invoice(do_not_save=1, parent_cost_center="Main - _TC") - pi.apply_discount_on = "Grand Total" - pi.additional_discount_account = additional_discount_account - pi.additional_discount_percentage = 10 - pi.disable_rounded_total = 1 - pi.append( - "taxes", - { - "charge_type": "On Net Total", - "account_head": "_Test Account VAT - _TC", - "cost_center": "Main - _TC", - "description": "Test", - "rate": 10, - }, - ) - pi.submit() - - expected_gle = [ - ["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()], - ["_Test Account VAT - _TC", 25.0, 0.0, nowdate()], - ["Creditors - _TC", 0.0, 247.5, nowdate()], - ["Discount Account - _TC", 0.0, 27.5, nowdate()], - ] - - check_gl_entries(self, pi.name, expected_gle, nowdate()) - def test_purchase_invoice_change_naming_series(self): pi = frappe.copy_doc(test_records[1]) pi.insert() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index adcbb83e8d..73ec051c6d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -479,9 +479,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e is_cash_or_non_trade_discount() { this.frm.set_df_property("additional_discount_account", "hidden", 1 - this.frm.doc.is_cash_or_non_trade_discount); + this.frm.set_df_property("additional_discount_account", "reqd", this.frm.doc.is_cash_or_non_trade_discount); + if (!this.frm.doc.is_cash_or_non_trade_discount) { this.frm.set_value("additional_discount_account", ""); } + + this.calculate_taxes_and_totals(); } }; diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 19a234d9df..4008863e9b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1033,22 +1033,6 @@ class SalesInvoice(SellingController): ) ) - if self.apply_discount_on == "Grand Total" and self.get("is_cash_or_discount_account"): - gl_entries.append( - self.get_gl_dict( - { - "account": self.additional_discount_account, - "against": self.debit_to, - "debit": self.base_discount_amount, - "debit_in_account_currency": self.discount_amount, - "cost_center": self.cost_center, - "project": self.project, - }, - self.currency, - item=self, - ) - ) - def make_tax_gl_entries(self, gl_entries): enable_discount_accounting = cint( frappe.db.get_single_value("Selling Settings", "enable_discount_accounting") 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 a519d8be73..6004e2b19b 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -318,7 +318,6 @@ def get_advance_vouchers( "is_cancelled": 0, "party_type": party_type, "party": ["in", parties], - "against_voucher": ["is", "not set"], } if company: diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index e937edbeb2..63242e83fe 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -178,6 +178,11 @@ class ReceivablePayableReport(object): key = (ple.against_voucher_type, ple.against_voucher_no, ple.party) row = self.voucher_balance.get(key) + + if not row: + # no invoice, this is an invoice / stand-alone payment / credit note + row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party)) + return row def update_voucher_balance(self, ple): @@ -187,7 +192,11 @@ class ReceivablePayableReport(object): if not row: return - amount = ple.amount + # amount in "Party Currency", if its supplied. If not, amount in company currency + if self.filters.get(scrub(self.party_type)): + amount = ple.amount_in_account_currency + else: + amount = ple.amount amount_in_account_currency = ple.amount_in_account_currency # update voucher @@ -685,7 +694,7 @@ class ReceivablePayableReport(object): ple.party, ple.posting_date, ple.due_date, - ple.account_currency.as_("currency"), + ple.account_currency, ple.amount, ple.amount_in_account_currency, ) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index edddbbce21..bac8beed2e 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -1,19 +1,27 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, getdate, today from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order -class TestAccountsReceivable(unittest.TestCase): - def test_accounts_receivable(self): +class TestAccountsReceivable(FrappeTestCase): + def setUp(self): frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'") + frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'") + frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'") frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'") frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'") + def tearDown(self): + frappe.db.rollback() + + def test_accounts_receivable(self): filters = { "company": "_Test Company 2", "based_on_payment_terms": 1, @@ -66,6 +74,50 @@ class TestAccountsReceivable(unittest.TestCase): ], ) + def test_payment_againt_po_in_receivable_report(self): + """ + Payments made against Purchase Order will show up as outstanding amount + """ + + so = make_sales_order( + company="_Test Company 2", + customer="_Test Customer 2", + warehouse="Finished Goods - _TC2", + currency="EUR", + debit_to="Debtors - _TC2", + income_account="Sales - _TC2", + expense_account="Cost of Goods Sold - _TC2", + cost_center="Main - _TC2", + ) + + pe = get_payment_entry(so.doctype, so.name) + pe = pe.save().submit() + + filters = { + "company": "_Test Company 2", + "based_on_payment_terms": 0, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + report = execute(filters) + + expected_data_after_payment = [0, 1000, 0, -1000] + + row = report[1][0] + self.assertEqual( + expected_data_after_payment, + [ + row.invoiced, + row.paid, + row.credit_note, + row.outstanding, + ], + ) + def make_sales_invoice(): frappe.set_user("Administrator") diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 526ea9d6e2..54af225915 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -616,7 +616,7 @@ class GrossProfitGenerator(object): previous_stock_value = len(my_sle) > i + 1 and flt(my_sle[i + 1].stock_value) or 0.0 if previous_stock_value: - return (previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) + return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) else: return flt(row.qty) * self.get_average_buying_rate(row, item_code) else: 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 4e29ee53ea..31a4837d46 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -15,9 +15,12 @@ frappe.ui.form.on("Request for Quotation",{ frm.fields_dict["suppliers"].grid.get_field("contact").get_query = function(doc, cdt, cdn) { let d = locals[cdt][cdn]; return { - query: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_supplier_contacts", - filters: {'supplier': d.supplier} - } + query: "frappe.contacts.doctype.contact.contact.contact_query", + filters: { + link_doctype: "Supplier", + link_name: d.supplier || "" + } + }; } }, 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 3ef57bb70f..ee28eb6ce2 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -286,18 +286,6 @@ def get_list_context(context=None): return list_context -@frappe.whitelist() -@frappe.validate_and_sanitize_search_inputs -def get_supplier_contacts(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql( - """select `tabContact`.name from `tabContact`, `tabDynamic Link` - where `tabDynamic Link`.link_doctype = 'Supplier' and (`tabDynamic Link`.link_name=%(name)s - and `tabDynamic Link`.link_name like %(txt)s) and `tabContact`.name = `tabDynamic Link`.parent - limit %(page_len)s offset %(start)s""", - {"start": start, "page_len": page_len, "txt": "%%%s%%" % txt, "name": filters.get("supplier")}, - ) - - @frappe.whitelist() def make_supplier_quotation_from_rfq(source_name, target_doc=None, for_supplier=None): def postprocess(source, target_doc): diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py index c772c1a1b1..d13d9701f3 100644 --- a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py +++ b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py @@ -4,6 +4,8 @@ # Decompiled by https://python-decompiler.com +import copy + import frappe from frappe.tests.utils import FrappeTestCase @@ -11,10 +13,12 @@ from erpnext.buying.report.subcontracted_item_to_be_received.subcontracted_item_ execute, ) from erpnext.controllers.tests.test_subcontracting_controller import ( + get_rm_items, get_subcontracting_order, make_service_item, + make_stock_in_entry, + make_stock_transfer_entry, ) -from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( make_subcontracting_receipt, ) @@ -36,15 +40,18 @@ class TestSubcontractedItemToBeReceived(FrappeTestCase): sco = get_subcontracting_order( service_items=service_items, supplier_warehouse="_Test Warehouse 1 - _TC" ) - make_stock_entry( - item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100 - ) - make_stock_entry( - item_code="_Test Item Home Desktop 100", - target="_Test Warehouse 1 - _TC", - qty=100, - basic_rate=100, + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), ) + make_subcontracting_receipt_against_sco(sco.name) sco.reload() col, data = execute( diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 70d2bc68a1..e689d567a1 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1109,17 +1109,17 @@ class AccountsController(TransactionBase): frappe.db.get_single_value("Selling Settings", "enable_discount_accounting") ) + if self.doctype == "Purchase Invoice": + dr_or_cr = "credit" + rev_dr_cr = "debit" + supplier_or_customer = self.supplier + + else: + dr_or_cr = "debit" + rev_dr_cr = "credit" + supplier_or_customer = self.customer + if enable_discount_accounting: - if self.doctype == "Purchase Invoice": - dr_or_cr = "credit" - rev_dr_cr = "debit" - supplier_or_customer = self.supplier - - else: - dr_or_cr = "debit" - rev_dr_cr = "credit" - supplier_or_customer = self.customer - for item in self.get("items"): if item.get("discount_amount") and item.get("discount_account"): discount_amount = item.discount_amount * item.qty @@ -1173,18 +1173,22 @@ class AccountsController(TransactionBase): ) ) - if self.get("discount_amount") and self.get("additional_discount_account"): - gl_entries.append( - self.get_gl_dict( - { - "account": self.additional_discount_account, - "against": supplier_or_customer, - dr_or_cr: self.discount_amount, - "cost_center": self.cost_center, - }, - item=self, - ) + if ( + (enable_discount_accounting or self.get("is_cash_or_non_trade_discount")) + and self.get("additional_discount_account") + and self.get("discount_amount") + ): + gl_entries.append( + self.get_gl_dict( + { + "account": self.additional_discount_account, + "against": supplier_or_customer, + dr_or_cr: self.discount_amount, + "cost_center": self.cost_center, + }, + item=self, ) + ) def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield): from erpnext.controllers.status_updater import get_allowance_for diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index e27718a9b4..36bed36484 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -36,6 +36,10 @@ class QualityInspectionNotSubmittedError(frappe.ValidationError): pass +class BatchExpiredError(frappe.ValidationError): + pass + + class StockController(AccountsController): def validate(self): super(StockController, self).validate() @@ -77,6 +81,10 @@ class StockController(AccountsController): def validate_serialized_batch(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + is_material_issue = False + if self.doctype == "Stock Entry" and self.purpose == "Material Issue": + is_material_issue = True + for d in self.get("items"): if hasattr(d, "serial_no") and hasattr(d, "batch_no") and d.serial_no and d.batch_no: serial_nos = frappe.get_all( @@ -93,6 +101,9 @@ class StockController(AccountsController): ) ) + if is_material_issue: + continue + if flt(d.qty) > 0.0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2: expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date") @@ -100,7 +111,8 @@ class StockController(AccountsController): frappe.throw( _("Row #{0}: The batch {1} has already expired.").format( d.idx, get_link_to_form("Batch", d.get("batch_no")) - ) + ), + BatchExpiredError, ) def clean_serial_nos(self): @@ -310,7 +322,13 @@ class StockController(AccountsController): ) if ( self.doctype - not in ("Purchase Receipt", "Purchase Invoice", "Stock Reconciliation", "Stock Entry") + not in ( + "Purchase Receipt", + "Purchase Invoice", + "Stock Reconciliation", + "Stock Entry", + "Subcontracting Receipt", + ) and not is_expense_account ): frappe.throw( @@ -374,9 +392,24 @@ class StockController(AccountsController): def update_inventory_dimensions(self, row, sl_dict) -> None: dimensions = get_evaluated_inventory_dimension(row, sl_dict, parent_doc=self) for dimension in dimensions: - if dimension and row.get(dimension.source_fieldname): + if not dimension: + continue + + if row.get(dimension.source_fieldname): sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname) + if not sl_dict.get(dimension.target_fieldname) and dimension.fetch_from_parent: + sl_dict[dimension.target_fieldname] = self.get(dimension.fetch_from_parent) + + # Get value based on doctype name + if not sl_dict.get(dimension.target_fieldname): + fieldname = frappe.get_cached_value( + "DocField", {"parent": self.doctype, "options": dimension.fetch_from_parent}, "fieldname" + ) + + if fieldname and self.get(fieldname): + sl_dict[dimension.target_fieldname] = self.get(fieldname) + def make_sl_entries(self, sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): from erpnext.stock.stock_ledger import make_sl_entries diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 2a2f8f562e..1372c89d47 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -490,7 +490,7 @@ class SubcontractingController(StockController): row.item_code, row.get(self.subcontract_data.order_field), ) and transfer_item.qty > 0: - qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0 + qty = flt(self.__get_qty_based_on_material_transfer(row, transfer_item)) transfer_item.qty -= qty self.__add_supplied_item(row, transfer_item.get("item_details"), qty) @@ -720,6 +720,25 @@ class SubcontractingController(StockController): sco_doc = frappe.get_doc("Subcontracting Order", sco) sco_doc.update_status() + def set_missing_values_in_additional_costs(self): + self.total_additional_costs = sum(flt(item.amount) for item in self.get("additional_costs")) + + if self.total_additional_costs: + if self.distribute_additional_costs_based_on == "Amount": + total_amt = sum(flt(item.amount) for item in self.get("items")) + for item in self.items: + item.additional_cost_per_qty = ( + (item.amount * self.total_additional_costs) / total_amt + ) / item.qty + else: + total_qty = sum(flt(item.qty) for item in self.get("items")) + additional_cost_per_qty = self.total_additional_costs / total_qty + for item in self.items: + item.additional_cost_per_qty = additional_cost_per_qty + else: + for item in self.items: + item.additional_cost_per_qty = 0 + @frappe.whitelist() def get_current_stock(self): if self.doctype in ["Purchase Receipt", "Subcontracting Receipt"]: @@ -730,7 +749,7 @@ class SubcontractingController(StockController): {"item_code": item.rm_item_code, "warehouse": self.supplier_warehouse}, "actual_qty", ) - item.current_stock = flt(actual_qty) or 0 + item.current_stock = flt(actual_qty) @property def sub_contracted_items(self): diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 26b8db4a81..b73b8adcfe 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -37,6 +37,11 @@ class calculate_taxes_and_totals(object): self.set_discount_amount() self.apply_discount_amount() + # Update grand total as per cash and non trade discount + if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"): + self.doc.grand_total -= self.doc.discount_amount + self.doc.base_grand_total -= self.doc.base_discount_amount + self.calculate_shipping_charges() if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]: @@ -500,9 +505,6 @@ class calculate_taxes_and_totals(object): else: self.doc.grand_total = flt(self.doc.net_total) - if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"): - self.doc.grand_total -= self.doc.discount_amount - if self.doc.get("taxes"): self.doc.total_taxes_and_charges = flt( self.doc.grand_total - self.doc.net_total - flt(self.doc.rounding_adjustment), @@ -597,16 +599,16 @@ class calculate_taxes_and_totals(object): if not self.doc.apply_discount_on: frappe.throw(_("Please select Apply Discount On")) + self.doc.base_discount_amount = flt( + self.doc.discount_amount * self.doc.conversion_rate, self.doc.precision("base_discount_amount") + ) + if self.doc.apply_discount_on == "Grand Total" and self.doc.get( "is_cash_or_non_trade_discount" ): self.discount_amount_applied = True return - self.doc.base_discount_amount = flt( - self.doc.discount_amount * self.doc.conversion_rate, self.doc.precision("base_discount_amount") - ) - total_for_discount_amount = self.get_total_for_discount_amount() taxes = self.doc.get("taxes") net_total = 0 diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index 4fab8058b8..bc503f5440 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -36,6 +36,36 @@ class TestSubcontractingController(FrappeTestCase): sco.remove_empty_rows() self.assertEqual((len_before - 1), len(sco.service_items)) + def test_set_missing_values_in_additional_costs(self): + sco = get_subcontracting_order(do_not_submit=1) + + rate_without_additional_cost = sco.items[0].rate + amount_without_additional_cost = sco.items[0].amount + + additional_amount = 120 + sco.append( + "additional_costs", + { + "expense_account": "Cost of Goods Sold - _TC", + "description": "Test", + "amount": additional_amount, + }, + ) + sco.save() + + additional_cost_per_qty = additional_amount / sco.items[0].qty + + self.assertEqual(sco.items[0].additional_cost_per_qty, additional_cost_per_qty) + self.assertEqual(rate_without_additional_cost + additional_cost_per_qty, sco.items[0].rate) + self.assertEqual(amount_without_additional_cost + additional_amount, sco.items[0].amount) + + sco.additional_costs = [] + sco.save() + + self.assertEqual(sco.items[0].additional_cost_per_qty, 0) + self.assertEqual(rate_without_additional_cost, sco.items[0].rate) + self.assertEqual(amount_without_additional_cost, sco.items[0].amount) + def test_create_raw_materials_supplied(self): sco = get_subcontracting_order() sco.supplied_items = None diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py index 7d676e4235..cd4aaee2c4 100644 --- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py +++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py @@ -12,7 +12,9 @@ from decimal import Decimal import frappe from bs4 import BeautifulSoup as bs from frappe import _ -from frappe.custom.doctype.custom_field.custom_field import create_custom_field +from frappe.custom.doctype.custom_field.custom_field import ( + create_custom_fields as _create_custom_fields, +) from frappe.model.document import Document from frappe.utils.data import format_datetime @@ -577,22 +579,25 @@ class TallyMigration(Document): new_year.save() oldest_year = new_year - def create_custom_fields(doctypes): - tally_guid_df = { - "fieldtype": "Data", - "fieldname": "tally_guid", - "read_only": 1, - "label": "Tally GUID", - } - tally_voucher_no_df = { - "fieldtype": "Data", - "fieldname": "tally_voucher_no", - "read_only": 1, - "label": "Tally Voucher Number", - } - for df in [tally_guid_df, tally_voucher_no_df]: - for doctype in doctypes: - create_custom_field(doctype, df) + def create_custom_fields(): + _create_custom_fields( + { + ("Journal Entry", "Purchase Invoice", "Sales Invoice"): [ + { + "fieldtype": "Data", + "fieldname": "tally_guid", + "read_only": 1, + "label": "Tally GUID", + }, + { + "fieldtype": "Data", + "fieldname": "tally_voucher_no", + "read_only": 1, + "label": "Tally Voucher Number", + }, + ] + } + ) def create_price_list(): frappe.get_doc( @@ -628,7 +633,7 @@ class TallyMigration(Document): create_fiscal_years(vouchers) create_price_list() - create_custom_fields(["Journal Entry", "Purchase Invoice", "Sales Invoice"]) + create_custom_fields() total = len(vouchers) is_last = False diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py index 2e18776a92..4aa98aab56 100644 --- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py +++ b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py @@ -6,7 +6,7 @@ from urllib.parse import urlparse import frappe from frappe import _ -from frappe.custom.doctype.custom_field.custom_field import create_custom_field +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.model.document import Document from frappe.utils.nestedset import get_root_of @@ -19,27 +19,24 @@ class WoocommerceSettings(Document): def create_delete_custom_fields(self): if self.enable_sync: - custom_fields = {} - # create - for doctype in ["Customer", "Sales Order", "Item", "Address"]: - df = dict( - fieldname="woocommerce_id", - label="Woocommerce ID", - fieldtype="Data", - read_only=1, - print_hide=1, - ) - create_custom_field(doctype, df) - - for doctype in ["Customer", "Address"]: - df = dict( - fieldname="woocommerce_email", - label="Woocommerce Email", - fieldtype="Data", - read_only=1, - print_hide=1, - ) - create_custom_field(doctype, df) + create_custom_fields( + { + ("Customer", "Sales Order", "Item", "Address"): dict( + fieldname="woocommerce_id", + label="Woocommerce ID", + fieldtype="Data", + read_only=1, + print_hide=1, + ), + ("Customer", "Address"): dict( + fieldname="woocommerce_email", + label="Woocommerce Email", + fieldtype="Data", + read_only=1, + print_hide=1, + ), + } + ) if not frappe.get_value("Item Group", {"name": _("WooCommerce Products")}): item_group = frappe.new_doc("Item Group") diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py index fd9f74e8c9..fd0f783575 100644 --- a/erpnext/erpnext_integrations/exotel_integration.py +++ b/erpnext/erpnext_integrations/exotel_integration.py @@ -26,6 +26,7 @@ def handle_incoming_call(**kwargs): except Exception as e: frappe.db.rollback() exotel_settings.log_error("Error in Exotel incoming call") + frappe.db.commit() @frappe.whitelist(allow_guest=True) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index c4f0c59c38..a08feb4447 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -520,6 +520,10 @@ accounting_dimension_doctypes = [ "Purchase Order", "Purchase Receipt", "Sales Order", + "Subcontracting Order", + "Subcontracting Order Item", + "Subcontracting Receipt", + "Subcontracting Receipt Item", ] # get matching queries for Bank Reconciliation diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index 4680456211..6d62aefdca 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -135,7 +135,11 @@ def calculate_accrual_amount_for_demand_loans( def make_accrual_interest_entry_for_demand_loans( posting_date, process_loan_interest, open_loans=None, loan_type=None, accrual_type="Regular" ): - query_filters = {"status": ("in", ["Disbursed", "Partially Disbursed"]), "docstatus": 1} + query_filters = { + "status": ("in", ["Disbursed", "Partially Disbursed"]), + "docstatus": 1, + "is_term_loan": 0, + } if loan_type: query_filters.update({"loan_type": loan_type}) @@ -232,6 +236,7 @@ def get_term_loans(date, term_loan=None, loan_type=None): AND l.is_term_loan =1 AND rs.payment_date <= %s AND rs.is_accrued=0 {0} + AND rs.interest_amount > 0 AND l.status = 'Disbursed' ORDER BY rs.payment_date""".format( condition diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index 09d4429712..3dc6b0f900 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -415,7 +415,7 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No }, "Maintenance Schedule Item": { "doctype": "Maintenance Visit Purpose", - "condition": lambda doc: doc.item_name == item_name, + "condition": lambda doc: doc.item_name == item_name if item_name else True, "field_map": {"sales_person": "service_person"}, "postprocess": update_serial, }, diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index b29f6710e1..70637d3ef2 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -189,8 +189,8 @@ class BOM(WebsiteGenerator): self.validate_transfer_against() self.set_routing_operations() self.validate_operations() - self.update_exploded_items(save=False) self.calculate_cost() + self.update_exploded_items(save=False) self.update_stock_qty() self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False) self.validate_scrap_items() diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index a190cc7430..27f3cc905b 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -611,6 +611,34 @@ class TestBOM(FrappeTestCase): bom.reload() self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name) + def test_exploded_items_rate(self): + rm_item = make_item( + properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89} + ).name + fg_item = make_item(properties={"is_stock_item": 1}).name + + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + bom = make_bom(item=fg_item, raw_materials=[rm_item], do_not_save=True) + + bom.rm_cost_as_per = "Last Purchase Rate" + bom.save() + self.assertEqual(bom.items[0].base_rate, 89) + self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate) + + bom.rm_cost_as_per = "Price List" + bom.save() + self.assertEqual(bom.items[0].base_rate, 0.0) + self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate) + + bom.rm_cost_as_per = "Valuation Rate" + bom.save() + self.assertEqual(bom.items[0].base_rate, 99) + self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate) + + bom.submit() + self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate) + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json index 0a8ae7b4a7..c5266119dc 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.json +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json @@ -184,6 +184,7 @@ "in_list_view": 1, "label": "Rate", "options": "currency", + "read_only": 1, "reqd": 1 }, { @@ -288,7 +289,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-05-19 02:32:43.785470", + "modified": "2022-07-28 10:20:51.559010", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Item", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 70ccb78278..2cdf8d3ea9 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -482,7 +482,6 @@ class ProductionPlan(Document): "bom_no", "stock_uom", "bom_level", - "production_plan_item", "schedule_date", ]: if row.get(field): @@ -639,6 +638,9 @@ class ProductionPlan(Document): sub_assembly_items_store = [] # temporary store to process all subassembly items for row in self.po_items: + if not row.item_code: + frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx)) + bom_data = [] get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty) self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 040e791e00..e2415ad848 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -11,8 +11,9 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import ( get_warehouse_list, ) from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError +from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry as make_se_from_wo from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order -from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, @@ -583,9 +584,6 @@ class TestProductionPlan(FrappeTestCase): Test Prod Plan impact via: SO -> Prod Plan -> WO -> SE -> SE (cancel) """ from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record - from erpnext.manufacturing.doctype.work_order.work_order import ( - make_stock_entry as make_se_from_wo, - ) make_stock_entry( item_code="Raw Material Item 1", target="Work In Progress - _TC", qty=2, basic_rate=100 @@ -629,9 +627,6 @@ class TestProductionPlan(FrappeTestCase): def test_production_plan_pending_qty_independent_items(self): "Test Prod Plan impact if items are added independently (no from SO or MR)." from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record - from erpnext.manufacturing.doctype.work_order.work_order import ( - make_stock_entry as make_se_from_wo, - ) make_stock_entry( item_code="Raw Material Item 1", target="Work In Progress - _TC", qty=2, basic_rate=100 @@ -728,6 +723,57 @@ class TestProductionPlan(FrappeTestCase): for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items): self.assertEqual(po_item.name, subassy_item.production_plan_item) + def test_produced_qty_for_multi_level_bom_item(self): + # Create Items and BOMs + rm_item = make_item(properties={"is_stock_item": 1}).name + sub_assembly_item = make_item(properties={"is_stock_item": 1}).name + fg_item = make_item(properties={"is_stock_item": 1}).name + + make_stock_entry( + item_code=rm_item, + qty=60, + to_warehouse="Work In Progress - _TC", + rate=99, + purpose="Material Receipt", + ) + + make_bom(item=sub_assembly_item, raw_materials=[rm_item], rm_qty=3) + make_bom(item=fg_item, raw_materials=[sub_assembly_item], rm_qty=4) + + # Step - 1: Create Production Plan + pln = create_production_plan(item_code=fg_item, planned_qty=5, skip_getting_mr_items=1) + pln.get_sub_assembly_items() + + # Step - 2: Create Work Orders + pln.make_work_order() + work_orders = frappe.get_all("Work Order", filters={"production_plan": pln.name}, pluck="name") + sa_wo = fg_wo = None + for work_order in work_orders: + wo_doc = frappe.get_doc("Work Order", work_order) + if wo_doc.production_plan_item: + wo_doc.update( + {"wip_warehouse": "Work In Progress - _TC", "fg_warehouse": "Finished Goods - _TC"} + ) + fg_wo = wo_doc.name + else: + wo_doc.update( + {"wip_warehouse": "Work In Progress - _TC", "fg_warehouse": "Work In Progress - _TC"} + ) + sa_wo = wo_doc.name + wo_doc.submit() + + # Step - 3: Complete Work Orders + se = frappe.get_doc(make_se_from_wo(sa_wo, "Manufacture")) + se.submit() + + se = frappe.get_doc(make_se_from_wo(fg_wo, "Manufacture")) + se.submit() + + # Step - 4: Check Production Plan Item Produced Qty + pln.load_from_db() + self.assertEqual(pln.status, "Completed") + self.assertEqual(pln.po_items[0].produced_qty, 5) + def create_production_plan(**args): """ diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index b556d9974a..a53c42c5ec 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -26,6 +26,8 @@ from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.utils import get_bin +test_dependencies = ["BOM"] + class TestWorkOrder(FrappeTestCase): def setUp(self): diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e5beacde2b..d92353aca4 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -310,4 +310,5 @@ erpnext.patches.v14_0.crm_ux_cleanup erpnext.patches.v14_0.remove_india_localisation # 14-07-2022 erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation erpnext.patches.v14_0.remove_hr_and_payroll_modules # 20-07-2022 -erpnext.patches.v14_0.fix_crm_no_of_employees \ No newline at end of file +erpnext.patches.v14_0.fix_crm_no_of_employees +erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes \ No newline at end of file diff --git a/erpnext/patches/v14_0/create_accounting_dimensions_in_subcontracting_doctypes.py b/erpnext/patches/v14_0/create_accounting_dimensions_in_subcontracting_doctypes.py new file mode 100644 index 0000000000..b349c07f6d --- /dev/null +++ b/erpnext/patches/v14_0/create_accounting_dimensions_in_subcontracting_doctypes.py @@ -0,0 +1,47 @@ +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_field + + +def execute(): + accounting_dimensions = frappe.db.get_all( + "Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"] + ) + + if not accounting_dimensions: + return + + count = 1 + for d in accounting_dimensions: + + if count % 2 == 0: + insert_after_field = "dimension_col_break" + else: + insert_after_field = "accounting_dimensions_section" + + for doctype in [ + "Subcontracting Order", + "Subcontracting Order Item", + "Subcontracting Receipt", + "Subcontracting Receipt Item", + ]: + + field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname}) + + if field: + continue + + df = { + "fieldname": d.fieldname, + "label": d.label, + "fieldtype": "Link", + "options": d.document_type, + "insert_after": insert_after_field, + } + + try: + create_custom_field(doctype, df, ignore_validate=True) + frappe.clear_cache(doctype=doctype) + except Exception: + pass + + count += 1 diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index e78e4b6577..a2be936768 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -379,7 +379,7 @@ def get_users_for_project(doctype, txt, searchfield, start, page_len, filters): {fcond} {mcond} order by (case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end), - (case when locate(%(_txt)s, full_name) > 0 then locate(%(_txt)s, full_name) else 99999 end) + (case when locate(%(_txt)s, full_name) > 0 then locate(%(_txt)s, full_name) else 99999 end), idx desc, name, full_name limit %(page_len)s offset %(start)s""".format( diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 16b0b4a866..4c3e9dcf0a 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -39,6 +39,12 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { this._calculate_taxes_and_totals(); this.calculate_discount_amount(); + // # Update grand total as per cash and non trade discount + if (this.frm.doc.apply_discount_on == "Grand Total" && this.frm.doc.is_cash_or_non_trade_discount) { + this.frm.doc.grand_total -= this.frm.doc.discount_amount; + this.frm.doc.base_grand_total -= this.frm.doc.base_discount_amount; + } + await this.calculate_shipping_charges(); // Advance calculation applicable to Sales /Purchase Invoice @@ -633,6 +639,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { this.frm.doc.base_discount_amount = flt(this.frm.doc.discount_amount * this.frm.doc.conversion_rate, precision("base_discount_amount")); + if (this.frm.doc.apply_discount_on == "Grand Total" && this.frm.doc.is_cash_or_non_trade_discount) { + return; + } + var total_for_discount_amount = this.get_total_for_discount_amount(); var net_total = 0; // calculate item amount after Discount Amount diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 13d5069ea6..c12a9f86ae 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -253,16 +253,20 @@ def get_past_order_list(search_term, status, limit=20): "POS Invoice", filters={"customer": ["like", "%{}%".format(search_term)], "status": status}, fields=fields, + page_length=limit, ) invoices_by_name = frappe.db.get_all( "POS Invoice", filters={"name": ["like", "%{}%".format(search_term)], "status": status}, fields=fields, + page_length=limit, ) invoice_list = invoices_by_customer + invoices_by_name elif status: - invoice_list = frappe.db.get_all("POS Invoice", filters={"status": status}, fields=fields) + invoice_list = frappe.db.get_all( + "POS Invoice", filters={"status": status}, fields=fields, page_length=limit + ) return invoice_list diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index eacf480ef8..e7dd211c0f 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -524,7 +524,8 @@ erpnext.PointOfSale.ItemCart = class { const currency = this.events.get_frm().doc.currency; const taxes_html = taxes.map(t => { if (t.tax_amount_after_discount_amount == 0.0) return; - const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`; + // if tax rate is 0, don't print it. + const description = /[0-9]+/.test(t.description) ? t.description : ((t.rate != 0) ? `${t.description} @ ${t.rate}%`: t.description); return `