diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index dc97189835..8c31df387a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1689,13 +1689,42 @@ def get_outstanding_reference_documents(args, validate=False): return data -def split_invoices_based_on_payment_terms(outstanding_invoices, company): - invoice_ref_based_on_payment_terms = {} +def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list: + """Split a list of invoices based on their payment terms.""" + exc_rates = get_currency_data(outstanding_invoices, company) + outstanding_invoices_after_split = [] + for entry in outstanding_invoices: + if entry.voucher_type in ["Sales Invoice", "Purchase Invoice"]: + if payment_term_template := frappe.db.get_value( + entry.voucher_type, entry.voucher_no, "payment_terms_template" + ): + split_rows = get_split_invoice_rows(entry, payment_term_template, exc_rates) + if not split_rows: + continue + + frappe.msgprint( + _("Splitting {0} {1} into {2} rows as per Payment Terms").format( + _(entry.voucher_type), frappe.bold(entry.voucher_no), len(split_rows) + ), + alert=True, + ) + outstanding_invoices_after_split += split_rows + continue + + # If not an invoice or no payment terms template, add as it is + outstanding_invoices_after_split.append(entry) + + return outstanding_invoices_after_split + + +def get_currency_data(outstanding_invoices: list, company: str = None) -> dict: + """Get currency and conversion data for a list of invoices.""" + exc_rates = frappe._dict() company_currency = ( frappe.db.get_value("Company", company, "default_currency") if company else None ) - exc_rates = frappe._dict() + for doctype in ["Sales Invoice", "Purchase Invoice"]: invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype] for x in frappe.db.get_all( @@ -1710,72 +1739,54 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company): company_currency=company_currency, ) - for idx, d in enumerate(outstanding_invoices): - if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]: - payment_term_template = frappe.db.get_value( - d.voucher_type, d.voucher_no, "payment_terms_template" + return exc_rates + + +def get_split_invoice_rows(invoice: dict, payment_term_template: str, exc_rates: dict) -> list: + """Split invoice based on its payment schedule table.""" + split_rows = [] + allocate_payment_based_on_payment_terms = frappe.db.get_value( + "Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms" + ) + + if not allocate_payment_based_on_payment_terms: + return [invoice] + + payment_schedule = frappe.get_all( + "Payment Schedule", filters={"parent": invoice.voucher_no}, fields=["*"], order_by="due_date" + ) + for payment_term in payment_schedule: + if not payment_term.outstanding > 0.1: + continue + + doc_details = exc_rates.get(payment_term.parent, None) + is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and ( + doc_details.party_account_currency != doc_details.company_currency + ) + payment_term_outstanding = flt(payment_term.outstanding) + if not is_multi_currency_acc: + payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding) + + split_rows.append( + frappe._dict( + { + "due_date": invoice.due_date, + "currency": invoice.currency, + "voucher_no": invoice.voucher_no, + "voucher_type": invoice.voucher_type, + "posting_date": invoice.posting_date, + "invoice_amount": flt(invoice.invoice_amount), + "outstanding_amount": payment_term_outstanding + if payment_term_outstanding + else invoice.outstanding_amount, + "payment_term_outstanding": payment_term_outstanding, + "payment_amount": payment_term.payment_amount, + "payment_term": payment_term.payment_term, + } ) - if payment_term_template: - allocate_payment_based_on_payment_terms = frappe.get_cached_value( - "Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms" - ) - if allocate_payment_based_on_payment_terms: - payment_schedule = frappe.get_all( - "Payment Schedule", filters={"parent": d.voucher_no}, fields=["*"] - ) + ) - for payment_term in payment_schedule: - if payment_term.outstanding > 0.1: - doc_details = exc_rates.get(payment_term.parent, None) - is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and ( - doc_details.party_account_currency != doc_details.company_currency - ) - payment_term_outstanding = flt(payment_term.outstanding) - if not is_multi_currency_acc: - payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding) - - invoice_ref_based_on_payment_terms.setdefault(idx, []) - invoice_ref_based_on_payment_terms[idx].append( - frappe._dict( - { - "due_date": d.due_date, - "currency": d.currency, - "voucher_no": d.voucher_no, - "voucher_type": d.voucher_type, - "posting_date": d.posting_date, - "invoice_amount": flt(d.invoice_amount), - "outstanding_amount": payment_term_outstanding - if payment_term_outstanding - else d.outstanding_amount, - "payment_term_outstanding": payment_term_outstanding, - "payment_amount": payment_term.payment_amount, - "payment_term": payment_term.payment_term, - "account": d.account, - } - ) - ) - - outstanding_invoices_after_split = [] - if invoice_ref_based_on_payment_terms: - for idx, ref in invoice_ref_based_on_payment_terms.items(): - voucher_no = ref[0]["voucher_no"] - voucher_type = ref[0]["voucher_type"] - - frappe.msgprint( - _("Spliting {} {} into {} row(s) as per Payment Terms").format( - voucher_type, voucher_no, len(ref) - ), - alert=True, - ) - - outstanding_invoices_after_split += invoice_ref_based_on_payment_terms[idx] - - existing_row = list(filter(lambda x: x.get("voucher_no") == voucher_no, outstanding_invoices)) - index = outstanding_invoices.index(existing_row[0]) - outstanding_invoices.pop(index) - - outstanding_invoices_after_split += outstanding_invoices - return outstanding_invoices_after_split + return split_rows def get_orders_to_be_billed( diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index d5438efa6b..73a5bd7d7f 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -6,11 +6,12 @@ import unittest import frappe from frappe import qb from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import flt, nowdate +from frappe.utils import add_days, flt, nowdate from erpnext.accounts.doctype.account.test_account import create_account from erpnext.accounts.doctype.payment_entry.payment_entry import ( InvalidPaymentEntry, + get_outstanding_reference_documents, get_payment_entry, get_reference_details, ) @@ -1471,6 +1472,45 @@ class TestPaymentEntry(FrappeTestCase): for field in ["account", "debit", "credit"]: self.assertEqual(self.expected_gle[row][field], gl_entries[row][field]) + def test_outstanding_invoices_api(self): + """ + Test if `get_outstanding_reference_documents` fetches invoices in the right order. + """ + customer = create_customer("Max Mustermann", "INR") + create_payment_terms_template() + + # SI has an earlier due date and SI2 has a later due date + si = create_sales_invoice( + qty=1, rate=100, customer=customer, posting_date=add_days(nowdate(), -4) + ) + si2 = create_sales_invoice(do_not_save=1, qty=1, rate=100, customer=customer) + si2.payment_terms_template = "Test Receivable Template" + si2.submit() + + args = { + "posting_date": nowdate(), + "company": "_Test Company", + "party_type": "Customer", + "payment_type": "Pay", + "party": customer, + "party_account": "Debtors - _TC", + } + args.update( + { + "get_outstanding_invoices": True, + "from_posting_date": add_days(nowdate(), -4), + "to_posting_date": add_days(nowdate(), 2), + } + ) + references = get_outstanding_reference_documents(args) + + self.assertEqual(len(references), 3) + self.assertEqual(references[0].voucher_no, si.name) + self.assertEqual(references[1].voucher_no, si2.name) + self.assertEqual(references[2].voucher_no, si2.name) + self.assertEqual(references[1].payment_term, "Basic Amount Receivable") + self.assertEqual(references[2].payment_term, "Tax Receivable") + def create_payment_entry(**args): payment_entry = frappe.new_doc("Payment Entry") @@ -1531,6 +1571,9 @@ def create_payment_terms_template(): def create_payment_terms_template_with_discount( name=None, discount_type=None, discount=None, template_name=None ): + """ + Create a Payment Terms Template with % or amount discount. + """ create_payment_term(name or "30 Credit Days with 10% Discount") template_name = template_name or "Test Discount Template" diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.py b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.py index a4141afb37..b57ebecbac 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.py +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.py @@ -34,4 +34,6 @@ class PaymentReconciliationAllocation(Document): unreconciled_amount: DF.Currency # end: auto-generated types - pass + @staticmethod + def get_list(args): + pass diff --git a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.py b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.py index 1e9f6cec82..fa18ccd135 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.py +++ b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.py @@ -26,4 +26,6 @@ class PaymentReconciliationInvoice(Document): parenttype: DF.Data # end: auto-generated types - pass + @staticmethod + def get_list(args): + pass diff --git a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.py b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.py index aa956fe59b..4ab80ecaaf 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.py +++ b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.py @@ -30,4 +30,6 @@ class PaymentReconciliationPayment(Document): remark: DF.SmallText | None # end: auto-generated types - pass + @staticmethod + def get_list(args): + pass diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 57feaa03eb..18aa6820a3 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -581,6 +581,8 @@ def apply_pricing_rule_on_transaction(doc): if d.price_or_product_discount == "Price": if d.apply_discount_on: doc.set("apply_discount_on", d.apply_discount_on) + # Variable to track whether the condition has been met + condition_met = False for field in ["additional_discount_percentage", "discount_amount"]: pr_field = "discount_percentage" if field == "additional_discount_percentage" else field @@ -603,6 +605,11 @@ def apply_pricing_rule_on_transaction(doc): if coupon_code_pricing_rule == d.name: # if selected coupon code is linked with pricing rule doc.set(field, d.get(pr_field)) + + # Set the condition_met variable to True and break out of the loop + condition_met = True + break + else: # reset discount if not linked doc.set(field, 0) @@ -611,6 +618,10 @@ def apply_pricing_rule_on_transaction(doc): doc.set(field, 0) doc.calculate_taxes_and_totals() + + # Break out of the main loop if the condition is met + if condition_met: + break elif d.price_or_product_discount == "Product": item_details = frappe._dict({"parenttype": doc.doctype, "free_item_data": []}) get_product_discount_rule(d, item_details, doc=doc) diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py index 8c23c67c20..7aa631bc48 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -126,7 +126,7 @@ class RepostAccountingLedger(Document): return rendered_page def on_submit(self): - if len(self.vouchers) > 1: + if len(self.vouchers) > 5: job_name = "repost_accounting_ledger_" + self.name frappe.enqueue( method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost", @@ -170,8 +170,6 @@ def start_repost(account_repost_doc=str) -> None: doc.make_gl_entries(1) doc.make_gl_entries() - frappe.db.commit() - def get_allowed_types_from_settings(): return [ diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py index dda0ec778f..d6f7096132 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py @@ -20,18 +20,11 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase): self.create_company() self.create_customer() self.create_item() - self.update_repost_settings() + update_repost_settings() - def teadDown(self): + def tearDown(self): frappe.db.rollback() - def update_repost_settings(self): - allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"] - repost_settings = frappe.get_doc("Repost Accounting Ledger Settings") - for x in allowed_types: - repost_settings.append("allowed_types", {"document_type": x, "allowed": True}) - repost_settings.save() - def test_01_basic_functions(self): si = create_sales_invoice( item=self.item, @@ -90,9 +83,6 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase): # Submit repost document ral.save().submit() - # background jobs don't run on test cases. Manually triggering repost function. - start_repost(ral.name) - res = ( qb.from_(gl) .select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit")) @@ -177,26 +167,6 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase): pe = get_payment_entry(si.doctype, si.name) pe.save().submit() - # without deletion flag set - ral = frappe.new_doc("Repost Accounting Ledger") - ral.company = self.company - ral.delete_cancelled_entries = False - ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) - ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name}) - ral.save() - - # assert preview data is generated - preview = ral.generate_preview() - self.assertIsNotNone(preview) - - ral.save().submit() - - # background jobs don't run on test cases. Manually triggering repost function. - start_repost(ral.name) - - self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1})) - self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1})) - # with deletion flag set ral = frappe.new_doc("Repost Accounting Ledger") ral.company = self.company @@ -205,6 +175,38 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase): ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name}) ral.save().submit() - start_repost(ral.name) self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1})) self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1})) + + def test_05_without_deletion_flag(self): + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + ) + + pe = get_payment_entry(si.doctype, si.name) + pe.save().submit() + + # without deletion flag set + ral = frappe.new_doc("Repost Accounting Ledger") + ral.company = self.company + ral.delete_cancelled_entries = False + ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) + ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name}) + ral.save().submit() + + self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1})) + self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1})) + + +def update_repost_settings(): + allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"] + repost_settings = frappe.get_doc("Repost Accounting Ledger Settings") + for x in allowed_types: + repost_settings.append("allowed_types", {"document_type": x, "allowed": True}) + repost_settings.save() diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index e3d8f453f5..450c8effea 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2799,6 +2799,12 @@ class TestSalesInvoice(FrappeTestCase): @change_settings("Selling Settings", {"enable_discount_accounting": 1}) def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self): + from erpnext.accounts.doctype.repost_accounting_ledger.test_repost_accounting_ledger import ( + update_repost_settings, + ) + + update_repost_settings() + additional_discount_account = create_account( account_name="Discount Account", parent_account="Indirect Expenses - _TC", diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 096bb10706..7355c4b8a1 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -8,7 +8,17 @@ import re import frappe from frappe import _ -from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate +from frappe.utils import ( + add_days, + add_months, + cint, + cstr, + flt, + formatdate, + get_first_day, + getdate, + today, +) from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, @@ -43,6 +53,8 @@ def get_period_list( year_start_date = getdate(period_start_date) year_end_date = getdate(period_end_date) + year_end_date = getdate(today()) if year_end_date > getdate(today()) else year_end_date + months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity] period_list = [] diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index 0021140282..67234ccd84 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -340,6 +340,10 @@ class AssetDepreciationSchedule(Document): n == 0 and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata) and not self.opening_accumulated_depreciation + and get_updated_rate_of_depreciation_for_wdv_and_dd( + asset_doc, value_after_depreciation, row, False + ) + == row.rate_of_depreciation ): from_date = add_days( asset_doc.available_for_use_date, -1 @@ -605,7 +609,9 @@ def get_depreciation_amount( @erpnext.allow_regional -def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb_row): +def get_updated_rate_of_depreciation_for_wdv_and_dd( + asset, depreciable_value, fb_row, show_msg=True +): return fb_row.rate_of_depreciation diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 8ab52e4e51..ae51fbd6ea 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -292,6 +292,7 @@ class AccountsController(TransactionBase): def on_trash(self): self._remove_references_in_repost_doctypes() self._remove_references_in_unreconcile() + self.remove_serial_and_batch_bundle() # delete sl and gl entries on deletion of transaction if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"): @@ -307,6 +308,15 @@ class AccountsController(TransactionBase): (self.doctype, self.name), ) + def remove_serial_and_batch_bundle(self): + bundles = frappe.get_all( + "Serial and Batch Bundle", + filters={"voucher_type": self.doctype, "voucher_no": self.name, "docstatus": ("!=", 1)}, + ) + + for bundle in bundles: + frappe.delete_doc("Serial and Batch Bundle", bundle.name) + def validate_deferred_income_expense_account(self): field_map = { "Sales Invoice": "deferred_revenue_account", diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 17ad155031..f6b6802d58 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -637,6 +637,7 @@ additional_timeline_content = { extend_bootinfo = [ "erpnext.support.doctype.service_level_agreement.service_level_agreement.add_sla_doctypes", + "erpnext.startup.boot.bootinfo", ] diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 0860d9c667..3ed7fc75cf 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -36,14 +36,14 @@ erpnext.buying = { // no idea where me is coming from if(this.frm.get_field('shipping_address')) { - this.frm.set_query("shipping_address", function() { - if(me.frm.doc.customer) { + this.frm.set_query("shipping_address", () => { + if(this.frm.doc.customer) { return { query: 'frappe.contacts.doctype.address.address.address_query', - filters: { link_doctype: 'Customer', link_name: me.frm.doc.customer } + filters: { link_doctype: 'Customer', link_name: this.frm.doc.customer } }; } else - return erpnext.queries.company_address_query(me.frm.doc) + return erpnext.queries.company_address_query(this.frm.doc) }); } } diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index c4a3c6fb1f..c1c4b99dca 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -471,7 +471,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe item.pricing_rules = '' return this.frm.call({ method: "erpnext.stock.get_item_details.get_item_details", - child: item, args: { doc: me.frm.doc, args: { @@ -520,6 +519,19 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe callback: function(r) { if(!r.exc) { frappe.run_serially([ + () => { + var child = locals[cdt][cdn]; + var std_field_list = ["doctype"] + .concat(frappe.model.std_fields_list) + .concat(frappe.model.child_table_field_list); + + for (var key in r.message) { + if (std_field_list.indexOf(key) === -1) { + if (key === "qty" && child[key]) continue; + child[key] = r.message[key]; + } + } + }, () => { var d = locals[cdt][cdn]; me.add_taxes_from_item_tax_template(d.item_tax_rate); diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index 1b10d8ad3a..17341d19d9 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -2,10 +2,16 @@ frappe.provide("erpnext.financial_statements"); erpnext.financial_statements = { "filters": get_filters(), - "formatter": function(value, row, column, data, default_formatter) { + "formatter": function(value, row, column, data, default_formatter, filter) { if (data && column.fieldname=="account") { value = data.account_name || value; + if (filter && filter?.text && filter?.type == "contains") { + if (!value.toLowerCase().includes(filter.text)) { + return value; + } + } + if (data.account) { column.link_onclick = "erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")"; diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 25fc754b9a..e9d06dfbec 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -8,7 +8,7 @@ $.extend(erpnext, { if(!company && cur_frm) company = cur_frm.doc.company; if(company) - return frappe.get_doc(":Company", company).default_currency || frappe.boot.sysdefaults.currency; + return frappe.get_doc(":Company", company)?.default_currency || frappe.boot.sysdefaults.currency; else return frappe.boot.sysdefaults.currency; }, diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 0de6774393..3b9a551b43 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -31,7 +31,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { secondary_action: () => this.edit_full_form(), }); - this.dialog.set_value("qty", this.item.qty).then(() => { + let qty = this.item.stock_qty || this.item.transfer_qty || this.item.qty; + this.dialog.set_value("qty", qty).then(() => { if (this.item.serial_no) { this.dialog.set_value("scan_serial_no", this.item.serial_no); frappe.model.set_value(this.item.doctype, this.item.name, 'serial_no', ''); diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 49ba78c63a..32d92f6459 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -2,8 +2,8 @@ # License: GNU General Public License v3. See license.txt -import os import json +import os import frappe from frappe import _ @@ -114,10 +114,11 @@ def update_regional_tax_settings(country, company): frappe.scrub(country) ) frappe.get_attr(module_name)(country, company) - except Exception as e: + except (ImportError, AttributeError): + pass + except Exception: # Log error and ignore if failed to setup regional tax settings frappe.log_error("Unable to setup regional tax settings") - pass def make_taxes_and_charges_template(company_name, doctype, template): diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py index bdbf8b4fac..4b4d14f89e 100644 --- a/erpnext/startup/boot.py +++ b/erpnext/startup/boot.py @@ -75,3 +75,11 @@ def update_page_info(bootinfo): "Sales Person Tree": {"title": "Sales Person Tree", "route": "Tree/Sales Person"}, } ) + + +def bootinfo(bootinfo): + if bootinfo.get("user") and bootinfo["user"].get("name"): + bootinfo["user"]["employee"] = "" + employee = frappe.db.get_value("Employee", {"user_id": bootinfo["user"]["name"]}, "name") + if employee: + bootinfo["user"]["employee"] = employee diff --git a/erpnext/startup/leaderboard.py b/erpnext/startup/leaderboard.py index da7edbf814..5a60d2ff96 100644 --- a/erpnext/startup/leaderboard.py +++ b/erpnext/startup/leaderboard.py @@ -1,5 +1,5 @@ import frappe -from frappe.utils import cint +from frappe.utils.deprecations import deprecated def get_leaderboards(): @@ -54,12 +54,13 @@ def get_leaderboards(): @frappe.whitelist() def get_all_customers(date_range, company, field, limit=None): + filters = [["docstatus", "=", "1"], ["company", "=", company]] + from_date, to_date = parse_date_range(date_range) if field == "outstanding_amount": - filters = [["docstatus", "=", "1"], ["company", "=", company]] - if date_range: - date_range = frappe.parse_json(date_range) - filters.append(["posting_date", ">=", "between", [date_range[0], date_range[1]]]) - return frappe.db.get_all( + if from_date and to_date: + filters.append(["posting_date", "between", [from_date, to_date]]) + + return frappe.get_list( "Sales Invoice", fields=["customer as name", "sum(outstanding_amount) as value"], filters=filters, @@ -69,26 +70,20 @@ def get_all_customers(date_range, company, field, limit=None): ) else: if field == "total_sales_amount": - select_field = "sum(so_item.base_net_amount)" + select_field = "base_net_total" elif field == "total_qty_sold": - select_field = "sum(so_item.stock_qty)" + select_field = "total_qty" - date_condition = get_date_condition(date_range, "so.transaction_date") + if from_date and to_date: + filters.append(["transaction_date", "between", [from_date, to_date]]) - return frappe.db.sql( - """ - select so.customer as name, {0} as value - FROM `tabSales Order` as so JOIN `tabSales Order Item` as so_item - ON so.name = so_item.parent - where so.docstatus = 1 {1} and so.company = %s - group by so.customer - order by value DESC - limit %s - """.format( - select_field, date_condition - ), - (company, cint(limit)), - as_dict=1, + return frappe.get_list( + "Sales Order", + fields=["customer as name", f"sum({select_field}) as value"], + filters=filters, + group_by="customer", + order_by="value desc", + limit=limit, ) @@ -96,55 +91,58 @@ def get_all_customers(date_range, company, field, limit=None): def get_all_items(date_range, company, field, limit=None): if field in ("available_stock_qty", "available_stock_value"): select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(stock_value)" - return frappe.db.get_all( + results = frappe.db.get_all( "Bin", fields=["item_code as name", "{0} as value".format(select_field)], group_by="item_code", order_by="value desc", limit=limit, ) + readable_active_items = set(frappe.get_list("Item", filters={"disabled": 0}, pluck="name")) + return [item for item in results if item["name"] in readable_active_items] else: if field == "total_sales_amount": - select_field = "sum(order_item.base_net_amount)" + select_field = "base_net_amount" select_doctype = "Sales Order" elif field == "total_purchase_amount": - select_field = "sum(order_item.base_net_amount)" + select_field = "base_net_amount" select_doctype = "Purchase Order" elif field == "total_qty_sold": - select_field = "sum(order_item.stock_qty)" + select_field = "stock_qty" select_doctype = "Sales Order" elif field == "total_qty_purchased": - select_field = "sum(order_item.stock_qty)" + select_field = "stock_qty" select_doctype = "Purchase Order" - date_condition = get_date_condition(date_range, "sales_order.transaction_date") + filters = [["docstatus", "=", "1"], ["company", "=", company]] + from_date, to_date = parse_date_range(date_range) + if from_date and to_date: + filters.append(["transaction_date", "between", [from_date, to_date]]) - return frappe.db.sql( - """ - select order_item.item_code as name, {0} as value - from `tab{1}` sales_order join `tab{1} Item` as order_item - on sales_order.name = order_item.parent - where sales_order.docstatus = 1 - and sales_order.company = %s {2} - group by order_item.item_code - order by value desc - limit %s - """.format( - select_field, select_doctype, date_condition - ), - (company, cint(limit)), - as_dict=1, - ) # nosec + child_doctype = f"{select_doctype} Item" + return frappe.get_list( + select_doctype, + fields=[ + f"`tab{child_doctype}`.item_code as name", + f"sum(`tab{child_doctype}`.{select_field}) as value", + ], + filters=filters, + order_by="value desc", + group_by=f"`tab{child_doctype}`.item_code", + limit=limit, + ) @frappe.whitelist() def get_all_suppliers(date_range, company, field, limit=None): + filters = [["docstatus", "=", "1"], ["company", "=", company]] + from_date, to_date = parse_date_range(date_range) + if field == "outstanding_amount": - filters = [["docstatus", "=", "1"], ["company", "=", company]] - if date_range: - date_range = frappe.parse_json(date_range) - filters.append(["posting_date", "between", [date_range[0], date_range[1]]]) - return frappe.db.get_all( + if from_date and to_date: + filters.append(["posting_date", "between", [from_date, to_date]]) + + return frappe.get_list( "Purchase Invoice", fields=["supplier as name", "sum(outstanding_amount) as value"], filters=filters, @@ -154,48 +152,40 @@ def get_all_suppliers(date_range, company, field, limit=None): ) else: if field == "total_purchase_amount": - select_field = "sum(purchase_order_item.base_net_amount)" + select_field = "base_net_total" elif field == "total_qty_purchased": - select_field = "sum(purchase_order_item.stock_qty)" + select_field = "total_qty" - date_condition = get_date_condition(date_range, "purchase_order.modified") + if from_date and to_date: + filters.append(["transaction_date", "between", [from_date, to_date]]) - return frappe.db.sql( - """ - select purchase_order.supplier as name, {0} as value - FROM `tabPurchase Order` as purchase_order LEFT JOIN `tabPurchase Order Item` - as purchase_order_item ON purchase_order.name = purchase_order_item.parent - where - purchase_order.docstatus = 1 - {1} - and purchase_order.company = %s - group by purchase_order.supplier - order by value DESC - limit %s""".format( - select_field, date_condition - ), - (company, cint(limit)), - as_dict=1, - ) # nosec + return frappe.get_list( + "Purchase Order", + fields=["supplier as name", f"sum({select_field}) as value"], + filters=filters, + group_by="supplier", + order_by="value desc", + limit=limit, + ) @frappe.whitelist() def get_all_sales_partner(date_range, company, field, limit=None): if field == "total_sales_amount": - select_field = "sum(`base_net_total`)" + select_field = "base_net_total" elif field == "total_commission": - select_field = "sum(`total_commission`)" + select_field = "total_commission" - filters = {"sales_partner": ["!=", ""], "docstatus": 1, "company": company} - if date_range: - date_range = frappe.parse_json(date_range) - filters["transaction_date"] = ["between", [date_range[0], date_range[1]]] + filters = [["docstatus", "=", "1"], ["company", "=", company], ["sales_partner", "is", "set"]] + from_date, to_date = parse_date_range(date_range) + if from_date and to_date: + filters.append(["transaction_date", "between", [from_date, to_date]]) return frappe.get_list( "Sales Order", fields=[ - "`sales_partner` as name", - "{} as value".format(select_field), + "sales_partner as name", + f"sum({select_field}) as value", ], filters=filters, group_by="sales_partner", @@ -206,27 +196,29 @@ def get_all_sales_partner(date_range, company, field, limit=None): @frappe.whitelist() def get_all_sales_person(date_range, company, field=None, limit=0): - date_condition = get_date_condition(date_range, "sales_order.transaction_date") + filters = [ + ["docstatus", "=", "1"], + ["company", "=", company], + ["Sales Team", "sales_person", "is", "set"], + ] + from_date, to_date = parse_date_range(date_range) + if from_date and to_date: + filters.append(["transaction_date", "between", [from_date, to_date]]) - return frappe.db.sql( - """ - select sales_team.sales_person as name, sum(sales_order.base_net_total) as value - from `tabSales Order` as sales_order join `tabSales Team` as sales_team - on sales_order.name = sales_team.parent and sales_team.parenttype = 'Sales Order' - where sales_order.docstatus = 1 - and sales_order.company = %s - {date_condition} - group by sales_team.sales_person - order by value DESC - limit %s - """.format( - date_condition=date_condition - ), - (company, cint(limit)), - as_dict=1, + return frappe.get_list( + "Sales Order", + fields=[ + "`tabSales Team`.sales_person as name", + "sum(`tabSales Team`.allocated_amount) as value", + ], + filters=filters, + group_by="`tabSales Team`.sales_person", + order_by="value desc", + limit=limit, ) +@deprecated def get_date_condition(date_range, field): date_condition = "" if date_range: @@ -236,3 +228,11 @@ def get_date_condition(date_range, field): field, frappe.db.escape(from_date), frappe.db.escape(to_date) ) return date_condition + + +def parse_date_range(date_range): + if date_range: + date_range = frappe.parse_json(date_range) + return date_range[0], date_range[1] + + return None, None diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js index cda444510a..9f01ee9ae6 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js @@ -121,7 +121,7 @@ frappe.ui.form.on('Serial and Batch Bundle', { frappe.throw(__("Please attach CSV file")); } - if (frm.doc.has_serial_no && !prompt_data.using_csv_file && !prompt_data.serial_nos) { + if (frm.doc.has_serial_no && !prompt_data.csv_file && !prompt_data.serial_nos) { frappe.throw(__("Please enter serial nos")); } }, diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json index d46b07a3e1..7a58462357 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json @@ -1,7 +1,7 @@ { "actions": [], "autoname": "naming_series:", - "creation": "2022-09-29 14:56:38.338267", + "creation": "2023-08-11 17:22:12.907518", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -250,7 +250,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-07-28 12:56:03.072224", + "modified": "2023-12-07 17:56:55.528563", "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Bundle", @@ -270,6 +270,118 @@ "share": 1, "submit": 1, "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Delivery User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Delivery Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing Manager", + "share": 1, + "submit": 1, + "write": 1 } ], "sort_field": "modified", diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index f9392934aa..28b7dc337b 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -506,6 +506,22 @@ class SerialandBatchBundle(Document): serial_batches = {} for row in self.entries: + if self.has_serial_no and not row.serial_no: + frappe.throw( + _("At row {0}: Serial No is mandatory for Item {1}").format( + bold(row.idx), bold(self.item_code) + ), + title=_("Serial No is mandatory"), + ) + + if self.has_batch_no and not row.batch_no: + frappe.throw( + _("At row {0}: Batch No is mandatory for Item {1}").format( + bold(row.idx), bold(self.item_code) + ), + title=_("Batch No is mandatory"), + ) + if row.serial_no: serial_nos.append(row.serial_no) @@ -688,6 +704,7 @@ class SerialandBatchBundle(Document): "item_code": self.item_code, "warehouse": self.warehouse, "batch_no": batches, + "consider_negative_batches": True, } ) ) @@ -698,6 +715,9 @@ class SerialandBatchBundle(Document): available_batches = get_available_batches_qty(available_batches) for batch_no in batches: if batch_no not in available_batches or available_batches[batch_no] < 0: + if flt(available_batches.get(batch_no)) < 0: + self.validate_negative_batch(batch_no, available_batches[batch_no]) + self.throw_error_message( f"Batch {bold(batch_no)} is not available in the selected warehouse {self.warehouse}" ) @@ -789,6 +809,9 @@ def parse_csv_file_to_get_serial_batch(reader): if index == 0: has_serial_no = row[0] == "Serial No" has_batch_no = row[0] == "Batch No" + if not has_batch_no: + has_batch_no = row[1] == "Batch No" + continue if not row[0]: @@ -805,6 +828,13 @@ def parse_csv_file_to_get_serial_batch(reader): } ) + batch_nos.append( + { + "batch_no": row[1], + "qty": row[2], + } + ) + serial_nos.append(_dict) elif has_batch_no: batch_nos.append( @@ -840,6 +870,9 @@ def make_serial_nos(item_code, serial_nos): serial_nos_details = [] user = frappe.session.user for serial_no in serial_nos: + if frappe.db.exists("Serial No", serial_no): + continue + serial_nos_details.append( ( serial_no, @@ -870,7 +903,7 @@ def make_serial_nos(item_code, serial_nos): frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details)) - frappe.msgprint(_("Serial Nos are created successfully")) + frappe.msgprint(_("Serial Nos are created successfully"), alert=True) def make_batch_nos(item_code, batch_nos): @@ -881,6 +914,9 @@ def make_batch_nos(item_code, batch_nos): batch_nos_details = [] user = frappe.session.user for batch_no in batch_nos: + if frappe.db.exists("Batch", batch_no): + continue + batch_nos_details.append( (batch_no, batch_no, now(), now(), user, user, item.item_code, item.item_name, item.description) ) @@ -899,7 +935,7 @@ def make_batch_nos(item_code, batch_nos): frappe.db.bulk_insert("Batch", fields=fields, values=set(batch_nos_details)) - frappe.msgprint(_("Batch Nos are created successfully")) + frappe.msgprint(_("Batch Nos are created successfully"), alert=True) def parse_serial_nos(data): @@ -1454,7 +1490,8 @@ def get_auto_batch_nos(kwargs): available_batches, stock_ledgers_batches, pos_invoice_batches, sre_reserved_batches ) - available_batches = list(filter(lambda x: x.qty > 0, available_batches)) + if not kwargs.consider_negative_batches: + available_batches = list(filter(lambda x: x.qty > 0, available_batches)) if not qty: return available_batches diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index 0e01b20e7c..d74d657f38 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -368,6 +368,58 @@ class TestSerialandBatchBundle(FrappeTestCase): # Batch does not belong to serial no self.assertRaises(frappe.exceptions.ValidationError, doc.save) + def test_auto_delete_draft_serial_and_batch_bundle(self): + serial_and_batch_code = "New Serial No Auto Delete 1" + make_item( + serial_and_batch_code, + { + "has_serial_no": 1, + "serial_no_series": "TEST-SER-VALL-.#####", + "is_stock_item": 1, + }, + ) + + ste = make_stock_entry( + item_code=serial_and_batch_code, + target="_Test Warehouse - _TC", + qty=1, + rate=500, + do_not_submit=True, + ) + + serial_no = "SN-TEST-AUTO-DEL" + if not frappe.db.exists("Serial No", serial_no): + frappe.get_doc( + { + "doctype": "Serial No", + "serial_no": serial_no, + "item_code": serial_and_batch_code, + "company": "_Test Company", + } + ).insert(ignore_permissions=True) + + bundle_doc = make_serial_batch_bundle( + { + "item_code": serial_and_batch_code, + "warehouse": "_Test Warehouse - _TC", + "voucher_type": "Stock Entry", + "posting_date": ste.posting_date, + "posting_time": ste.posting_time, + "qty": 1, + "serial_nos": [serial_no], + "type_of_transaction": "Inward", + "do_not_submit": True, + } + ) + + bundle_doc.reload() + ste.items[0].serial_and_batch_bundle = bundle_doc.name + ste.save() + ste.reload() + + ste.delete() + self.assertFalse(frappe.db.exists("Serial and Batch Bundle", bundle_doc.name)) + def get_batch_from_bundle(bundle): from erpnext.stock.serial_batch_bundle import get_batch_nos diff --git a/erpnext/stock/doctype/serial_and_batch_entry/serial_and_batch_entry.json b/erpnext/stock/doctype/serial_and_batch_entry/serial_and_batch_entry.json index 09565cbc8a..5de2c2ee65 100644 --- a/erpnext/stock/doctype/serial_and_batch_entry/serial_and_batch_entry.json +++ b/erpnext/stock/doctype/serial_and_batch_entry/serial_and_batch_entry.json @@ -27,7 +27,6 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Serial No", - "mandatory_depends_on": "eval:parent.has_serial_no == 1", "options": "Serial No", "search_index": 1 }, @@ -38,7 +37,6 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Batch No", - "mandatory_depends_on": "eval:parent.has_batch_no == 1", "options": "Batch", "search_index": 1 }, @@ -122,7 +120,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-07-03 15:29:50.199075", + "modified": "2023-12-10 19:47:48.227772", "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Entry", diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 6eea476fb6..420afe8c4f 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1737,6 +1737,45 @@ class TestStockEntry(FrappeTestCase): self.assertFalse(doc.is_enqueue_action()) frappe.flags.in_test = True + def test_negative_batch(self): + item_code = "Test Negative Batch Item - 001" + make_item( + item_code, + {"has_batch_no": 1, "create_new_batch": 1, "batch_naming_series": "Test-BCH-NNS.#####"}, + ) + + se1 = make_stock_entry( + item_code=item_code, + purpose="Material Receipt", + qty=100, + target="_Test Warehouse - _TC", + ) + + se1.reload() + + batch_no = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle) + + se2 = make_stock_entry( + item_code=item_code, + purpose="Material Issue", + batch_no=batch_no, + qty=10, + source="_Test Warehouse - _TC", + ) + + se2.reload() + + se3 = make_stock_entry( + item_code=item_code, + purpose="Material Receipt", + qty=100, + target="_Test Warehouse - _TC", + ) + + se3.reload() + + self.assertRaises(frappe.ValidationError, se1.cancel) + def make_serialized_item(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index b3998b7c7e..8e9dcb0fc5 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -209,7 +209,7 @@ frappe.ui.form.on("Stock Reconciliation", { set_amount_quantity: function(doc, cdt, cdn) { var d = frappe.model.get_doc(cdt, cdn); - if (d.qty & d.valuation_rate) { + if (d.qty && d.valuation_rate) { frappe.model.set_value(cdt, cdn, "amount", flt(d.qty) * flt(d.valuation_rate)); frappe.model.set_value(cdt, cdn, "quantity_difference", flt(d.qty) - flt(d.current_qty)); frappe.model.set_value(cdt, cdn, "amount_difference", flt(d.amount) - flt(d.current_amount)); diff --git a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py index ae12fbb3e4..810dc4666f 100644 --- a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py +++ b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py @@ -1,9 +1,12 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +import copy + import frappe from frappe import _ +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_serial_nos_from_sle from erpnext.stock.stock_ledger import get_stock_ledger_entries @@ -15,8 +18,8 @@ def execute(filters=None): def get_columns(filters): columns = [ - {"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date"}, - {"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time"}, + {"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date", "width": 120}, + {"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time", "width": 90}, { "label": _("Voucher Type"), "fieldtype": "Link", @@ -29,7 +32,7 @@ def get_columns(filters): "fieldtype": "Dynamic Link", "fieldname": "voucher_no", "options": "voucher_type", - "width": 180, + "width": 230, }, { "label": _("Company"), @@ -49,7 +52,7 @@ def get_columns(filters): "label": _("Status"), "fieldtype": "Data", "fieldname": "status", - "width": 120, + "width": 90, }, { "label": _("Serial No"), @@ -62,7 +65,7 @@ def get_columns(filters): "label": _("Valuation Rate"), "fieldtype": "Float", "fieldname": "valuation_rate", - "width": 150, + "width": 130, }, { "label": _("Qty"), @@ -102,15 +105,29 @@ def get_data(filters): } ) - serial_nos = [{"serial_no": row.serial_no, "valuation_rate": row.valuation_rate}] + serial_nos = [] + if row.serial_no: + parsed_serial_nos = get_serial_nos_from_sle(row.serial_no) + for serial_no in parsed_serial_nos: + if filters.get("serial_no") and filters.get("serial_no") != serial_no: + continue + + serial_nos.append( + { + "serial_no": serial_no, + "valuation_rate": abs(row.stock_value_difference / row.actual_qty), + } + ) + if row.serial_and_batch_bundle: - serial_nos = bundle_wise_serial_nos.get(row.serial_and_batch_bundle, []) + serial_nos.extend(bundle_wise_serial_nos.get(row.serial_and_batch_bundle, [])) for index, bundle_data in enumerate(serial_nos): if index == 0: - args.serial_no = bundle_data.get("serial_no") - args.valuation_rate = bundle_data.get("valuation_rate") - data.append(args) + new_args = copy.deepcopy(args) + new_args.serial_no = bundle_data.get("serial_no") + new_args.valuation_rate = bundle_data.get("valuation_rate") + data.append(new_args) else: data.append( { diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 6c187f8368..0fe8c13efb 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -7,6 +7,7 @@ from frappe.model.mapper import get_mapped_doc from frappe.utils import flt from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created +from erpnext.buying.doctype.purchase_order.purchase_order import update_status as update_po_status from erpnext.controllers.subcontracting_controller import SubcontractingController from erpnext.stock.stock_balance import update_bin_qty from erpnext.stock.utils import get_bin @@ -308,6 +309,9 @@ class SubcontractingOrder(SubcontractingController): "Subcontracting Order", self.name, "status", status, update_modified=update_modified ) + if status == "Closed": + update_po_status("Closed", self.purchase_order) + @frappe.whitelist() def make_subcontracting_receipt(source_name, target_doc=None):