diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 63758a5e7fb6..85713c6c9a6a 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -6,7 +6,7 @@ from frappe import _, msgprint from frappe.model.document import Document from frappe.query_builder.custom import ConstantColumn -from frappe.utils import flt, fmt_money, getdate +from frappe.utils import flt, fmt_money, get_link_to_form, getdate from pypika import Order import erpnext @@ -96,8 +96,11 @@ def update_clearance_date(self): if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date): frappe.throw( - _("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}").format( - d.idx, d.clearance_date, d.cheque_date + _("Row #{0}: For {1} Clearance date {2} cannot be before Cheque Date {3}").format( + d.idx, + get_link_to_form(d.payment_document, d.payment_entry), + d.clearance_date, + d.cheque_date, ) ) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index c28dcf525df1..57107c7a4533 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -35,6 +35,11 @@ frappe.ui.form.on("Payment Entry", { var account_types = ["Pay", "Internal Transfer"].includes(frm.doc.payment_type) ? ["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]]; + + if (frm.doc.party_type == "Shareholder") { + account_types.push("Equity"); + } + return { filters: { account_type: ["in", account_types], @@ -90,6 +95,9 @@ frappe.ui.form.on("Payment Entry", { var account_types = ["Receive", "Internal Transfer"].includes(frm.doc.payment_type) ? ["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]]; + if (frm.doc.party_type == "Shareholder") { + account_types.push("Equity"); + } return { filters: { account_type: ["in", account_types], @@ -412,6 +420,12 @@ frappe.ui.form.on("Payment Entry", { return { query: "erpnext.controllers.queries.employee_query", }; + } else if (frm.doc.party_type == "Shareholder") { + return { + filters: { + company: frm.doc.company, + }, + }; } }); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 454101027e7f..13baff9e7a82 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1740,7 +1740,7 @@ def get_outstanding_reference_documents(args, validate=False): d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no") # Get negative outstanding sales /purchase invoices - if args.get("party_type") != "Employee" and not args.get("voucher_no"): + if args.get("party_type") != "Employee": negative_outstanding_invoices = get_negative_outstanding_invoices( args.get("party_type"), args.get("party"), diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index a2c6a9d856a1..da63ccb6ad7a 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -15,7 +15,7 @@ ) from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate from erpnext.accounts.party import get_party_account, get_party_bank_account -from erpnext.accounts.utils import get_account_currency +from erpnext.accounts.utils import get_account_currency, get_currency_precision from erpnext.utilities import payment_app_import_guard @@ -552,7 +552,7 @@ def get_amount(ref_doc, payment_account=None): grand_total = ref_doc.outstanding_amount if grand_total > 0: - return grand_total + return flt(grand_total, get_currency_precision()) else: frappe.throw(_("Payment Entry is already created")) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 9c7911d7caee..b848fde08d89 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -486,7 +486,7 @@ def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules, row_item): continue stock_qty = row.get("qty") * (row.get("conversion_factor") or 1.0) - amount = stock_qty * (row.get("price_list_rate") or row.get("rate")) + amount = stock_qty * (flt(row.get("price_list_rate")) or flt(row.get("rate"))) pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, row) if pricing_rules and pricing_rules[0]: diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index eb7c486f2e1b..31143fb72b85 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2236,6 +2236,62 @@ def test_make_pr_and_pi_from_po(self): self.assertEqual(pi_expected_values[i][1], gle.debit) self.assertEqual(pi_expected_values[i][2], gle.credit) + def test_adjust_incoming_rate_from_pi_with_multi_currency(self): + from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import ( + make_landed_cost_voucher, + ) + + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) + + frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1) + + # Increase the cost of the item + + pr = make_purchase_receipt( + qty=10, rate=1, currency="USD", do_not_save=1, supplier="_Test Supplier USD" + ) + pr.conversion_rate = 6300 + pr.plc_conversion_rate = 1 + pr.save() + pr.submit() + + self.assertEqual(pr.conversion_rate, 6300) + self.assertEqual(pr.plc_conversion_rate, 1) + self.assertEqual(pr.base_grand_total, 6300 * 10) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 6300 * 10) + + make_landed_cost_voucher( + company=pr.company, + receipt_document_type="Purchase Receipt", + receipt_document=pr.name, + charges=3000, + distribute_charges_based_on="Qty", + ) + + pi = create_purchase_invoice_from_receipt(pr.name) + for row in pi.items: + row.rate = 1.1 + + pi.save() + pi.submit() + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 7230 * 10) + + frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0) + + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1) + def set_advance_flag(company, flag, default_account): frappe.db.set_value( diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 916757a8d6d7..d57f1de43794 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -737,10 +737,7 @@ def force_fetch_subscription_updates(self): elif self.generate_invoice_at == "Days before the current subscription period": processing_date = add_days(self.current_invoice_start, -self.number_of_days) - process_subscription = frappe.new_doc("Process Subscription") - process_subscription.posting_date = processing_date - process_subscription.subscription = self.name - process_subscription.save().submit() + self.process(posting_date=processing_date) def is_prorate() -> int: diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js index 49771d7ebe9f..0f74df6f9090 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js @@ -46,5 +46,11 @@ frappe.query_reports["Asset Depreciations and Balances"] = { options: "Asset", depends_on: "eval: doc.group_by == 'Asset'", }, + { + fieldname: "finance_book", + label: __("Finance Book"), + fieldtype: "Link", + options: "Finance Book", + }, ], }; diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index f9a008ade7fd..34cced2ca171 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -69,6 +69,9 @@ def get_asset_categories_for_grouped_by_category(filters): condition = "" if filters.get("asset_category"): condition += " and asset_category = %(asset_category)s" + if filters.get("finance_book"): + condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)" + # nosemgrep return frappe.db.sql( f""" @@ -119,6 +122,7 @@ def get_asset_categories_for_grouped_by_category(filters): "from_date": filters.from_date, "company": filters.company, "asset_category": filters.get("asset_category"), + "finance_book": filters.get("finance_book"), }, as_dict=1, ) @@ -128,6 +132,10 @@ def get_asset_details_for_grouped_by_category(filters): condition = "" if filters.get("asset"): condition += " and name = %(asset)s" + if filters.get("finance_book"): + condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = `tabAsset`.name and ads.finance_book = %(finance_book)s)" + + # nosemgrep return frappe.db.sql( f""" SELECT name, @@ -176,6 +184,7 @@ def get_asset_details_for_grouped_by_category(filters): "from_date": filters.from_date, "company": filters.company, "asset": filters.get("asset"), + "finance_book": filters.get("finance_book"), }, as_dict=1, ) diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py index 8a8e3a599723..c7dba16492e7 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py @@ -154,8 +154,8 @@ def get_payment_entries(filters): select "Payment Entry" as payment_document, name as payment_entry, reference_no, reference_date as ref_date, - if(paid_to=%(account)s, received_amount, 0) as debit, - if(paid_from=%(account)s, paid_amount, 0) as credit, + if(paid_to=%(account)s, received_amount_after_tax, 0) as debit, + if(paid_from=%(account)s, paid_amount_after_tax, 0) as credit, posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date, if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency from `tabPayment Entry` diff --git a/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/__init__.py b/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.js b/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.js new file mode 100644 index 000000000000..e83fc6f5b57b --- /dev/null +++ b/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.js @@ -0,0 +1,44 @@ +// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["Cheques and Deposits Incorrectly cleared"] = { + filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), + }, + { + fieldname: "account", + label: __("Bank Account"), + fieldtype: "Link", + options: "Account", + default: frappe.defaults.get_user_default("Company") + ? locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"] + : "", + reqd: 1, + get_query: function () { + var company = frappe.query_report.get_filter_value("company"); + return { + query: "erpnext.controllers.queries.get_account_list", + filters: [ + ["Account", "account_type", "in", "Bank, Cash"], + ["Account", "is_group", "=", 0], + ["Account", "disabled", "=", 0], + ["Account", "company", "=", company], + ], + }; + }, + }, + { + fieldname: "report_date", + label: __("Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + }, + ], +}; diff --git a/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.json b/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.json new file mode 100644 index 000000000000..50cf765ca3d4 --- /dev/null +++ b/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2024-07-30 17:20:07.570971", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letterhead": null, + "modified": "2024-07-30 17:20:07.570971", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Cheques and Deposits Incorrectly cleared", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Payment Entry", + "report_name": "Cheques and Deposits Incorrectly cleared", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts User" + }, + { + "role": "Accounts Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.py b/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.py new file mode 100644 index 000000000000..891dc2c4bb10 --- /dev/null +++ b/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.py @@ -0,0 +1,153 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _, qb +from frappe.query_builder import CustomFunction +from frappe.query_builder.custom import ConstantColumn + + +def execute(filters=None): + columns = get_columns() + data = build_data(filters) + return columns, data + + +def build_payment_entry_dict(row: dict) -> dict: + row_dict = frappe._dict() + row_dict.update( + { + "payment_document": row.get("doctype"), + "payment_entry": row.get("name"), + "posting_date": row.get("posting_date"), + "clearance_date": row.get("clearance_date"), + } + ) + if row.get("payment_type") == "Receive" and row.get("party_type") in ["Customer", "Supplier"]: + row_dict.update( + { + "debit": row.get("amount"), + "credit": 0, + } + ) + else: + row_dict.update( + { + "debit": 0, + "credit": row.get("amount"), + } + ) + return row_dict + + +def build_journal_entry_dict(row: dict) -> dict: + row_dict = frappe._dict() + row_dict.update( + { + "payment_document": row.get("doctype"), + "payment_entry": row.get("name"), + "posting_date": row.get("posting_date"), + "clearance_date": row.get("clearance_date"), + "debit": row.get("debit_in_account_currency"), + "credit": row.get("credit_in_account_currency"), + } + ) + return row_dict + + +def build_data(filters): + vouchers = get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters) + data = [] + for x in vouchers: + if x.doctype == "Payment Entry": + data.append(build_payment_entry_dict(x)) + elif x.doctype == "Journal Entry": + data.append(build_journal_entry_dict(x)) + return data + + +def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters): + je = qb.DocType("Journal Entry") + jea = qb.DocType("Journal Entry Account") + doctype_name = ConstantColumn("Journal Entry") + + journals = ( + qb.from_(je) + .inner_join(jea) + .on(je.name == jea.parent) + .select( + doctype_name.as_("doctype"), + je.name, + jea.debit_in_account_currency, + jea.credit_in_account_currency, + je.posting_date, + je.clearance_date, + ) + .where( + je.docstatus.eq(1) + & jea.account.eq(filters.account) + & je.posting_date.gt(filters.report_date) + & je.clearance_date.lte(filters.report_date) + & (je.is_opening.isnull() | je.is_opening.eq("No")) + ) + .run(as_dict=1) + ) + + ifelse = CustomFunction("IF", ["condition", "then", "else"]) + pe = qb.DocType("Payment Entry") + doctype_name = ConstantColumn("Payment Entry") + payments = ( + qb.from_(pe) + .select( + doctype_name.as_("doctype"), + pe.name, + ifelse(pe.paid_from.eq(filters.account), pe.paid_amount, pe.received_amount).as_("amount"), + pe.payment_type, + pe.party_type, + pe.posting_date, + pe.clearance_date, + ) + .where( + pe.docstatus.eq(1) + & (pe.paid_from.eq(filters.account) | pe.paid_to.eq(filters.account)) + & pe.posting_date.gt(filters.report_date) + & pe.clearance_date.lte(filters.report_date) + ) + .run(as_dict=1) + ) + + return journals + payments + + +def get_columns(): + return [ + { + "fieldname": "payment_document", + "label": _("Payment Document Type"), + "fieldtype": "Data", + "width": 220, + }, + { + "fieldname": "payment_entry", + "label": _("Payment Document"), + "fieldtype": "Dynamic Link", + "options": "payment_document", + "width": 220, + }, + { + "fieldname": "debit", + "label": _("Debit"), + "fieldtype": "Currency", + "options": "account_currency", + "width": 120, + }, + { + "fieldname": "credit", + "label": _("Credit"), + "fieldtype": "Currency", + "options": "account_currency", + "width": 120, + }, + {"fieldname": "posting_date", "label": _("Posting Date"), "fieldtype": "Date", "width": 110}, + {"fieldname": "clearance_date", "label": _("Clearance Date"), "fieldtype": "Date", "width": 110}, + ] 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 bad89e93259a..679fbfe2e58d 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -767,8 +767,12 @@ def get_daily_depr_amount(asset, row, schedule_idx, amount): every_year_depr = amount / total_years + depr_period_start_date = add_days( + get_last_day(add_months(row.depreciation_start_date, row.frequency_of_depreciation * -1)), 1 + ) + year_start_date = add_years( - row.depreciation_start_date, (row.frequency_of_depreciation * schedule_idx) // 12 + depr_period_start_date, ((row.frequency_of_depreciation * schedule_idx) // 12) ) year_end_date = add_days(add_years(year_start_date, 1), -1) diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index bccab8b01e07..3b72953c5636 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -97,7 +97,7 @@ def autoname(self): elif supp_master_name == "Naming Series": set_name_by_naming_series(self) else: - self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self) + set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self) def on_update(self): self.create_primary_contact() diff --git a/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.js b/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.js new file mode 100644 index 000000000000..37870b43b6db --- /dev/null +++ b/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.js @@ -0,0 +1,62 @@ +// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["Item-wise Purchase History"] = { + filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, + }, + { + fieldname: "from_date", + reqd: 1, + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + }, + { + fieldname: "to_date", + reqd: 1, + default: frappe.datetime.get_today(), + label: __("To Date"), + fieldtype: "Date", + }, + { + fieldname: "item_group", + label: __("Item Group"), + fieldtype: "Link", + options: "Item Group", + }, + { + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + options: "Item", + get_query: () => { + return { + query: "erpnext.controllers.queries.item_query", + }; + }, + }, + { + fieldname: "supplier", + label: __("Supplier"), + fieldtype: "Link", + options: "Supplier", + }, + ], + + formatter: function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + let format_fields = ["received_qty", "billed_amt"]; + + if (format_fields.includes(column.fieldname) && data && data[column.fieldname] > 0) { + value = "" + value + ""; + } + return value; + }, +}; diff --git a/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.json b/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.json index 521c68c53298..35045afcf8b9 100644 --- a/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.json +++ b/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.json @@ -1,30 +1,30 @@ { - "add_total_row": 1, - "apply_user_permissions": 1, - "creation": "2013-05-03 14:55:53", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2017-02-24 20:08:57.446613", - "modified_by": "Administrator", - "module": "Buying", - "name": "Item-wise Purchase History", - "owner": "Administrator", - "query": "select\n po_item.item_code as \"Item Code:Link/Item:120\",\n\tpo_item.item_name as \"Item Name::120\",\n po_item.item_group as \"Item Group:Link/Item Group:120\",\n\tpo_item.description as \"Description::150\",\n\tpo_item.qty as \"Qty:Float:100\",\n\tpo_item.uom as \"UOM:Link/UOM:80\",\n\tpo_item.base_rate as \"Rate:Currency:120\",\n\tpo_item.base_amount as \"Amount:Currency:120\",\n\tpo.name as \"Purchase Order:Link/Purchase Order:120\",\n\tpo.transaction_date as \"Transaction Date:Date:140\",\n\tpo.supplier as \"Supplier:Link/Supplier:130\",\n sup.supplier_name as \"Supplier Name::150\",\n\tpo_item.project as \"Project:Link/Project:130\",\n\tifnull(po_item.received_qty, 0) as \"Received Qty:Float:120\",\n\tpo.company as \"Company:Link/Company:\"\nfrom\n\t`tabPurchase Order` po, `tabPurchase Order Item` po_item, `tabSupplier` sup\nwhere\n\tpo.name = po_item.parent and po.supplier = sup.name and po.docstatus = 1\norder by po.name desc", - "ref_doctype": "Purchase Order", - "report_name": "Item-wise Purchase History", - "report_type": "Query Report", + "add_total_row": 1, + "creation": "2013-05-03 14:55:53", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 5, + "is_standard": "Yes", + "modified": "2024-06-19 12:12:15.418799", + "modified_by": "Administrator", + "module": "Buying", + "name": "Item-wise Purchase History", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Purchase Order", + "report_name": "Item-wise Purchase History", + "report_type": "Script Report", "roles": [ { "role": "Stock User" - }, + }, { "role": "Purchase Manager" - }, + }, { "role": "Purchase User" } - ] + ] } \ No newline at end of file diff --git a/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.py b/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.py new file mode 100644 index 000000000000..a8950af3ea37 --- /dev/null +++ b/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.py @@ -0,0 +1,276 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.utils import flt +from frappe.utils.nestedset import get_descendants_of + + +def execute(filters=None): + filters = frappe._dict(filters or {}) + if filters.from_date > filters.to_date: + frappe.throw(_("From Date cannot be greater than To Date")) + + columns = get_columns(filters) + data = get_data(filters) + + chart_data = get_chart_data(data) + + return columns, data, None, chart_data + + +def get_columns(filters): + return [ + { + "label": _("Item Code"), + "fieldtype": "Link", + "fieldname": "item_code", + "options": "Item", + "width": 120, + }, + { + "label": _("Item Name"), + "fieldtype": "Data", + "fieldname": "item_name", + "width": 140, + }, + { + "label": _("Item Group"), + "fieldtype": "Link", + "fieldname": "item_group", + "options": "Item Group", + "width": 120, + }, + { + "label": _("Description"), + "fieldtype": "Data", + "fieldname": "description", + "width": 140, + }, + { + "label": _("Quantity"), + "fieldtype": "Float", + "fieldname": "quantity", + "width": 120, + }, + { + "label": _("UOM"), + "fieldtype": "Link", + "fieldname": "uom", + "options": "UOM", + "width": 90, + }, + { + "label": _("Rate"), + "fieldname": "rate", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Amount"), + "fieldname": "amount", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Purchase Order"), + "fieldtype": "Link", + "fieldname": "purchase_order", + "options": "Purchase Order", + "width": 160, + }, + { + "label": _("Transaction Date"), + "fieldtype": "Date", + "fieldname": "transaction_date", + "width": 110, + }, + { + "label": _("Supplier"), + "fieldtype": "Link", + "fieldname": "supplier", + "options": "Supplier", + "width": 100, + }, + { + "label": _("Supplier Name"), + "fieldtype": "Data", + "fieldname": "supplier_name", + "width": 140, + }, + { + "label": _("Supplier Group"), + "fieldtype": "Link", + "fieldname": "supplier_group", + "options": "Supplier Group", + "width": 120, + }, + { + "label": _("Project"), + "fieldtype": "Link", + "fieldname": "project", + "options": "Project", + "width": 100, + }, + { + "label": _("Received Quantity"), + "fieldtype": "Float", + "fieldname": "received_qty", + "width": 150, + }, + { + "label": _("Billed Amount"), + "fieldtype": "Currency", + "fieldname": "billed_amt", + "options": "currency", + "width": 120, + }, + { + "label": _("Company"), + "fieldtype": "Link", + "fieldname": "company", + "options": "Company", + "width": 100, + }, + { + "label": _("Currency"), + "fieldtype": "Link", + "fieldname": "currency", + "options": "Currency", + "hidden": 1, + }, + ] + + +def get_data(filters): + data = [] + + company_list = get_descendants_of("Company", filters.get("company")) + company_list.append(filters.get("company")) + + supplier_details = get_supplier_details() + item_details = get_item_details() + purchase_order_records = get_purchase_order_details(company_list, filters) + + for record in purchase_order_records: + supplier_record = supplier_details.get(record.supplier) + item_record = item_details.get(record.item_code) + row = { + "item_code": record.get("item_code"), + "item_name": item_record.get("item_name"), + "item_group": item_record.get("item_group"), + "description": record.get("description"), + "quantity": record.get("qty"), + "uom": record.get("uom"), + "rate": record.get("base_rate"), + "amount": record.get("base_amount"), + "purchase_order": record.get("name"), + "transaction_date": record.get("transaction_date"), + "supplier": record.get("supplier"), + "supplier_name": supplier_record.get("supplier_name"), + "supplier_group": supplier_record.get("supplier_group"), + "project": record.get("project"), + "received_qty": flt(record.get("received_qty")), + "billed_amt": flt(record.get("billed_amt")), + "company": record.get("company"), + } + row["currency"] = frappe.get_cached_value("Company", row["company"], "default_currency") + data.append(row) + + return data + + +def get_supplier_details(): + details = frappe.get_all("Supplier", fields=["name", "supplier_name", "supplier_group"]) + supplier_details = {} + for d in details: + supplier_details.setdefault( + d.name, + frappe._dict({"supplier_name": d.supplier_name, "supplier_group": d.supplier_group}), + ) + return supplier_details + + +def get_item_details(): + details = frappe.db.get_all("Item", fields=["name", "item_name", "item_group"]) + item_details = {} + for d in details: + item_details.setdefault(d.name, frappe._dict({"item_name": d.item_name, "item_group": d.item_group})) + return item_details + + +def get_purchase_order_details(company_list, filters): + db_po = frappe.qb.DocType("Purchase Order") + db_po_item = frappe.qb.DocType("Purchase Order Item") + + query = ( + frappe.qb.from_(db_po) + .inner_join(db_po_item) + .on(db_po_item.parent == db_po.name) + .select( + db_po.name, + db_po.supplier, + db_po.transaction_date, + db_po.project, + db_po.company, + db_po_item.item_code, + db_po_item.description, + db_po_item.qty, + db_po_item.uom, + db_po_item.base_rate, + db_po_item.base_amount, + db_po_item.received_qty, + (db_po_item.billed_amt * db_po.conversion_rate).as_("billed_amt"), + ) + .where(db_po.docstatus == 1) + .where(db_po.company.isin(tuple(company_list))) + ) + + for field in ("item_code", "item_group"): + if filters.get(field): + query = query.where(db_po_item[field] == filters[field]) + + if filters.get("from_date"): + query = query.where(db_po.transaction_date >= filters.from_date) + + if filters.get("to_date"): + query = query.where(db_po.transaction_date <= filters.to_date) + + if filters.get("supplier"): + query = query.where(db_po.supplier == filters.supplier) + + return query.run(as_dict=1) + + +def get_chart_data(data): + item_wise_purchase_map = {} + labels, datapoints = [], [] + + for row in data: + item_key = row.get("item_code") + + if item_key not in item_wise_purchase_map: + item_wise_purchase_map[item_key] = 0 + + item_wise_purchase_map[item_key] = flt(item_wise_purchase_map[item_key]) + flt(row.get("amount")) + + item_wise_purchase_map = { + item: value + for item, value in (sorted(item_wise_purchase_map.items(), key=lambda i: i[1], reverse=True)) + } + + for key in item_wise_purchase_map: + labels.append(key) + datapoints.append(item_wise_purchase_map[key]) + + return { + "data": { + "labels": labels[:30], # show max of 30 items in chart + "datasets": [{"name": _("Total Purchase Amount"), "values": datapoints[:30]}], + }, + "type": "bar", + "fieldtype": "Currency", + } diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 58e120e14b08..ce5d813b8013 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1334,6 +1334,12 @@ def make_exchange_gain_loss_journal( # Cancelling existing exchange gain/loss journals is handled during the `on_cancel` event. # see accounts/utils.py:cancel_exchange_gain_loss_journal() if self.docstatus == 1: + if dimensions_dict is None: + dimensions_dict = frappe._dict() + active_dimensions = get_dimensions()[0] + for dim in active_dimensions: + dimensions_dict[dim.fieldname] = self.get(dim.fieldname) + if self.get("doctype") == "Journal Entry": # 'args' is populated with exchange gain/loss account and the amount to be booked. # These are generated by Sales/Purchase Invoice during reconciliation and advance allocation. diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json index 07641d20c332..75373398b095 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -185,7 +185,8 @@ { "fieldname": "expected_closing", "fieldtype": "Date", - "label": "Expected Closing Date" + "label": "Expected Closing Date", + "no_copy": 1 }, { "fieldname": "section_break_14", @@ -357,6 +358,7 @@ "fieldname": "transaction_date", "fieldtype": "Date", "label": "Opportunity Date", + "no_copy": 1, "oldfieldname": "transaction_date", "oldfieldtype": "Date", "reqd": 1, @@ -388,6 +390,7 @@ "fieldname": "first_response_time", "fieldtype": "Duration", "label": "First Response Time", + "no_copy": 1, "read_only": 1 }, { @@ -622,7 +625,7 @@ "icon": "fa fa-info-sign", "idx": 195, "links": [], - "modified": "2022-10-13 12:42:21.545636", + "modified": "2024-08-20 04:12:29.095761", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity", diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index d1f2cdbd1aa4..627e5262dc9d 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2053,6 +2053,55 @@ def test_partial_material_consumption_with_batch(self): "BOM", ) + def test_disassemby_order(self): + fg_item = "Test Disassembly Item" + source_warehouse = "Stores - _TC" + raw_materials = ["Test Disassembly RM Item 1", "Test Disassembly RM Item 2"] + + make_item(fg_item, {"is_stock_item": 1}) + for item in raw_materials: + make_item(item, {"is_stock_item": 1}) + test_stock_entry.make_stock_entry( + item_code=item, + target=source_warehouse, + qty=1, + basic_rate=100, + ) + + make_bom(item=fg_item, source_warehouse=source_warehouse, raw_materials=raw_materials) + + wo = make_wo_order_test_record( + item=fg_item, + qty=1, + source_warehouse=source_warehouse, + skip_transfer=1, + ) + + stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1)) + for row in stock_entry.items: + if row.item_code in raw_materials: + row.s_warehouse = source_warehouse + + stock_entry.submit() + + wo.reload() + self.assertEqual(wo.status, "Completed") + + stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Disassemble", 1)) + stock_entry.save() + + self.assertEqual(stock_entry.purpose, "Disassemble") + + for row in stock_entry.items: + if row.item_code == fg_item: + self.assertTrue(row.s_warehouse) + self.assertFalse(row.t_warehouse) + else: + self.assertFalse(row.s_warehouse) + self.assertTrue(row.t_warehouse) + + stock_entry.submit() + def make_operation(**kwargs): kwargs = frappe._dict(kwargs) @@ -2370,6 +2419,7 @@ def make_wo_order_test_record(**args): wo_order.batch_size = args.batch_size or 0 if args.source_warehouse: + wo_order.source_warehouse = args.source_warehouse for item in wo_order.get("required_items"): item.source_warehouse = args.source_warehouse diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 1da33f0ad9b1..ae888c797144 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -183,13 +183,30 @@ frappe.ui.form.on("Work Order", { } } + if (frm.doc.status == "Completed") { + if (frm.doc.__onload.backflush_raw_materials_based_on == "Material Transferred for Manufacture") { + frm.add_custom_button( + __("BOM"), + () => { + frm.trigger("make_bom"); + }, + __("Create") + ); + } + } + if ( - frm.doc.status == "Completed" && - frm.doc.__onload.backflush_raw_materials_based_on == "Material Transferred for Manufacture" + frm.doc.docstatus === 1 && + ["Closed", "Completed"].includes(frm.doc.status) && + frm.doc.produced_qty > 0 ) { - frm.add_custom_button(__("Create BOM"), () => { - frm.trigger("make_bom"); - }); + frm.add_custom_button( + __("Disassembly Order"), + () => { + frm.trigger("make_disassembly_order"); + }, + __("Create") + ); } frm.trigger("add_custom_button_to_return_components"); @@ -337,6 +354,23 @@ frappe.ui.form.on("Work Order", { }); }, + make_disassembly_order(frm) { + erpnext.work_order + .show_prompt_for_qty_input(frm, "Disassemble") + .then((data) => { + return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", { + work_order_id: frm.doc.name, + purpose: "Disassemble", + qty: data.qty, + target_warehouse: data.target_warehouse, + }); + }) + .then((stock_entry) => { + frappe.model.sync(stock_entry); + frappe.set_route("Form", stock_entry.doctype, stock_entry.name); + }); + }, + show_progress_for_items: function (frm) { var bars = []; var message = ""; @@ -745,6 +779,10 @@ erpnext.work_order = { get_max_transferable_qty: (frm, purpose) => { let max = 0; + if (purpose === "Disassemble") { + return flt(frm.doc.produced_qty); + } + if (frm.doc.skip_transfer) { max = flt(frm.doc.qty) - flt(frm.doc.produced_qty); } else { @@ -759,15 +797,38 @@ erpnext.work_order = { show_prompt_for_qty_input: function (frm, purpose) { let max = this.get_max_transferable_qty(frm, purpose); + + let fields = [ + { + fieldtype: "Float", + label: __("Qty for {0}", [__(purpose)]), + fieldname: "qty", + description: __("Max: {0}", [max]), + default: max, + }, + ]; + + if (purpose === "Disassemble") { + fields.push({ + fieldtype: "Link", + options: "Warehouse", + fieldname: "target_warehouse", + label: __("Target Warehouse"), + default: frm.doc.source_warehouse || frm.doc.wip_warehouse, + get_query() { + return { + filters: { + company: frm.doc.company, + is_group: 0, + }, + }; + }, + }); + } + return new Promise((resolve, reject) => { frappe.prompt( - { - fieldtype: "Float", - label: __("Qty for {0}", [__(purpose)]), - fieldname: "qty", - description: __("Max: {0}", [max]), - default: max, - }, + fields, (data) => { max += (frm.doc.qty * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100; diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 904e42a6a035..057c49949ecc 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1359,7 +1359,7 @@ def set_work_order_ops(name): @frappe.whitelist() -def make_stock_entry(work_order_id, purpose, qty=None): +def make_stock_entry(work_order_id, purpose, qty=None, target_warehouse=None): work_order = frappe.get_doc("Work Order", work_order_id) if not frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group"): wip_warehouse = work_order.wip_warehouse @@ -1389,9 +1389,16 @@ def make_stock_entry(work_order_id, purpose, qty=None): stock_entry.to_warehouse = work_order.fg_warehouse stock_entry.project = work_order.project + if purpose == "Disassemble": + stock_entry.from_warehouse = work_order.fg_warehouse + stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse + stock_entry.set_stock_entry_type() stock_entry.get_items() - stock_entry.set_serial_no_batch_for_finished_good() + + if purpose != "Disassemble": + stock_entry.set_serial_no_batch_for_finished_good() + return stock_entry.as_dict() diff --git a/erpnext/patches.txt b/erpnext/patches.txt index bece96d64e89..feaac9480991 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -372,3 +372,5 @@ erpnext.patches.v15_0.update_asset_repair_field_in_stock_entry erpnext.patches.v15_0.update_total_number_of_booked_depreciations erpnext.patches.v15_0.do_not_use_batchwise_valuation erpnext.patches.v15_0.drop_index_posting_datetime_from_sle +erpnext.patches.v15_0.add_disassembly_order_stock_entry_type #1 +erpnext.patches.v15_0.set_standard_stock_entry_type diff --git a/erpnext/patches/v15_0/add_disassembly_order_stock_entry_type.py b/erpnext/patches/v15_0/add_disassembly_order_stock_entry_type.py new file mode 100644 index 000000000000..1f3413b172b7 --- /dev/null +++ b/erpnext/patches/v15_0/add_disassembly_order_stock_entry_type.py @@ -0,0 +1,13 @@ +import frappe + + +def execute(): + if not frappe.db.exists("Stock Entry Type", "Disassemble"): + frappe.get_doc( + { + "doctype": "Stock Entry Type", + "name": "Disassemble", + "purpose": "Disassemble", + "is_standard": 1, + } + ).insert(ignore_permissions=True) diff --git a/erpnext/patches/v15_0/set_standard_stock_entry_type.py b/erpnext/patches/v15_0/set_standard_stock_entry_type.py new file mode 100644 index 000000000000..4a721abae402 --- /dev/null +++ b/erpnext/patches/v15_0/set_standard_stock_entry_type.py @@ -0,0 +1,17 @@ +import frappe + + +def execute(): + for stock_entry_type in [ + "Material Issue", + "Material Receipt", + "Material Transfer", + "Material Transfer for Manufacture", + "Material Consumption for Manufacture", + "Manufacture", + "Repack", + "Send to Subcontractor", + "Disassemble", + ]: + if frappe.db.exists("Stock Entry Type", stock_entry_type): + frappe.db.set_value("Stock Entry Type", stock_entry_type, "is_standard", 1) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 4696c7f3b4dc..d8bf4a5cf179 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1421,12 +1421,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe let show = cint(this.frm.doc.discount_amount) || ((this.frm.doc.taxes || []).filter(function(d) {return d.included_in_print_rate===1}).length); - if(frappe.meta.get_docfield(cur_frm.doctype, "net_total")) + if(this.frm.doc.doctype && frappe.meta.get_docfield(this.frm.doc.doctype, "net_total")) { this.frm.toggle_display("net_total", show); + } - if(frappe.meta.get_docfield(cur_frm.doctype, "base_net_total")) + if(this.frm.doc.doctype && frappe.meta.get_docfield(this.frm.doc.doctype, "base_net_total")) { this.frm.toggle_display("base_net_total", (show && (me.frm.doc.currency != company_currency))); - + } } change_grid_labels(company_currency) { diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 59f082431b80..dec09e512fe7 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -308,13 +308,15 @@ "fetch_from": "customer_primary_contact.mobile_no", "fieldname": "mobile_no", "fieldtype": "Read Only", - "label": "Mobile No" + "label": "Mobile No", + "options": "Mobile" }, { "fetch_from": "customer_primary_contact.email_id", "fieldname": "email_id", "fieldtype": "Read Only", - "label": "Email Id" + "label": "Email Id", + "options": "Email" }, { "fieldname": "column_break_26", @@ -592,7 +594,7 @@ "link_fieldname": "party" } ], - "modified": "2024-05-08 18:03:20.716169", + "modified": "2024-06-17 03:24:59.612974", "modified_by": "Administrator", "module": "Selling", "name": "Customer", @@ -677,4 +679,4 @@ "states": [], "title_field": "customer_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 9bd9b5e760ad..0e9c1f3e790c 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -105,7 +105,7 @@ def autoname(self): elif cust_master_name == "Naming Series": set_name_by_naming_series(self) else: - self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self) + set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self) def get_customer_name(self): if frappe.db.get_value("Customer", self.customer_name) and not frappe.flags.in_import: diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 0bcb9fb60194..270a9e060543 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -66,29 +66,54 @@ def install(country=None): "parent_item_group": _("All Item Groups"), }, # Stock Entry Type - {"doctype": "Stock Entry Type", "name": "Material Issue", "purpose": "Material Issue"}, - {"doctype": "Stock Entry Type", "name": "Material Receipt", "purpose": "Material Receipt"}, + { + "doctype": "Stock Entry Type", + "name": "Material Issue", + "purpose": "Material Issue", + "is_standard": 1, + }, + { + "doctype": "Stock Entry Type", + "name": "Material Receipt", + "purpose": "Material Receipt", + "is_standard": 1, + }, { "doctype": "Stock Entry Type", "name": "Material Transfer", "purpose": "Material Transfer", + "is_standard": 1, + }, + { + "doctype": "Stock Entry Type", + "name": "Manufacture", + "purpose": "Manufacture", + "is_standard": 1, + }, + { + "doctype": "Stock Entry Type", + "name": "Repack", + "purpose": "Repack", + "is_standard": 1, }, - {"doctype": "Stock Entry Type", "name": "Manufacture", "purpose": "Manufacture"}, - {"doctype": "Stock Entry Type", "name": "Repack", "purpose": "Repack"}, + {"doctype": "Stock Entry Type", "name": "Disassemble", "purpose": "Disassemble", "is_standard": 1}, { "doctype": "Stock Entry Type", "name": "Send to Subcontractor", "purpose": "Send to Subcontractor", + "is_standard": 1, }, { "doctype": "Stock Entry Type", "name": "Material Transfer for Manufacture", "purpose": "Material Transfer for Manufacture", + "is_standard": 1, }, { "doctype": "Stock Entry Type", "name": "Material Consumption for Manufacture", "purpose": "Material Consumption for Manufacture", + "is_standard": 1, }, # territory: with two default territories, one for home country and one named Rest of the World { diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 117ed2614391..7bf3ca4d7285 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -267,6 +267,7 @@ def update_completed_qty(self, mr_items=None, update_modified=True): mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance") for d in self.get("items"): + precision = d.precision("ordered_qty") if d.name in mr_items: if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"): d.ordered_qty = flt(mr_items_ordered_qty.get(d.name)) @@ -276,14 +277,14 @@ def update_completed_qty(self, mr_items=None, update_modified=True): (d.qty + (d.qty * (mr_qty_allowance / 100))), d.precision("ordered_qty") ) - if d.ordered_qty and d.ordered_qty > allowed_qty: + if d.ordered_qty and flt(d.ordered_qty, precision) > flt(allowed_qty, precision): frappe.throw( _( "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than allowed requested quantity {2} for Item {3}" ).format(d.ordered_qty, d.parent, allowed_qty, d.item_code) ) - elif d.ordered_qty and d.ordered_qty > d.stock_qty: + elif d.ordered_qty and flt(d.ordered_qty, precision) > flt(d.stock_qty, precision): frappe.throw( _( "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than requested quantity {2} for Item {3}" diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 3c97d13b904a..afcb00141a15 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1075,6 +1075,7 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate if item.billed_amt and item.amount: adjusted_amt = flt(item.billed_amt) - flt(item.amount) + adjusted_amt = adjusted_amt * flt(pr_doc.conversion_rate) item.db_set("rate_difference_with_purchase_invoice", adjusted_amt, update_modified=False) percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index c239360d945b..4cb53e753e7a 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -3,7 +3,7 @@ import frappe from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, cint, cstr, flt, getdate, nowtime, today +from frappe.utils import add_days, cint, cstr, flt, get_datetime, getdate, nowtime, today from pypika import functions as fn import erpnext @@ -3592,6 +3592,71 @@ def test_internal_transfer_for_batch_items_with_cancel_use_serial_batch_fields(s inter_transfer_dn.cancel() frappe.db.set_single_value("Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 1) + def test_sles_with_same_posting_datetime_and_creation(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + from erpnext.stock.report.stock_balance.stock_balance import execute + + item_code = "Test Item for SLE with same posting datetime and creation" + create_item(item_code) + + pr = make_purchase_receipt( + item_code=item_code, + qty=10, + rate=100, + posting_date="2023-11-06", + posting_time="00:00:00", + ) + + sr = make_stock_entry( + item_code=item_code, + source=pr.items[0].warehouse, + qty=10, + posting_date="2023-11-07", + posting_time="14:28:0.330404", + ) + + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": sr.doctype, "voucher_no": sr.name, "item_code": sr.items[0].item_code}, + "name", + ) + + sle_doc = frappe.get_doc("Stock Ledger Entry", sle) + sle_doc.db_set("creation", "2023-11-07 14:28:01.208930") + + sle_doc.reload() + self.assertEqual(get_datetime(sle_doc.creation), get_datetime("2023-11-07 14:28:01.208930")) + + sr = make_stock_entry( + item_code=item_code, + target=pr.items[0].warehouse, + qty=50, + posting_date="2023-11-07", + posting_time="14:28:0.920825", + ) + + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": sr.doctype, "voucher_no": sr.name, "item_code": sr.items[0].item_code}, + "name", + ) + + sle_doc = frappe.get_doc("Stock Ledger Entry", sle) + sle_doc.db_set("creation", "2023-11-07 14:28:01.044561") + + sle_doc.reload() + self.assertEqual(get_datetime(sle_doc.creation), get_datetime("2023-11-07 14:28:01.044561")) + + pr.repost_future_sle_and_gle(force=True) + + columns, data = execute( + filters=frappe._dict( + {"item_code": item_code, "warehouse": pr.items[0].warehouse, "company": pr.company} + ) + ) + + self.assertEqual(data[0].get("bal_qty"), 50.0) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 3f467d3627ac..5ba9f2973a1b 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -127,7 +127,7 @@ "label": "Purpose", "oldfieldname": "purpose", "oldfieldtype": "Select", - "options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor", + "options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor\nDisassemble", "read_only": 1, "search_index": 1 }, @@ -143,7 +143,7 @@ "reqd": 1 }, { - "depends_on": "eval:in_list([\"Material Transfer for Manufacture\", \"Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)", + "depends_on": "eval:in_list([\"Material Transfer for Manufacture\", \"Manufacture\", \"Material Consumption for Manufacture\", \"Disassemble\"], doc.purpose)", "fieldname": "work_order", "fieldtype": "Link", "label": "Work Order", @@ -242,7 +242,7 @@ }, { "default": "0", - "depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \"Send to Subcontractor\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)", + "depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \"Send to Subcontractor\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\", \"Disassemble\"], doc.purpose)", "fieldname": "from_bom", "fieldtype": "Check", "label": "From BOM", @@ -697,7 +697,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-08-13 19:02:42.386955", + "modified": "2024-08-13 19:05:42.386955", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 83ae8d09f66c..0ef54c785557 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -132,6 +132,7 @@ class StockEntry(StockController): "Manufacture", "Repack", "Send to Subcontractor", + "Disassemble", ] remarks: DF.Text | None sales_invoice_no: DF.Link | None @@ -337,6 +338,7 @@ def validate_purpose(self): "Repack", "Send to Subcontractor", "Material Consumption for Manufacture", + "Disassemble", ] if self.purpose not in valid_purposes: @@ -616,6 +618,7 @@ def validate_work_order(self): "Manufacture", "Material Transfer for Manufacture", "Material Consumption for Manufacture", + "Disassemble", ): # check if work order is entered @@ -983,7 +986,7 @@ def set_total_amount(self): def set_stock_entry_type(self): if self.purpose: self.stock_entry_type = frappe.get_cached_value( - "Stock Entry Type", {"purpose": self.purpose}, "name" + "Stock Entry Type", {"purpose": self.purpose, "is_standard": 1}, "name" ) def set_purpose_for_stock_entry(self): @@ -1703,11 +1706,63 @@ def set_items_for_stock_in(self): }, ) + def get_items_for_disassembly(self): + """Get items for Disassembly Order""" + + if not self.work_order: + frappe.throw(_("The Work Order is mandatory for Disassembly Order")) + + items = self.get_items_from_manufacture_entry() + + s_warehouse = "" + if self.work_order: + s_warehouse = frappe.db.get_value("Work Order", self.work_order, "fg_warehouse") + + for row in items: + child_row = self.append("items", {}) + for field, value in row.items(): + if value is not None: + child_row.set(field, value) + + child_row.s_warehouse = (self.from_warehouse or s_warehouse) if row.is_finished_item else "" + child_row.t_warehouse = self.to_warehouse if not row.is_finished_item else "" + child_row.is_finished_item = 0 if row.is_finished_item else 1 + + def get_items_from_manufacture_entry(self): + return frappe.get_all( + "Stock Entry", + fields=[ + "`tabStock Entry Detail`.`item_code`", + "`tabStock Entry Detail`.`item_name`", + "`tabStock Entry Detail`.`description`", + "`tabStock Entry Detail`.`qty`", + "`tabStock Entry Detail`.`transfer_qty`", + "`tabStock Entry Detail`.`stock_uom`", + "`tabStock Entry Detail`.`uom`", + "`tabStock Entry Detail`.`basic_rate`", + "`tabStock Entry Detail`.`conversion_factor`", + "`tabStock Entry Detail`.`is_finished_item`", + "`tabStock Entry Detail`.`batch_no`", + "`tabStock Entry Detail`.`serial_no`", + "`tabStock Entry Detail`.`use_serial_batch_fields`", + ], + filters=[ + ["Stock Entry", "purpose", "=", "Manufacture"], + ["Stock Entry", "work_order", "=", self.work_order], + ["Stock Entry", "docstatus", "=", 1], + ["Stock Entry Detail", "docstatus", "=", 1], + ], + order_by="`tabStock Entry Detail`.`idx` desc, `tabStock Entry Detail`.`is_finished_item` desc", + ) + @frappe.whitelist() def get_items(self): self.set("items", []) self.validate_work_order() + if self.purpose == "Disassemble": + return self.get_items_for_disassembly() + if not self.posting_date or not self.posting_time: frappe.throw(_("Posting date and posting time is mandatory")) diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json index db8e1fef69ba..c522df599416 100644 --- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json +++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json @@ -7,7 +7,8 @@ "engine": "InnoDB", "field_order": [ "purpose", - "add_to_transit" + "add_to_transit", + "is_standard" ], "fields": [ { @@ -16,7 +17,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Purpose", - "options": "\nMaterial Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor", + "options": "\nMaterial Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor\nDisassemble", "reqd": 1, "set_only_once": 1 }, @@ -26,13 +27,21 @@ "fieldname": "add_to_transit", "fieldtype": "Check", "label": "Add to Transit" + }, + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "label": "Is Standard", + "read_only": 1 } ], "links": [], - "modified": "2024-07-08 08:41:19.385020", + "modified": "2024-08-24 16:00:22.696958", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Type", + "naming_rule": "Set by user", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py index 034223122f60..8d05a43408b9 100644 --- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py +++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py @@ -2,7 +2,7 @@ # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document @@ -16,6 +16,7 @@ class StockEntryType(Document): from frappe.types import DF add_to_transit: DF.Check + is_standard: DF.Check purpose: DF.Literal[ "", "Material Issue", @@ -26,9 +27,25 @@ class StockEntryType(Document): "Manufacture", "Repack", "Send to Subcontractor", + "Disassemble", ] # end: auto-generated types def validate(self): + self.validate_standard_type() if self.add_to_transit and self.purpose != "Material Transfer": self.add_to_transit = 0 + + def validate_standard_type(self): + if self.is_standard and self.name not in [ + "Material Issue", + "Material Receipt", + "Material Transfer", + "Material Transfer for Manufacture", + "Material Consumption for Manufacture", + "Manufacture", + "Repack", + "Send to Subcontractor", + "Disassemble", + ]: + frappe.throw(f"Stock Entry Type {self.name} cannot be set as standard") diff --git a/erpnext/stock/doctype/stock_entry_type/test_stock_entry_type.py b/erpnext/stock/doctype/stock_entry_type/test_stock_entry_type.py index 83ebe7e651b5..61156545e340 100644 --- a/erpnext/stock/doctype/stock_entry_type/test_stock_entry_type.py +++ b/erpnext/stock/doctype/stock_entry_type/test_stock_entry_type.py @@ -3,6 +3,33 @@ import unittest +import frappe + class TestStockEntryType(unittest.TestCase): - pass + def test_stock_entry_type_non_standard(self): + stock_entry_type = "Test Manufacturing" + + doc = frappe.get_doc( + { + "doctype": "Stock Entry Type", + "__newname": stock_entry_type, + "purpose": "Manufacture", + "is_standard": 1, + } + ) + + self.assertRaises(frappe.ValidationError, doc.insert) + + def test_stock_entry_type_is_standard(self): + for stock_entry_type in [ + "Material Issue", + "Material Receipt", + "Material Transfer", + "Material Transfer for Manufacture", + "Material Consumption for Manufacture", + "Manufacture", + "Repack", + "Send to Subcontractor", + ]: + self.assertTrue(frappe.db.get_value("Stock Entry Type", stock_entry_type, "is_standard")) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json index 58b6e4a74b60..d9af64c3983a 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json @@ -352,7 +352,8 @@ { "fieldname": "posting_datetime", "fieldtype": "Datetime", - "label": "Posting Datetime" + "label": "Posting Datetime", + "search_index": 1 } ], "hide_toolbar": 1, @@ -361,7 +362,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2024-06-27 16:23:18.820049", + "modified": "2024-08-27 09:28:03.961443", "modified_by": "Administrator", "module": "Stock", "name": "Stock Ledger Entry", diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index cff1886d7fe9..319303dbbb0d 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -351,3 +351,4 @@ def on_doctype_update(): frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"]) frappe.db.add_index("Stock Ledger Entry", ["warehouse", "item_code"], "item_warehouse") + frappe.db.add_index("Stock Ledger Entry", ["posting_datetime", "creation"]) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index 587b25ea1545..42e402e00059 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -1187,7 +1187,7 @@ def test_backdated_sle_with_same_timestamp(self): qty=5, posting_date="2021-01-01", rate=10, - posting_time="02:00:00.1234", + posting_time="02:00:00", ) time.sleep(3) @@ -1199,7 +1199,7 @@ def test_backdated_sle_with_same_timestamp(self): qty=100, rate=10, posting_date="2021-01-01", - posting_time="02:00:00", + posting_time="02:00:00.1234", ) sle = frappe.get_all( diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index 822da13cc722..0bb9d40581a6 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -168,7 +168,7 @@ def get_stock_ledger_entries_for_batch_bundle(filters): & (sle.has_batch_no == 1) & (sle.posting_date <= filters["to_date"]) ) - .groupby(batch_package.batch_no, batch_package.warehouse) + .groupby(sle.voucher_no, batch_package.batch_no, batch_package.warehouse) .orderby(sle.item_code, sle.warehouse) ) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 71e664dd0668..46724be5927d 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -183,7 +183,7 @@ def set_serial_and_batch_bundle(self, sn_doc): } if self.sle.actual_qty < 0 and self.is_material_transfer(): - values_to_update["valuation_rate"] = sn_doc.avg_rate + values_to_update["valuation_rate"] = flt(sn_doc.avg_rate) if not frappe.db.get_single_value( "Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle" diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 0961d9bd35e1..2aacd86e3d24 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1543,7 +1543,7 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc and ( posting_datetime {operator} %(posting_datetime)s ) - order by posting_datetime desc, creation desc + order by posting_date desc, posting_time desc, creation desc limit 1 for update""", { @@ -1636,7 +1636,7 @@ def get_stock_ledger_entries( where item_code = %(item_code)s and is_cancelled = 0 {conditions} - order by posting_datetime {order}, creation {order} + order by posting_date {order}, posting_time {order}, creation {order} {limit} {for_update}""".format( conditions=conditions, limit=limit or "", @@ -1753,7 +1753,7 @@ def get_valuation_rate( AND valuation_rate >= 0 AND is_cancelled = 0 AND NOT (voucher_no = %s AND voucher_type = %s) - order by posting_datetime desc, name desc limit 1""", + order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type), ): return flt(last_valuation_rate[0][0]) @@ -2004,7 +2004,7 @@ def get_future_sle_with_negative_qty(args): and posting_datetime >= %(posting_datetime)s and is_cancelled = 0 and qty_after_transaction < 0 - order by posting_datetime asc + order by posting_date asc, posting_time asc limit 1 """, args, @@ -2018,14 +2018,14 @@ def get_future_sle_with_negative_batch_qty(args): with batch_ledger as ( select posting_date, posting_time, posting_datetime, voucher_type, voucher_no, - sum(actual_qty) over (order by posting_datetime, creation) as cumulative_total + sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total from `tabStock Ledger Entry` where item_code = %(item_code)s and warehouse = %(warehouse)s and batch_no=%(batch_no)s and is_cancelled = 0 - order by posting_datetime, creation + order by posting_date, posting_time, creation ) select * from batch_ledger where