diff --git a/erpnext/accounts/doctype/account/account.js b/erpnext/accounts/doctype/account/account.js index f26d1f82797..e9fc37992a6 100644 --- a/erpnext/accounts/doctype/account/account.js +++ b/erpnext/accounts/doctype/account/account.js @@ -94,8 +94,8 @@ frappe.ui.form.on("Account", { function () { frappe.route_options = { account: frm.doc.name, - from_date: frappe.sys_defaults.year_start_date, - to_date: frappe.sys_defaults.year_end_date, + from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], + to_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], company: frm.doc.company, }; frappe.set_route("query-report", "General Ledger"); diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js index e0bfa6e902a..3b939aa3920 100644 --- a/erpnext/accounts/doctype/account/account_tree.js +++ b/erpnext/accounts/doctype/account/account_tree.js @@ -279,8 +279,8 @@ frappe.treeview_settings["Account"] = { click: function (node, btn) { frappe.route_options = { account: node.label, - from_date: frappe.sys_defaults.year_start_date, - to_date: frappe.sys_defaults.year_end_date, + from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], + to_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], company: frappe.treeview_settings["Account"].treeview.page.fields_dict.company.get_value(), }; diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index d2abbf8e55c..1fda91185a7 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1523,7 +1523,7 @@ class PaymentEntry(AccountsController): if paid_amount > total_negative_outstanding: if total_negative_outstanding == 0: frappe.msgprint( - _("Cannot {0} from {2} without any negative outstanding invoice").format( + _("Cannot {0} from {1} without any negative outstanding invoice").format( self.payment_type, self.party_type, ) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 90e606d7861..57f2d791199 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -987,6 +987,45 @@ class TestPricingRule(FrappeTestCase): so.save() self.assertEqual(len(so.items), 1) + def test_pricing_rule_for_product_free_item_round_free_qty(self): + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule") + test_record = { + "doctype": "Pricing Rule", + "title": "_Test Pricing Rule", + "apply_on": "Item Code", + "currency": "USD", + "items": [ + { + "item_code": "_Test Item", + } + ], + "selling": 1, + "rate": 0, + "min_qty": 100, + "max_qty": 0, + "price_or_product_discount": "Product", + "same_item": 1, + "free_qty": 10, + "round_free_qty": 1, + "is_recursive": 1, + "recurse_for": 100, + "company": "_Test Company", + } + frappe.get_doc(test_record.copy()).insert() + + # With pricing rule + so = make_sales_order(item_code="_Test Item", qty=100) + so.load_from_db() + self.assertEqual(so.items[1].is_free_item, 1) + self.assertEqual(so.items[1].item_code, "_Test Item") + self.assertEqual(so.items[1].qty, 10) + + so = make_sales_order(item_code="_Test Item", qty=150) + so.load_from_db() + self.assertEqual(so.items[1].is_free_item, 1) + self.assertEqual(so.items[1].item_code, "_Test Item") + self.assertEqual(so.items[1].qty, 10) + def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self): frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1") frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2") diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 0b3117365c0..bec4d2407de 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -642,7 +642,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): if transaction_qty: qty = flt(transaction_qty) * qty / pricing_rule.recurse_for if pricing_rule.round_free_qty: - qty = math.floor(qty) + qty = (flt(transaction_qty) // pricing_rule.recurse_for) * (pricing_rule.free_qty or 1) if not qty: return 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 550e80ec8bc..de8aecda72b 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -27,7 +27,7 @@ class RepostAccountingLedger(Document): latest_pcv = ( frappe.db.get_all( "Period Closing Voucher", - filters={"company": self.company}, + filters={"company": self.company, "docstatus": 1}, order_by="posting_date desc", pluck="posting_date", limit=1, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index dffd5d98c4f..73e51f2fcb4 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -667,20 +667,6 @@ frappe.ui.form.on('Sales Invoice', { } } - frm.set_query('company_address', function(doc) { - if(!doc.company) { - frappe.throw(__('Please set Company')); - } - - return { - query: 'frappe.contacts.doctype.address.address.address_query', - filters: { - link_doctype: 'Company', - link_name: doc.company - } - }; - }); - frm.set_query('pos_profile', function(doc) { if(!doc.company) { frappe.throw(_('Please set Company')); diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index b2a24338cbf..a5c90a37d4d 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -1004,15 +1004,15 @@ class ReceivablePayableReport: def get_columns(self): self.columns = [] - self.add_column("Posting Date", fieldtype="Date") + self.add_column(_("Posting Date"), fieldtype="Date") self.add_column( - label="Party Type", + label=_("Party Type"), fieldname="party_type", fieldtype="Data", width=100, ) self.add_column( - label="Party", + label=_("Party"), fieldname="party", fieldtype="Dynamic Link", options="party_type", @@ -1028,10 +1028,10 @@ class ReceivablePayableReport: if self.party_naming_by == "Naming Series": if self.account_type == "Payable": - label = "Supplier Name" + label = _("Supplier Name") fieldname = "supplier_name" else: - label = "Customer Name" + label = _("Customer Name") fieldname = "customer_name" self.add_column( label=label, @@ -1057,7 +1057,7 @@ class ReceivablePayableReport: width=180, ) - self.add_column(label="Due Date", fieldtype="Date") + self.add_column(label=_("Due Date"), fieldtype="Date") if self.account_type == "Payable": self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data") diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index d6a0acce427..edc0ecbd581 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -421,6 +421,7 @@ class GrossProfitGenerator: if grouped_by_invoice: buying_amount = 0 + base_amount = 0 for row in reversed(self.si_list): if self.filters.get("group_by") == "Monthly": @@ -461,12 +462,11 @@ class GrossProfitGenerator: else: row.buying_amount = flt(self.get_buying_amount(row, row.item_code), self.currency_precision) - if grouped_by_invoice: - if row.indent == 1.0: - buying_amount += row.buying_amount - elif row.indent == 0.0: - row.buying_amount = buying_amount - buying_amount = 0 + if grouped_by_invoice and row.indent == 0.0: + row.buying_amount = buying_amount + row.base_amount = base_amount + buying_amount = 0 + base_amount = 0 # get buying rate if flt(row.qty): @@ -476,11 +476,19 @@ class GrossProfitGenerator: if self.is_not_invoice_row(row): row.buying_rate, row.base_rate = 0.0, 0.0 + if self.is_not_invoice_row(row): + self.update_return_invoices(row) + + if grouped_by_invoice and row.indent == 1.0: + buying_amount += row.buying_amount + base_amount += row.base_amount + # calculate gross profit row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision) if row.base_amount: row.gross_profit_percent = flt( - (row.gross_profit / row.base_amount) * 100.0, self.currency_precision + (row.gross_profit / row.base_amount) * 100.0, + self.currency_precision, ) else: row.gross_profit_percent = 0.0 @@ -491,33 +499,29 @@ class GrossProfitGenerator: if self.grouped: self.get_average_rate_based_on_group_by() + def update_return_invoices(self, row): + if row.parent in self.returned_invoices and row.item_code in self.returned_invoices[row.parent]: + returned_item_rows = self.returned_invoices[row.parent][row.item_code] + for returned_item_row in returned_item_rows: + # returned_items 'qty' should be stateful + if returned_item_row.qty != 0: + if row.qty >= abs(returned_item_row.qty): + row.qty += returned_item_row.qty + row.base_amount += flt(returned_item_row.base_amount, self.currency_precision) + returned_item_row.qty = 0 + returned_item_row.base_amount = 0 + + else: + row.qty = 0 + row.base_amount = 0 + returned_item_row.qty += row.qty + returned_item_row.base_amount += row.base_amount + + row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision) + def get_average_rate_based_on_group_by(self): for key in list(self.grouped): - if self.filters.get("group_by") == "Invoice": - for row in self.grouped[key]: - if row.indent == 1.0: - if ( - row.parent in self.returned_invoices - and row.item_code in self.returned_invoices[row.parent] - ): - returned_item_rows = self.returned_invoices[row.parent][row.item_code] - for returned_item_row in returned_item_rows: - # returned_items 'qty' should be stateful - if returned_item_row.qty != 0: - if row.qty >= abs(returned_item_row.qty): - row.qty += returned_item_row.qty - returned_item_row.qty = 0 - else: - row.qty = 0 - returned_item_row.qty += row.qty - row.base_amount += flt(returned_item_row.base_amount, self.currency_precision) - row.buying_amount = flt( - flt(row.qty) * flt(row.buying_rate), self.currency_precision - ) - if flt(row.qty) or row.base_amount: - row = self.set_average_rate(row) - self.grouped_data.append(row) - elif self.filters.get("group_by") == "Payment Term": + if self.filters.get("group_by") == "Payment Term": for i, row in enumerate(self.grouped[key]): invoice_portion = 0 @@ -537,7 +541,7 @@ class GrossProfitGenerator: new_row = self.set_average_rate(new_row) self.grouped_data.append(new_row) - else: + elif self.filters.get("group_by") != "Invoice": for i, row in enumerate(self.grouped[key]): if i == 0: new_row = row diff --git a/erpnext/accounts/report/gross_profit/test_gross_profit.py b/erpnext/accounts/report/gross_profit/test_gross_profit.py index 741ea46a516..8b1621ae782 100644 --- a/erpnext/accounts/report/gross_profit/test_gross_profit.py +++ b/erpnext/accounts/report/gross_profit/test_gross_profit.py @@ -418,12 +418,12 @@ class TestGrossProfit(FrappeTestCase): "item_name": self.item, "warehouse": "Stores - _GP", "qty": 0.0, - "avg._selling_rate": 0.0, + "avg._selling_rate": 100, "valuation_rate": 0.0, - "selling_amount": -100.0, + "selling_amount": 0.0, "buying_amount": 0.0, - "gross_profit": -100.0, - "gross_profit_%": 100.0, + "gross_profit": 0.0, + "gross_profit_%": 0.0, } gp_entry = [x for x in data if x.parent_invoice == sinv.name] # Both items of Invoice should have '0' qty diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 88e60d71d8d..41df6361fad 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -916,13 +916,12 @@ class Asset(AccountsController): ].expected_value_after_useful_life value_after_depreciation = self.finance_books[idx].value_after_depreciation - if ( - flt(value_after_depreciation) <= expected_value_after_useful_life - or self.is_fully_depreciated - ): + if flt(value_after_depreciation) <= expected_value_after_useful_life: status = "Fully Depreciated" + self.is_fully_depreciated = 1 elif flt(value_after_depreciation) < flt(self.gross_purchase_amount): status = "Partially Depreciated" + self.is_fully_depreciated = 0 elif self.docstatus == 2: status = "Cancelled" return status diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index f7417cce76c..7c192118d27 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -454,7 +454,7 @@ def restore_asset(asset_name): def depreciate_asset(asset, date): - if not asset.calculate_depreciation: + if not asset.calculate_depreciation or asset.is_fully_depreciated: return asset.flags.ignore_validate_update_after_submit = True diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py index da1c70d3179..1a250acd4d2 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py @@ -18,6 +18,7 @@ def execute(filters=None): columns = get_columns(filters) data = get_data(filters) + update_received_amount(data) if not data: return [], [], None, [] @@ -60,7 +61,6 @@ def get_data(filters): (po_item.qty - po_item.received_qty).as_("pending_qty"), Sum(IfNull(pi_item.qty, 0)).as_("billed_qty"), po_item.base_amount.as_("amount"), - (po_item.received_qty * po_item.base_rate).as_("received_qty_amount"), (po_item.billed_amt * IfNull(po.conversion_rate, 1)).as_("billed_amount"), (po_item.base_amount - (po_item.billed_amt * IfNull(po.conversion_rate, 1))).as_( "pending_amount" @@ -92,6 +92,39 @@ def get_data(filters): return data +def update_received_amount(data): + pr_data = get_received_amount_data(data) + + for row in data: + row.received_qty_amount = flt(pr_data.get(row.name)) + + +def get_received_amount_data(data): + pr = frappe.qb.DocType("Purchase Receipt") + pr_item = frappe.qb.DocType("Purchase Receipt Item") + + query = ( + frappe.qb.from_(pr) + .inner_join(pr_item) + .on(pr_item.parent == pr.name) + .select( + pr_item.purchase_order_item, + Sum(pr_item.base_amount).as_("received_qty_amount"), + ) + .where((pr_item.parent == pr.name) & (pr.docstatus == 1)) + .groupby(pr_item.purchase_order_item) + ) + + query = query.where(pr_item.purchase_order_item.isin([row.name for row in data])) + + data = query.run() + + if not data: + return frappe._dict() + + return frappe._dict(data) + + def prepare_data(data, filters): completed, pending = 0, 0 pending_field = "pending_amount" diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json index 0f4d693544e..580168180a7 100644 --- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json +++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json @@ -15,6 +15,7 @@ "include_item_in_manufacturing", "qty_section", "required_qty", + "stock_uom", "rate", "amount", "column_break_11", @@ -138,11 +139,19 @@ "in_list_view": 1, "label": "Returned Qty ", "read_only": 1 + }, + { + "fetch_from": "item_code.stock_uom", + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2024-02-11 15:45:32.318374", + "modified": "2024-11-19 15:48:16.823384", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Item", @@ -153,4 +162,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 4c5094b2fd2..8cc7ada03c2 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -367,3 +367,4 @@ erpnext.patches.v14_0.delete_orphaned_asset_movement_item_records erpnext.patches.v14_0.remove_cancelled_asset_capitalization_from_asset erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1 erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter +erpnext.patches.v14_0.update_stock_uom_in_work_order_item diff --git a/erpnext/patches/v14_0/update_stock_uom_in_work_order_item.py b/erpnext/patches/v14_0/update_stock_uom_in_work_order_item.py new file mode 100644 index 00000000000..d611065d8f1 --- /dev/null +++ b/erpnext/patches/v14_0/update_stock_uom_in_work_order_item.py @@ -0,0 +1,15 @@ +import frappe + + +def execute(): + frappe.db.sql( + """ + UPDATE + `tabWork Order Item`, `tabItem` + SET + `tabWork Order Item`.stock_uom = `tabItem`.stock_uom + WHERE + `tabWork Order Item`.item_code = `tabItem`.name + AND `tabWork Order Item`.docstatus = 1 + """ + ) diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js index b7e685cd6fb..4bdd463f047 100644 --- a/erpnext/public/js/queries.js +++ b/erpnext/public/js/queries.js @@ -85,9 +85,13 @@ $.extend(erpnext.queries, { }, company_address_query: function (doc) { + if (!doc.company) { + frappe.throw(__("Please set {0}", [frappe.meta.get_label(doc.doctype, "company", doc.name)])); + } + return { query: "frappe.contacts.doctype.address.address.address_query", - filters: { is_your_company_address: 1, link_doctype: "Company", link_name: doc.company || "" }, + filters: { link_doctype: "Company", link_name: doc.company }, }; }, diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index d2f565ef70e..5d9966754bb 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -20,20 +20,6 @@ frappe.ui.form.on('Quotation', { frm.set_df_property('packed_items', 'cannot_add_rows', true); frm.set_df_property('packed_items', 'cannot_delete_rows', true); - - frm.set_query('company_address', function(doc) { - if(!doc.company) { - frappe.throw(__('Please set Company')); - } - - return { - query: 'frappe.contacts.doctype.address.address.address_query', - filters: { - link_doctype: 'Company', - link_name: doc.company - } - }; - }); }, refresh: function(frm) { diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 7bdaab75a53..0d9a0e2e8b3 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -21,20 +21,6 @@ frappe.ui.form.on("Sales Order", { frm.set_indicator_formatter('item_code', function(doc) { return (doc.stock_qty<=doc.delivered_qty) ? "green" : "orange" }) - frm.set_query('company_address', function(doc) { - if(!doc.company) { - frappe.throw(__('Please set Company')); - } - - return { - query: 'frappe.contacts.doctype.address.address.address_query', - filters: { - link_doctype: 'Company', - link_name: doc.company - } - }; - }) - frm.set_query("bom_no", "items", function(doc, cdt, cdn) { var row = locals[cdt][cdn]; return { diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index ad2f1e5f282..0293f37d0ad 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -40,6 +40,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran me.frm.set_query('customer_address', erpnext.queries.address_query); me.frm.set_query('shipping_address_name', erpnext.queries.address_query); me.frm.set_query('dispatch_address_name', erpnext.queries.dispatch_address_query); + me.frm.set_query('company_address', erpnext.queries.company_address_query); erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype); diff --git a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.js b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.js index a15b69b112a..5ec424f4c24 100644 --- a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.js +++ b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.js @@ -8,7 +8,7 @@ frappe.query_reports["Batch Item Expiry Status"] = { label: __("From Date"), fieldtype: "Date", width: "80", - default: frappe.sys_defaults.year_start_date, + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], reqd: 1, }, { diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js index 401ebe43028..f48e0101a0f 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js @@ -16,7 +16,7 @@ frappe.query_reports["Batch-Wise Balance History"] = { label: __("From Date"), fieldtype: "Date", width: "80", - default: frappe.sys_defaults.year_start_date, + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], reqd: 1, }, { diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.js b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.js index 3b12dab939a..8d46b14a177 100644 --- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.js +++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.js @@ -7,7 +7,7 @@ frappe.query_reports["Itemwise Recommended Reorder Level"] = { fieldname: "from_date", label: __("From Date"), fieldtype: "Date", - default: frappe.sys_defaults.year_start_date, + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], }, { fieldname: "to_date",