diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 39f9aaf7a6c..12b9800e688 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -204,6 +204,7 @@ class JournalEntry(AccountsController): if self.needs_repost: self.validate_for_repost() self.db_set("repost_required", self.needs_repost) + self.repost_accounting_entries() def on_cancel(self): # References for this Journal are removed on the `on_cancel` event in accounts_controller diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index cf0aae96260..c53faf9ff39 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -454,12 +454,9 @@ class TestJournalEntry(unittest.TestCase): # Change cost center for bank account - _Test Cost Center for BS Account create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") jv.accounts[1].cost_center = "_Test Cost Center for BS Account - _TC" + # Ledger reposted implicitly upon 'Update After Submit' jv.save() - # Check if repost flag gets set on update after submit - self.assertTrue(jv.repost_required) - jv.repost_accounting_entries() - # Check GL entries after reposting jv.load_from_db() self.expected_gle[0]["cost_center"] = "_Test Cost Center for BS Account - _TC" diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 0f64c9d697f..15f53d996da 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -808,6 +808,7 @@ class PurchaseInvoice(BuyingController): if self.needs_repost: self.validate_for_repost() self.db_set("repost_required", self.needs_repost) + self.repost_accounting_entries() def make_gl_entries(self, gl_entries=None, from_repost=False): update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 4980c22fac1..68e9d2b0e00 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2005,10 +2005,9 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): check_gl_entries(self, pi.name, expected_gle, nowdate()) pi.items[0].expense_account = "Service - _TC" + # Ledger reposted implicitly upon 'Update After Submit' pi.save() pi.load_from_db() - self.assertTrue(pi.repost_required) - pi.repost_accounting_entries() expected_gle = [ ["Creditors - _TC", 0.0, 1000, nowdate()], diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index babcb417f23..7694c23236b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -724,6 +724,7 @@ class SalesInvoice(SellingController): if self.needs_repost: self.validate_for_repost() self.db_set("repost_required", self.needs_repost) + self.repost_accounting_entries() def set_paid_amount(self): paid_amount = 0.0 diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index b0d62339be0..ee4e82b4dfa 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2940,13 +2940,9 @@ class TestSalesInvoice(FrappeTestCase): si.items[0].income_account = "Service - _TC" si.additional_discount_account = "_Test Account Sales - _TC" si.taxes[0].account_head = "TDS Payable - _TC" + # Ledger reposted implicitly upon 'Update After Submit' si.save() - si.load_from_db() - self.assertTrue(si.repost_required) - - si.repost_accounting_entries() - expected_gle = [ ["_Test Account Sales - _TC", 22.0, 0.0, nowdate()], ["Debtors - _TC", 88, 0.0, nowdate()], diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py index aa78f4b6204..e89a177a867 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.py +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py @@ -123,19 +123,15 @@ def get_provisional_profit_loss( for period in period_list: key = period if consolidated else period.key total_assets = flt(asset[0].get(key)) + effective_liability = 0.00 - if liability or equity: - effective_liability = 0.0 - if liability: - effective_liability += flt(liability[0].get(key)) - if equity: - effective_liability += flt(equity[0].get(key)) + if liability: + effective_liability += flt(liability[0].get(key)) + if equity: + effective_liability += flt(equity[0].get(key)) - provisional_profit_loss[key] = total_assets - effective_liability - else: - provisional_profit_loss[key] = total_assets - - total_row[key] = provisional_profit_loss[key] + provisional_profit_loss[key] = total_assets - effective_liability + total_row[key] = provisional_profit_loss[key] + effective_liability if provisional_profit_loss[key]: has_value = True diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index fe2746660eb..a9039a9cada 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -713,7 +713,8 @@ class GrossProfitGenerator: def get_average_buying_rate(self, row, item_code): args = row - if item_code not in self.average_buying_rate: + key = (item_code, row.warehouse) + if key not in self.average_buying_rate: args.update( { "voucher_type": row.parenttype, @@ -727,9 +728,9 @@ class GrossProfitGenerator: args.update({"serial_and_batch_bundle": row.serial_and_batch_bundle}) average_buying_rate = get_incoming_rate(args) - self.average_buying_rate[item_code] = flt(average_buying_rate) + self.average_buying_rate[key] = flt(average_buying_rate) - return self.average_buying_rate[item_code] + return self.average_buying_rate[key] def get_last_purchase_rate(self, item_code, row): purchase_invoice = frappe.qb.DocType("Purchase Invoice") diff --git a/erpnext/accounts/report/gross_profit/test_gross_profit.py b/erpnext/accounts/report/gross_profit/test_gross_profit.py index 741ea46a516..83de93891fe 100644 --- a/erpnext/accounts/report/gross_profit/test_gross_profit.py +++ b/erpnext/accounts/report/gross_profit/test_gross_profit.py @@ -558,3 +558,50 @@ class TestGrossProfit(FrappeTestCase): } gp_entry = [x for x in data if x.parent_invoice == sinv.name] self.assertDictContainsSubset(expected_entry, gp_entry[0]) + + def test_valuation_rate_without_previous_sle(self): + """ + Test Valuation rate calculation when stock ledger is empty and invoices are against different warehouses + """ + stock_settings = frappe.get_doc("Stock Settings") + stock_settings.valuation_method = "FIFO" + stock_settings.save() + + item = create_item( + item_code="_Test Wirebound Notebook", + is_stock_item=1, + ) + item.allow_negative_stock = True + item.save() + self.item = item.item_code + + item.reload() + item.valuation_rate = 1900 + item.save() + sinv1 = self.create_sales_invoice(qty=1, rate=2000, posting_date=nowdate(), do_not_submit=True) + sinv1.update_stock = 1 + sinv1.set_warehouse = self.warehouse + sinv1.items[0].warehouse = self.warehouse + sinv1.save().submit() + + item.reload() + item.valuation_rate = 1800 + item.save() + sinv2 = self.create_sales_invoice(qty=1, rate=2000, posting_date=nowdate(), do_not_submit=True) + sinv2.update_stock = 1 + sinv2.set_warehouse = self.finished_warehouse + sinv2.items[0].warehouse = self.finished_warehouse + sinv2.save().submit() + + filters = frappe._dict( + company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice" + ) + columns, data = execute(filters=filters) + + item_from_sinv1 = [x for x in data if x.parent_invoice == sinv1.name] + self.assertEqual(len(item_from_sinv1), 1) + self.assertEqual(1900, item_from_sinv1[0].valuation_rate) + + item_from_sinv2 = [x for x in data if x.parent_invoice == sinv2.name] + self.assertEqual(len(item_from_sinv2), 1) + self.assertEqual(1800, item_from_sinv2[0].valuation_rate) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 5ed62cb132f..c05adf6bb86 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -187,7 +187,7 @@ frappe.ui.form.on("Asset", { if (frm.doc.docstatus == 0) { frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); - if (frm.doc.is_composite_asset && !frm.doc.capitalized_in) { + if (frm.doc.is_composite_asset) { $(".primary-action").prop("hidden", true); $(".form-message").text("Capitalize this asset to confirm"); @@ -511,6 +511,8 @@ frappe.ui.form.on("Asset", { frappe.call({ args: { asset: frm.doc.name, + asset_name: frm.doc.asset_name, + item_code: frm.doc.item_code, }, method: "erpnext.assets.doctype.asset.asset.create_asset_capitalization", callback: function (r) { diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 99a430cbb40..152c40c00b3 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -75,8 +75,7 @@ "purchase_amount", "default_finance_book", "depr_entry_posting_status", - "amended_from", - "capitalized_in" + "amended_from" ], "fields": [ { @@ -222,7 +221,7 @@ "read_only": 1 }, { - "depends_on": "eval:!(doc.is_composite_asset && !doc.capitalized_in)", + "depends_on": "eval:!doc.is_composite_asset", "fieldname": "gross_purchase_amount", "fieldtype": "Currency", "label": "Gross Purchase Amount", @@ -508,14 +507,6 @@ "fieldtype": "Check", "label": "Is Composite Asset" }, - { - "fieldname": "capitalized_in", - "fieldtype": "Link", - "hidden": 1, - "label": "Capitalized In", - "options": "Asset Capitalization", - "read_only": 1 - }, { "depends_on": "eval:doc.docstatus > 0", "fieldname": "total_asset_cost", @@ -589,7 +580,7 @@ "link_fieldname": "target_asset" } ], - "modified": "2024-05-21 13:46:21.066483", + "modified": "2024-07-07 22:27:14.733839", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 8641bb33fad..e69fb728520 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -60,7 +60,6 @@ class Asset(AccountsController): available_for_use_date: DF.Date | None booked_fixed_asset: DF.Check calculate_depreciation: DF.Check - capitalized_in: DF.Link | None company: DF.Link comprehensive_insurance: DF.Data | None cost_center: DF.Link | None @@ -162,7 +161,7 @@ class Asset(AccountsController): def on_cancel(self): self.validate_cancellation() self.cancel_movement_entries() - self.cancel_capitalization() + self.reload() self.delete_depreciation_entries() cancel_asset_depr_schedules(self) self.set_status() @@ -524,16 +523,6 @@ class Asset(AccountsController): movement = frappe.get_doc("Asset Movement", movement.get("name")) movement.cancel() - def cancel_capitalization(self): - asset_capitalization = frappe.db.get_value( - "Asset Capitalization", - {"target_asset": self.name, "docstatus": 1, "entry_type": "Capitalization"}, - ) - - if asset_capitalization: - asset_capitalization = frappe.get_doc("Asset Capitalization", asset_capitalization) - asset_capitalization.cancel() - def delete_depreciation_entries(self): if self.calculate_depreciation: for row in self.get("finance_books"): @@ -872,10 +861,15 @@ def create_asset_repair(asset, asset_name): @frappe.whitelist() -def create_asset_capitalization(asset): +def create_asset_capitalization(asset, asset_name, item_code): asset_capitalization = frappe.new_doc("Asset Capitalization") asset_capitalization.update( - {"target_asset": asset, "capitalization_method": "Choose a WIP composite asset"} + { + "target_asset": asset, + "capitalization_method": "Choose a WIP composite asset", + "target_asset_name": asset_name, + "target_item_code": item_code, + } ) return asset_capitalization diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 573dd92c585..4c22f7ddd7a 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -138,22 +138,10 @@ class AssetCapitalization(StockController): "Asset", "Asset Movement", ) - self.cancel_target_asset() self.update_stock_ledger() self.make_gl_entries() self.restore_consumed_asset_items() - def on_trash(self): - frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None) - super().on_trash() - - def cancel_target_asset(self): - if self.entry_type == "Capitalization" and self.target_asset: - asset_doc = frappe.get_doc("Asset", self.target_asset) - asset_doc.db_set("capitalized_in", None) - if asset_doc.docstatus == 1: - asset_doc.cancel() - def set_title(self): self.title = self.target_asset_name or self.target_item_name or self.target_item_code @@ -329,8 +317,12 @@ class AssetCapitalization(StockController): if not self.target_is_fixed_asset and not self.get("asset_items"): frappe.throw(_("Consumed Asset Items is mandatory for Decapitalization")) - if not self.get("stock_items") and not self.get("asset_items"): - frappe.throw(_("Consumed Stock Items or Consumed Asset Items is mandatory for Capitalization")) + if not (self.get("stock_items") or self.get("asset_items") or self.get("service_items")): + frappe.throw( + _( + "Consumed Stock Items, Consumed Asset Items or Consumed Service Items is mandatory for Capitalization" + ) + ) def validate_item(self, item): from erpnext.stock.doctype.item.item import validate_end_of_life @@ -617,7 +609,6 @@ class AssetCapitalization(StockController): asset_doc.purchase_date = self.posting_date asset_doc.gross_purchase_amount = total_target_asset_value asset_doc.purchase_amount = total_target_asset_value - asset_doc.capitalized_in = self.name asset_doc.flags.ignore_validate = True asset_doc.flags.asset_created_via_asset_capitalization = True asset_doc.insert() @@ -653,7 +644,6 @@ class AssetCapitalization(StockController): asset_doc = frappe.get_doc("Asset", self.target_asset) asset_doc.gross_purchase_amount = total_target_asset_value asset_doc.purchase_amount = total_target_asset_value - asset_doc.capitalized_in = self.name asset_doc.flags.ignore_validate = True asset_doc.save() diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index 31723ef3be3..5508bdcbef2 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -386,6 +386,56 @@ class TestAssetCapitalization(unittest.TestCase): self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) self.assertFalse(get_actual_sle_dict(asset_capitalization.name)) + def test_capitalize_only_service_item(self): + company = "_Test Company" + # Variables + + service_rate = 500 + service_qty = 2 + service_amount = 1000 + + total_amount = 1000 + + wip_composite_asset = create_asset( + asset_name="Asset Capitalization WIP Composite Asset", + is_composite_asset=1, + warehouse="Stores - TCP1", + company=company, + ) + + # Create and submit Asset Captitalization + asset_capitalization = create_asset_capitalization( + entry_type="Capitalization", + capitalization_method="Choose a WIP composite asset", + target_asset=wip_composite_asset.name, + target_asset_location="Test Location", + service_qty=service_qty, + service_rate=service_rate, + service_expense_account="Expenses Included In Asset Valuation - _TC", + company=company, + submit=1, + ) + + self.assertEqual(asset_capitalization.service_items[0].amount, service_amount) + self.assertEqual(asset_capitalization.service_items_total, service_amount) + + target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset) + self.assertEqual(target_asset.gross_purchase_amount, total_amount) + self.assertEqual(target_asset.purchase_amount, total_amount) + + expected_gle = { + "_Test Fixed Asset - _TC": 1000.0, + "Expenses Included In Asset Valuation - _TC": -1000.0, + } + + actual_gle = get_actual_gle_dict(asset_capitalization.name) + self.assertEqual(actual_gle, expected_gle) + + # Cancel Asset Capitalization and make test entries and status are reversed + asset_capitalization.cancel() + self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) + self.assertFalse(get_actual_sle_dict(asset_capitalization.name)) + def create_asset_capitalization_data(): create_item("Capitalization Target Stock Item", is_stock_item=1, is_fixed_asset=0, is_purchase_item=0) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 30a5f38400e..958ac266e61 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -541,7 +541,9 @@ class BuyingController(SubcontractingController): "actual_qty": flt(pr_qty), "serial_and_batch_bundle": ( d.serial_and_batch_bundle - if not self.is_internal_transfer() or self.is_return + if not self.is_internal_transfer() + or self.is_return + or (self.is_internal_transfer() and self.docstatus == 2) else self.get_package_for_target_warehouse( d, type_of_transaction=type_of_transaction ) @@ -580,6 +582,14 @@ class BuyingController(SubcontractingController): (not cint(self.is_return) and self.docstatus == 2) or (cint(self.is_return) and self.docstatus == 1) ): + serial_and_batch_bundle = None + if self.is_internal_transfer() and self.docstatus == 2: + serial_and_batch_bundle = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_detail_no": d.name, "warehouse": d.warehouse}, + "serial_and_batch_bundle", + ) + from_warehouse_sle = self.get_sl_entries( d, { @@ -589,7 +599,7 @@ class BuyingController(SubcontractingController): "serial_and_batch_bundle": ( self.get_package_for_target_warehouse(d, d.from_warehouse, "Inward") if self.is_internal_transfer() and self.is_return - else None + else serial_and_batch_bundle ), }, ) diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index 7a1db6d2653..cc6870f892a 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -41,7 +41,8 @@ def get_variant(template, args=None, variant=None, manufacturer=None, manufactur if isinstance(args, str): args = json.loads(args) - if not args: + attribute_args = {k: v for k, v in args.items() if k != "use_template_image"} + if not attribute_args: frappe.throw(_("Please specify at least one attribute in the Attributes table")) return find_variant(template, args, variant) @@ -197,7 +198,8 @@ def find_variant(template, args, variant_item_code=None): @frappe.whitelist() -def create_variant(item, args): +def create_variant(item, args, use_template_image=False): + use_template_image = frappe.parse_json(use_template_image) if isinstance(args, str): args = json.loads(args) @@ -211,13 +213,18 @@ def create_variant(item, args): variant.set("attributes", variant_attributes) copy_attributes_to_variant(template, variant) + + if use_template_image and template.image: + variant.image = template.image + make_variant_item_code(template.item_code, template.item_name, variant) return variant @frappe.whitelist() -def enqueue_multiple_variant_creation(item, args): +def enqueue_multiple_variant_creation(item, args, use_template_image=False): + use_template_image = frappe.parse_json(use_template_image) # There can be innumerable attribute combinations, enqueue if isinstance(args, str): variants = json.loads(args) @@ -228,27 +235,31 @@ def enqueue_multiple_variant_creation(item, args): frappe.throw(_("Please do not create more than 500 items at a time")) return if total_variants < 10: - return create_multiple_variants(item, args) + return create_multiple_variants(item, args, use_template_image) else: frappe.enqueue( "erpnext.controllers.item_variant.create_multiple_variants", item=item, args=args, + use_template_image=use_template_image, now=frappe.flags.in_test, ) return "queued" -def create_multiple_variants(item, args): +def create_multiple_variants(item, args, use_template_image=False): count = 0 if isinstance(args, str): args = json.loads(args) + template_item = frappe.get_doc("Item", item) args_set = generate_keyed_value_combinations(args) for attribute_values in args_set: if not get_variant(item, args=attribute_values): variant = create_variant(item, attribute_values) + if use_template_image and template_item.image: + variant.image = template_item.image variant.save() count += 1 diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 71e40d509b2..7a85d2230f1 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -538,7 +538,9 @@ class SellingController(StockController): self.make_sl_entries(sl_entries) def get_sle_for_source_warehouse(self, item_row): - serial_and_batch_bundle = item_row.serial_and_batch_bundle + serial_and_batch_bundle = ( + item_row.serial_and_batch_bundle if not self.is_internal_transfer() else None + ) if serial_and_batch_bundle and self.is_internal_transfer() and self.is_return: if self.docstatus == 1: serial_and_batch_bundle = self.make_package_for_transfer( diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index d31ee258b27..a6727ef8826 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -908,6 +908,7 @@ class SubcontractingController(StockController): item, { "item_code": item.rm_item_code, + "incoming_rate": item.rate if self.is_return else 0, "warehouse": self.supplier_warehouse, "actual_qty": -1 * flt(item.consumed_qty, item.precision("consumed_qty")), "dependant_sle_voucher_detail_no": item.reference_name, diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b661e6f99a4..158d68ff275 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -362,7 +362,6 @@ erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2 erpnext.patches.v14_0.set_maintain_stock_for_bom_item erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency -erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset erpnext.patches.v15_0.rename_purchase_receipt_amount_to_purchase_amount erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1 erpnext.patches.v15_0.rename_number_of_depreciations_booked_to_opening_booked_depreciations diff --git a/erpnext/patches/v15_0/remove_cancelled_asset_capitalization_from_asset.py b/erpnext/patches/v15_0/remove_cancelled_asset_capitalization_from_asset.py deleted file mode 100644 index cb39a9280e4..00000000000 --- a/erpnext/patches/v15_0/remove_cancelled_asset_capitalization_from_asset.py +++ /dev/null @@ -1,11 +0,0 @@ -import frappe - - -def execute(): - cancelled_asset_capitalizations = frappe.get_all( - "Asset Capitalization", - filters={"docstatus": 2}, - fields=["name", "target_asset"], - ) - for asset_capitalization in cancelled_asset_capitalizations: - frappe.db.set_value("Asset", asset_capitalization.target_asset, "capitalized_in", None) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index a00b08ab097..38a089fdb03 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -603,6 +603,17 @@ class SalesOrder(SellingController): if total_picked_qty and total_qty: per_picked = total_picked_qty / total_qty * 100 + pick_percentage = frappe.db.get_single_value("Stock Settings", "over_picking_allowance") + if pick_percentage: + total_qty += flt(total_qty) * (pick_percentage / 100) + + if total_picked_qty > total_qty: + frappe.throw( + _( + "Total Picked Quantity {0} is more than ordered qty {1}. You can set the Over Picking Allowance in Stock Settings." + ).format(total_picked_qty, total_qty) + ) + self.db_set("per_picked", flt(per_picked), update_modified=False) def set_indicator(self): diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 5f44548a873..f27db868a07 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1918,6 +1918,93 @@ class TestDeliveryNote(FrappeTestCase): returned_serial_nos = get_serial_nos_from_bundle(dn_return.items[0].serial_and_batch_bundle) self.assertEqual(serial_nos, returned_serial_nos) + def test_same_posting_date_and_posting_time(self): + item_code = make_item( + "Test Same Posting Datetime Item", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "SS-ART11-TESTBATCH.#####", + "is_stock_item": 1, + }, + ).name + + se = make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=100, + basic_rate=50, + posting_date=add_days(nowdate(), -1), + ) + + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + + posting_date = today() + posting_time = nowtime() + dn1 = create_delivery_note( + posting_date=posting_date, + posting_time=posting_time, + item_code=item_code, + rate=300, + qty=25, + batch_no=batch_no, + use_serial_batch_fields=1, + ) + + dn2 = create_delivery_note( + posting_date=posting_date, + posting_time=posting_time, + item_code=item_code, + rate=300, + qty=25, + batch_no=batch_no, + use_serial_batch_fields=1, + ) + + dn3 = create_delivery_note( + posting_date=posting_date, + posting_time=posting_time, + item_code=item_code, + rate=300, + qty=25, + batch_no=batch_no, + use_serial_batch_fields=1, + ) + + dn4 = create_delivery_note( + posting_date=posting_date, + posting_time=posting_time, + item_code=item_code, + rate=300, + qty=25, + batch_no=batch_no, + use_serial_batch_fields=1, + ) + + for dn in [dn1, dn2, dn3, dn4]: + sles = frappe.get_all( + "Stock Ledger Entry", + fields=["stock_value_difference", "actual_qty"], + filters={"is_cancelled": 0, "voucher_no": dn.name, "docstatus": 1}, + ) + + for sle in sles: + self.assertEqual(sle.actual_qty, 25.0 * -1) + self.assertEqual(sle.stock_value_difference, 25.0 * 50 * -1) + + dn5 = create_delivery_note( + posting_date=posting_date, + posting_time=posting_time, + item_code=item_code, + rate=300, + qty=25, + batch_no=batch_no, + use_serial_batch_fields=1, + do_not_submit=True, + ) + + self.assertRaises(frappe.ValidationError, dn5.submit) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index d92a998a471..81eeb914af0 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -587,6 +587,14 @@ $.extend(erpnext.item, { me.multiple_variant_dialog = new frappe.ui.Dialog({ title: __("Select Attribute Values"), fields: [ + frm.doc.image + ? { + fieldtype: "Check", + label: __("Create a variant with the template image."), + fieldname: "use_template_image", + default: 0, + } + : null, { fieldtype: "HTML", fieldname: "help", @@ -594,11 +602,14 @@ $.extend(erpnext.item, { ${__("Select at least one value from each of the attributes.")} `, }, - ].concat(fields), + ] + .concat(fields) + .filter(Boolean), }); me.multiple_variant_dialog.set_primary_action(__("Create Variants"), () => { let selected_attributes = get_selected_attributes(); + let use_template_image = me.multiple_variant_dialog.get_value("use_template_image"); me.multiple_variant_dialog.hide(); frappe.call({ @@ -606,6 +617,7 @@ $.extend(erpnext.item, { args: { item: frm.doc.name, args: selected_attributes, + use_template_image: use_template_image, }, callback: function (r) { if (r.message === "queued") { @@ -720,6 +732,15 @@ $.extend(erpnext.item, { }); } + if (frm.doc.image) { + fields.push({ + fieldtype: "Check", + label: __("Create a variant with the template image."), + fieldname: "use_template_image", + default: 0, + }); + } + var d = new frappe.ui.Dialog({ title: __("Create Variant"), fields: fields, @@ -761,6 +782,7 @@ $.extend(erpnext.item, { args: { item: frm.doc.name, args: d.get_values(), + use_template_image: args.use_template_image, }, callback: function (r) { var doclist = frappe.model.sync(r.message); diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index b1c03bf8453..9fc304e4e21 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -1174,3 +1174,34 @@ class TestPickList(FrappeTestCase): row.qty = row.qty + 10 self.assertRaises(frappe.ValidationError, pl.save) + + def test_over_allowance_picking(self): + warehouse = "_Test Warehouse - _TC" + item = make_item( + "Test Over Allowance Picking Item", + properties={ + "is_stock_item": 1, + }, + ).name + + make_stock_entry(item=item, to_warehouse=warehouse, qty=100) + + so = make_sales_order(item_code=item, qty=10, rate=100) + + pl_doc = create_pick_list(so.name) + pl_doc.save() + self.assertEqual(pl_doc.locations[0].qty, 10) + + pl_doc.locations[0].qty = 15 + pl_doc.locations[0].stock_qty = 15 + pl_doc.save() + + self.assertEqual(pl_doc.locations[0].qty, 15) + self.assertRaises(frappe.ValidationError, pl_doc.submit) + + frappe.db.set_single_value("Stock Settings", "over_picking_allowance", 50) + + pl_doc.reload() + pl_doc.submit() + + frappe.db.set_single_value("Stock Settings", "over_picking_allowance", 0) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 869d3ed3cce..736ced72f33 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -3350,6 +3350,122 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(pr.grand_total, 0.0) self.assertEqual(pr.status, "Completed") + def test_internal_transfer_for_batch_items_with_cancel(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0) + + prepare_data_for_internal_transfer() + + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + + batch_item_doc = make_item( + "_Test Batch Item For Stock Transfer Cancel Case", + {"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "USBF-BT-CANBIFST-.####"}, + ) + + serial_item_doc = make_item( + "_Test Serial No Item For Stock Transfer Cancel Case", + {"has_serial_no": 1, "serial_no_series": "USBF-BT-CANBIFST-.####"}, + ) + + inward_entry = make_purchase_receipt( + item_code=batch_item_doc.name, + qty=10, + rate=150, + warehouse="Stores - TCP1", + company="_Test Company with perpetual inventory", + use_serial_batch_fields=0, + do_not_submit=1, + ) + + inward_entry.append( + "items", + { + "item_code": serial_item_doc.name, + "qty": 15, + "rate": 250, + "item_name": serial_item_doc.item_name, + "conversion_factor": 1.0, + "uom": serial_item_doc.stock_uom, + "stock_uom": serial_item_doc.stock_uom, + "warehouse": "Stores - TCP1", + "use_serial_batch_fields": 0, + }, + ) + + inward_entry.submit() + inward_entry.reload() + + for row in inward_entry.items: + self.assertTrue(row.serial_and_batch_bundle) + + inter_transfer_dn = create_delivery_note( + item_code=inward_entry.items[0].item_code, + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=10, + rate=500, + warehouse="Stores - TCP1", + target_warehouse="Work In Progress - TCP1", + batch_no=get_batch_from_bundle(inward_entry.items[0].serial_and_batch_bundle), + use_serial_batch_fields=0, + do_not_submit=1, + ) + + inter_transfer_dn.append( + "items", + { + "item_code": serial_item_doc.name, + "qty": 15, + "rate": 350, + "item_name": serial_item_doc.item_name, + "conversion_factor": 1.0, + "uom": serial_item_doc.stock_uom, + "stock_uom": serial_item_doc.stock_uom, + "warehouse": "Stores - TCP1", + "target_warehouse": "Work In Progress - TCP1", + "serial_no": "\n".join( + get_serial_nos_from_bundle(inward_entry.items[1].serial_and_batch_bundle) + ), + "use_serial_batch_fields": 0, + }, + ) + + inter_transfer_dn.submit() + inter_transfer_dn.reload() + for row in inter_transfer_dn.items: + if row.item_code == batch_item_doc.name: + self.assertEqual(row.rate, 150.0) + else: + self.assertEqual(row.rate, 250.0) + + self.assertTrue(row.serial_and_batch_bundle) + + inter_transfer_pr = make_inter_company_purchase_receipt(inter_transfer_dn.name) + for row in inter_transfer_pr.items: + row.from_warehouse = "Work In Progress - TCP1" + row.warehouse = "Stores - TCP1" + inter_transfer_pr.submit() + + for row in inter_transfer_pr.items: + if row.item_code == batch_item_doc.name: + self.assertEqual(row.rate, 150.0) + else: + self.assertEqual(row.rate, 250.0) + + self.assertTrue(row.serial_and_batch_bundle) + + inter_transfer_pr.cancel() + inter_transfer_dn.cancel() + + frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1) + 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/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index b8b17cb4c81..741d1fdfae9 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 @@ -300,6 +300,7 @@ class SerialandBatchBundle(Document): "batch_nos": {row.batch_no: row for row in self.entries if row.batch_no}, "voucher_type": self.voucher_type, "voucher_detail_no": self.voucher_detail_no, + "creation": self.creation, } ) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index f08ae6c286c..36f6f354bdf 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2756,7 +2756,7 @@ def make_stock_in_entry(source_name, target_doc=None): "batch_no": "batch_no", }, "postprocess": update_item, - "condition": lambda doc: flt(doc.qty) - flt(doc.transferred_qty) > 0.01, + "condition": lambda doc: flt(doc.qty) - flt(doc.transferred_qty) > 0.00001, }, }, target_doc, diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 5d9ddaccedf..431fdf681ff 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -26,6 +26,7 @@ "section_break_9", "over_delivery_receipt_allowance", "mr_qty_allowance", + "over_picking_allowance", "column_break_121", "role_allowed_to_over_deliver_receive", "allow_negative_stock", @@ -446,6 +447,12 @@ "fieldname": "do_not_use_batchwise_valuation", "fieldtype": "Check", "label": "Do Not Use Batch-wise Valuation" + }, + { + "description": "The percentage you are allowed to pick more items in the pick list than the ordered quantity.", + "fieldname": "over_picking_allowance", + "fieldtype": "Percent", + "label": "Over Picking Allowance" } ], "icon": "icon-cog", @@ -453,7 +460,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-07-04 12:45:09.811280", + "modified": "2024-07-15 17:18:23.872161", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index e786b1f67c6..fae75f49777 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -47,6 +47,7 @@ class StockSettings(Document): mr_qty_allowance: DF.Float naming_series_prefix: DF.Data | None over_delivery_receipt_allowance: DF.Float + over_picking_allowance: DF.Percent pick_serial_and_batch_based_on: DF.Literal["FIFO", "LIFO", "Expiry"] reorder_email_notify: DF.Check role_allowed_to_create_edit_back_dated_transactions: DF.Link | None 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 6ef02724f65..894a740c177 100644 --- a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py +++ b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py @@ -77,9 +77,8 @@ def get_columns(filters): }, { "label": _("Party Type"), - "fieldtype": "Link", + "fieldtype": "Data", "fieldname": "party_type", - "options": "DocType", "width": 90, }, { diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index c5346d2b0a3..2207b2e3c74 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -599,9 +599,15 @@ class BatchNoValuation(DeprecatedBatchNoValuation): timestamp_condition = "" if self.sle.posting_date and self.sle.posting_time: - timestamp_condition = CombineDatetime( - parent.posting_date, parent.posting_time - ) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time) + timestamp_condition = CombineDatetime(parent.posting_date, parent.posting_time) < CombineDatetime( + self.sle.posting_date, self.sle.posting_time + ) + + if self.sle.creation: + timestamp_condition |= ( + CombineDatetime(parent.posting_date, parent.posting_time) + == CombineDatetime(self.sle.posting_date, self.sle.posting_time) + ) & (parent.creation < self.sle.creation) query = ( frappe.qb.from_(parent) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 51d2708be24..ee5893eb826 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -200,7 +200,7 @@ def get_bin(item_code, warehouse): if not bin: bin_obj = _create_bin(item_code, warehouse) else: - bin_obj = frappe.get_doc("Bin", bin, for_update=True) + bin_obj = frappe.get_doc("Bin", bin) bin_obj.flags.ignore_permissions = True return bin_obj diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js index c4eea3fda45..c6ee3a3e0e3 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -9,6 +9,7 @@ frappe.ui.form.on("Subcontracting Order", { setup: (frm) => { frm.get_field("items").grid.cannot_add_rows = true; frm.get_field("items").grid.only_sortable(); + frm.trigger("set_queries"); frm.set_indicator_formatter("item_code", (doc) => (doc.qty <= doc.received_qty ? "green" : "orange")); @@ -93,6 +94,17 @@ frappe.ui.form.on("Subcontracting Order", { }); }, + set_queries: (frm) => { + frm.set_query("contact_person", erpnext.queries.contact_query); + frm.set_query("supplier_address", erpnext.queries.address_query); + + frm.set_query("billing_address", erpnext.queries.company_address_query); + + frm.set_query("shipping_address", () => { + return erpnext.queries.company_address_query(frm.doc); + }); + }, + onload: (frm) => { if (!frm.doc.transaction_date) { frm.set_value("transaction_date", frappe.datetime.get_today()); @@ -116,6 +128,8 @@ frappe.ui.form.on("Subcontracting Order", { }, refresh: function (frm) { + frappe.dynamic_link = { doc: frm.doc, fieldname: "supplier", doctype: "Supplier" }; + if (frm.doc.docstatus == 1 && frm.has_perm("submit")) { if (frm.doc.status == "Closed") { frm.add_custom_button( diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index b4a127702e0..83113a223c2 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -28,6 +28,8 @@ frappe.ui.form.on("Subcontracting Receipt", { }, refresh: (frm) => { + frappe.dynamic_link = { doc: frm.doc, fieldname: "supplier", doctype: "Supplier" }; + if (frm.doc.docstatus === 1) { frm.add_custom_button( __("Stock Ledger"), @@ -165,6 +167,15 @@ frappe.ui.form.on("Subcontracting Receipt", { }; }); + frm.set_query("contact_person", erpnext.queries.contact_query); + frm.set_query("supplier_address", erpnext.queries.address_query); + + frm.set_query("billing_address", erpnext.queries.company_address_query); + + frm.set_query("shipping_address", () => { + return erpnext.queries.company_address_query(frm.doc); + }); + frm.set_query("rejected_warehouse", () => { return { filters: {