diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 969027dd97c..cc72c313238 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -905,7 +905,7 @@ frappe.ui.form.on('Payment Entry', { function(d) { return flt(d.amount) })); frm.set_value("difference_amount", difference_amount - total_deductions + - frm.doc.base_total_taxes_and_charges); + flt(frm.doc.base_total_taxes_and_charges)); frm.events.hide_unhide_fields(frm); }, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index bb85fedab05..0dcd2e291a6 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1179,7 +1179,12 @@ class SalesInvoice(SellingController): if self.is_return: fixed_asset_gl_entries = get_gl_entries_on_asset_regain( - asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name") + asset, + item.base_net_amount, + item.finance_book, + self.get("doctype"), + self.get("name"), + self.get("posting_date"), ) asset.db_set("disposal_date", None) @@ -1194,7 +1199,12 @@ class SalesInvoice(SellingController): asset.reload() fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( - asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name") + asset, + item.base_net_amount, + item.finance_book, + self.get("doctype"), + self.get("name"), + self.get("posting_date"), ) asset.db_set("disposal_date", self.posting_date) diff --git a/erpnext/accounts/form_tour/sales_invoice/sales_invoice.json b/erpnext/accounts/form_tour/sales_invoice/sales_invoice.json new file mode 100644 index 00000000000..414b897d547 --- /dev/null +++ b/erpnext/accounts/form_tour/sales_invoice/sales_invoice.json @@ -0,0 +1,41 @@ +{ + "creation": "2023-05-23 09:58:17.235916", + "docstatus": 0, + "doctype": "Form Tour", + "first_document": 0, + "idx": 0, + "include_name_field": 0, + "is_standard": 1, + "modified": "2023-05-23 13:10:56.227127", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Sales Invoice", + "owner": "Administrator", + "reference_doctype": "Sales Invoice", + "save_on_complete": 1, + "steps": [ + { + "description": "Select a customer for whom this invoice is being prepared.", + "fieldname": "customer", + "fieldtype": "Link", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Customer", + "next_step_condition": "eval: doc.customer", + "position": "Right", + "title": "Select Customer" + }, + { + "child_doctype": "Sales Invoice Item", + "description": "Select item that you have sold along with quantity and rate.", + "fieldname": "items", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "parent_fieldname": "items", + "position": "Top", + "title": "Select Item" + } + ], + "title": "Sales Invoice" +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index fe1fd98aa09..8754bb7125a 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -343,7 +343,7 @@ class Asset(AccountsController): # if asset is being sold if date_of_disposal: - from_date = self.get_from_date(finance_book.finance_book) + from_date = self.get_from_date_for_disposal(finance_book) depreciation_amount, days, months = self.get_pro_rata_amt( finance_book, depreciation_amount, @@ -425,7 +425,7 @@ class Asset(AccountsController): depreciation_amount += value_after_depreciation - finance_book.expected_value_after_useful_life skip_row = True - if depreciation_amount > 0: + if flt(depreciation_amount, self.precision("gross_purchase_amount")) > 0: self._add_depreciation_row( schedule_date, depreciation_amount, @@ -500,16 +500,19 @@ class Asset(AccountsController): return start - def get_from_date(self, finance_book): + def get_from_date_for_disposal(self, finance_book): if not self.get("schedules"): - return self.available_for_use_date + return add_months( + getdate(self.available_for_use_date), + (self.number_of_depreciations_booked * finance_book.frequency_of_depreciation), + ) if len(self.finance_books) == 1: return self.schedules[-1].schedule_date from_date = "" for schedule in self.get("schedules"): - if schedule.finance_book == finance_book: + if schedule.finance_book == finance_book.finance_book: from_date = schedule.schedule_date if from_date: @@ -1287,9 +1290,11 @@ def get_straight_line_or_manual_depr_amount(asset, row): ) # if the Depreciation Schedule is being prepared for the first time else: - return (flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)) / flt( - row.total_number_of_depreciations - ) + return ( + flt(asset.gross_purchase_amount) + - flt(asset.opening_accumulated_depreciation) + - flt(row.expected_value_after_useful_life) + ) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) def get_wdv_or_dd_depr_amount( diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index a1d29f88cb1..163752b65eb 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -272,7 +272,7 @@ def scrap_asset(asset_name): je.company = asset.company je.remark = "Scrap Entry for asset {0}".format(asset_name) - for entry in get_gl_entries_on_asset_disposal(asset): + for entry in get_gl_entries_on_asset_disposal(asset, date): entry.update({"reference_type": "Asset", "reference_name": asset_name}) je.append("accounts", entry) @@ -395,8 +395,11 @@ def disposal_happens_in_the_future(posting_date_of_disposal): def get_gl_entries_on_asset_regain( - asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None + asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None, date=None ): + if not date: + date = getdate() + ( fixed_asset_account, asset, @@ -414,7 +417,7 @@ def get_gl_entries_on_asset_regain( "debit_in_account_currency": asset.gross_purchase_amount, "debit": asset.gross_purchase_amount, "cost_center": depreciation_cost_center, - "posting_date": getdate(), + "posting_date": date, }, item=asset, ), @@ -424,7 +427,7 @@ def get_gl_entries_on_asset_regain( "credit_in_account_currency": accumulated_depr_amount, "credit": accumulated_depr_amount, "cost_center": depreciation_cost_center, - "posting_date": getdate(), + "posting_date": date, }, item=asset, ), @@ -433,7 +436,7 @@ def get_gl_entries_on_asset_regain( profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount)) if profit_amount: get_profit_gl_entries( - asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center + asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date ) if voucher_type and voucher_no: @@ -445,8 +448,11 @@ def get_gl_entries_on_asset_regain( def get_gl_entries_on_asset_disposal( - asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None + asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None, date=None ): + if not date: + date = getdate() + ( fixed_asset_account, asset, @@ -464,7 +470,7 @@ def get_gl_entries_on_asset_disposal( "credit_in_account_currency": asset.gross_purchase_amount, "credit": asset.gross_purchase_amount, "cost_center": depreciation_cost_center, - "posting_date": getdate(), + "posting_date": date, }, item=asset, ), @@ -474,7 +480,7 @@ def get_gl_entries_on_asset_disposal( "debit_in_account_currency": accumulated_depr_amount, "debit": accumulated_depr_amount, "cost_center": depreciation_cost_center, - "posting_date": getdate(), + "posting_date": date, }, item=asset, ), @@ -483,7 +489,7 @@ def get_gl_entries_on_asset_disposal( profit_amount = flt(selling_amount) - flt(value_after_depreciation) if profit_amount: get_profit_gl_entries( - asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center + asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date ) if voucher_type and voucher_no: @@ -517,8 +523,12 @@ def get_asset_details(asset, finance_book=None): def get_profit_gl_entries( - asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center + asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date=None ): + + if not date: + date = getdate() + debit_or_credit = "debit" if profit_amount < 0 else "credit" gl_entries.append( asset.get_gl_dict( @@ -527,7 +537,7 @@ def get_profit_gl_entries( "cost_center": depreciation_cost_center, debit_or_credit: abs(profit_amount), debit_or_credit + "_in_account_currency": abs(profit_amount), - "posting_date": getdate(), + "posting_date": date, }, item=asset, ) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 8a1df6f71ce..1c0972f2374 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -327,6 +327,79 @@ class TestAsset(AssetSetup): si.cancel() self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") + def test_gle_made_by_asset_sale_for_existing_asset(self): + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + + asset = create_asset( + calculate_depreciation=1, + available_for_use_date="2020-04-01", + purchase_date="2020-04-01", + expected_value_after_useful_life=0, + total_number_of_depreciations=5, + number_of_depreciations_booked=2, + frequency_of_depreciation=12, + depreciation_start_date="2023-03-31", + opening_accumulated_depreciation=24000, + gross_purchase_amount=60000, + submit=1, + ) + + expected_depr_values = [ + ["2023-03-31", 12000, 36000], + ["2024-03-31", 12000, 48000], + ["2025-03-31", 12000, 60000], + ] + + for i, schedule in enumerate(asset.schedules): + self.assertEqual(getdate(expected_depr_values[i][0]), schedule.schedule_date) + self.assertEqual(expected_depr_values[i][1], schedule.depreciation_amount) + self.assertEqual(expected_depr_values[i][2], schedule.accumulated_depreciation_amount) + + post_depreciation_entries(date="2023-03-31") + + si = create_sales_invoice( + item_code="Macbook Pro", asset=asset.name, qty=1, rate=40000, posting_date=getdate("2023-05-23") + ) + asset.load_from_db() + + self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") + + expected_values = [["2023-03-31", 12000, 36000], ["2023-05-23", 1742.47, 37742.47]] + + for i, schedule in enumerate(asset.schedules): + self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) + self.assertEqual(expected_values[i][1], schedule.depreciation_amount) + self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) + self.assertTrue(schedule.journal_entry) + + expected_gle = ( + ( + "_Test Accumulated Depreciations - _TC", + 37742.47, + 0.0, + ), + ( + "_Test Fixed Asset - _TC", + 0.0, + 60000.0, + ), + ( + "_Test Gain/Loss on Asset Disposal - _TC", + 0.0, + 17742.47, + ), + ("Debtors - _TC", 40000.0, 0.0), + ) + + gle = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no = %s + order by account""", + si.name, + ) + + self.assertSequenceEqual(gle, expected_gle) + def test_asset_with_maintenance_required_status_after_sale(self): asset = create_asset( calculate_depreciation=1, @@ -649,7 +722,7 @@ class TestDepreciationMethods(AssetSetup): ) self.assertEqual(asset.status, "Draft") - expected_schedules = [["2032-12-31", 30000.0, 77095.89], ["2033-06-06", 12904.11, 90000.0]] + expected_schedules = [["2032-12-31", 42904.11, 90000.0]] schedules = [ [cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] for d in asset.get("schedules") diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 90b4d7e5834..f648823b5bf 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -436,6 +436,7 @@ class AssetCapitalization(StockController): item.get("finance_book") or self.get("finance_book"), self.get("doctype"), self.get("name"), + self.get("posting_date"), ) asset.db_set("disposal_date", self.posting_date) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index c3fa894e895..1e9c4dc8478 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -689,7 +689,6 @@ class SubcontractingController(StockController): "actual_qty": flt(item.rejected_qty) * flt(item.conversion_factor), "serial_no": cstr(item.rejected_serial_no).strip(), "incoming_rate": 0.0, - "recalculate_rate": 1, }, ) ) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 85061113cea..316e586b7a2 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -439,7 +439,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2022-11-09 15:02:44.490731", + "modified": "2023-05-23 09:56:43.826602", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 4a4046e47aa..a7d0b29f83c 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -730,7 +730,7 @@ class JobCard(Document): self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0] if self.docstatus < 2: - if self.for_quantity <= self.transferred_qty: + if flt(self.for_quantity) <= flt(self.transferred_qty): self.status = "Material Transferred" if self.time_logs: diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index fdaa4a2a1d4..232f1cb2c44 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -35,8 +35,12 @@ "section_break_25", "prod_plan_references", "section_break_24", - "get_sub_assembly_items", "combine_sub_items", + "section_break_ucc4", + "skip_available_sub_assembly_item", + "column_break_igxl", + "get_sub_assembly_items", + "section_break_g4ip", "sub_assembly_items", "download_materials_request_plan_section_section", "download_materials_required", @@ -351,12 +355,12 @@ { "fieldname": "section_break_24", "fieldtype": "Section Break", - "hide_border": 1 + "hide_border": 1, + "label": "Sub Assembly Items" }, { "fieldname": "sub_assembly_items", "fieldtype": "Table", - "label": "Sub Assembly Items", "no_copy": 1, "options": "Production Plan Sub Assembly Item" }, @@ -392,13 +396,33 @@ "fieldname": "download_materials_request_plan_section_section", "fieldtype": "Section Break", "label": "Download Materials Request Plan Section" + }, + { + "default": "0", + "description": "System consider the projected quantity to check available or will be available sub-assembly items ", + "fieldname": "skip_available_sub_assembly_item", + "fieldtype": "Check", + "label": "Skip Available Sub Assembly Items" + }, + { + "fieldname": "section_break_ucc4", + "fieldtype": "Column Break", + "hide_border": 1 + }, + { + "fieldname": "section_break_g4ip", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_igxl", + "fieldtype": "Column Break" } ], "icon": "fa fa-calendar", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-03-31 10:30:48.118932", + "modified": "2023-05-22 23:36:31.770517", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index f9e68b916f8..e40539acf34 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -718,7 +718,9 @@ class ProductionPlan(Document): frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx)) bom_data = [] - get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty) + + warehouse = row.warehouse if self.skip_available_sub_assembly_item else None + get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty, self.company, warehouse=warehouse) self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type) sub_assembly_items_store.extend(bom_data) @@ -894,7 +896,9 @@ def download_raw_materials(doc, warehouses=None): build_csv_response(item_list, doc.name) -def get_exploded_items(item_details, company, bom_no, include_non_stock_items, planned_qty=1): +def get_exploded_items( + item_details, company, bom_no, include_non_stock_items, planned_qty=1, doc=None +): bei = frappe.qb.DocType("BOM Explosion Item") bom = frappe.qb.DocType("BOM") item = frappe.qb.DocType("Item") @@ -1271,6 +1275,12 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d include_safety_stock = doc.get("include_safety_stock") so_item_details = frappe._dict() + + sub_assembly_items = {} + if doc.get("skip_available_sub_assembly_item"): + for d in doc.get("sub_assembly_items"): + sub_assembly_items.setdefault((d.get("production_item"), d.get("bom_no")), d.get("qty")) + for data in po_items: if not data.get("include_exploded_items") and doc.get("sub_assembly_items"): data["include_exploded_items"] = 1 @@ -1296,10 +1306,24 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get("idx"))) if bom_no: - if data.get("include_exploded_items") and include_subcontracted_items: + if ( + data.get("include_exploded_items") + and doc.get("sub_assembly_items") + and doc.get("skip_available_sub_assembly_item") + ): + item_details = get_raw_materials_of_sub_assembly_items( + item_details, + company, + bom_no, + include_non_stock_items, + sub_assembly_items, + planned_qty=planned_qty, + ) + + elif data.get("include_exploded_items") and include_subcontracted_items: # fetch exploded items from BOM item_details = get_exploded_items( - item_details, company, bom_no, include_non_stock_items, planned_qty=planned_qty + item_details, company, bom_no, include_non_stock_items, planned_qty=planned_qty, doc=doc ) else: item_details = get_subitems( @@ -1456,12 +1480,22 @@ def get_item_data(item_code): } -def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): +def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, company, warehouse=None, indent=0): data = get_bom_children(parent=bom_no) for d in data: if d.expandable: parent_item_code = frappe.get_cached_value("BOM", bom_no, "item") stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) + + if warehouse: + bin_dict = get_bin_details(d, company, for_warehouse=warehouse) + + if bin_dict and bin_dict[0].projected_qty > 0: + if bin_dict[0].projected_qty > stock_qty: + continue + else: + stock_qty = stock_qty - bin_dict[0].projected_qty + bom_data.append( frappe._dict( { @@ -1481,7 +1515,7 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): ) if d.value: - get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent + 1) + get_sub_assembly_items(d.value, bom_data, stock_qty, company, warehouse, indent=indent + 1) def set_default_warehouses(row, default_warehouses): @@ -1519,3 +1553,68 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): ) return reserved_qty_for_production_plan - reserved_qty_for_production + + +def get_raw_materials_of_sub_assembly_items( + item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1 +): + + bei = frappe.qb.DocType("BOM Item") + bom = frappe.qb.DocType("BOM") + item = frappe.qb.DocType("Item") + item_default = frappe.qb.DocType("Item Default") + item_uom = frappe.qb.DocType("UOM Conversion Detail") + + items = ( + frappe.qb.from_(bei) + .join(bom) + .on(bom.name == bei.parent) + .join(item) + .on(item.name == bei.item_code) + .left_join(item_default) + .on((item_default.parent == item.name) & (item_default.company == company)) + .left_join(item_uom) + .on((item.name == item_uom.parent) & (item_uom.uom == item.purchase_uom)) + .select( + (IfNull(Sum(bei.stock_qty / IfNull(bom.quantity, 1)), 0) * planned_qty).as_("qty"), + item.item_name, + item.name.as_("item_code"), + bei.description, + bei.stock_uom, + bei.bom_no, + item.min_order_qty, + bei.source_warehouse, + item.default_material_request_type, + item.min_order_qty, + item_default.default_warehouse, + item.purchase_uom, + item_uom.conversion_factor, + item.safety_stock, + ) + .where( + (bei.docstatus == 1) + & (bom.name == bom_no) + & (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1) + ) + .groupby(bei.item_code, bei.stock_uom) + ).run(as_dict=True) + + for item in items: + key = (item.item_code, item.bom_no) + if item.bom_no and key in sub_assembly_items: + planned_qty = flt(sub_assembly_items[key]) + get_raw_materials_of_sub_assembly_items( + item_details, + company, + item.bom_no, + include_non_stock_items, + sub_assembly_items, + planned_qty=planned_qty, + ) + else: + if not item.conversion_factor and item.purchase_uom: + item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom) + + item_details.setdefault(item.get("item_code"), item) + + return item_details diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 4648d896cee..75b43ec1c30 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -926,6 +926,50 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(after_qty, before_qty) + def test_skip_available_qty_for_sub_assembly_items(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + + bom_tree = { + "Fininshed Goods1 For SUB Test": { + "SubAssembly1 For SUB Test": {"ChildPart1 For SUB Test": {}}, + "SubAssembly2 For SUB Test": {}, + } + } + + parent_bom = create_nested_bom(bom_tree, prefix="") + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=10, + ignore_existing_ordered_qty=1, + do_not_submit=1, + skip_available_sub_assembly_item=1, + warehouse="_Test Warehouse - _TC", + ) + + make_stock_entry( + item_code="SubAssembly1 For SUB Test", + qty=5, + rate=100, + target="_Test Warehouse - _TC", + ) + + self.assertTrue(plan.skip_available_sub_assembly_item) + + plan.get_sub_assembly_items() + + for row in plan.sub_assembly_items: + if row.production_item == "SubAssembly1 For SUB Test": + self.assertEqual(row.qty, 5) + + mr_items = get_items_for_material_requests(plan.as_dict()) + for row in mr_items: + row = frappe._dict(row) + if row.item_code == "ChildPart1 For SUB Test": + self.assertEqual(row.quantity, 5) + + if row.item_code == "SubAssembly2 For SUB Test": + self.assertEqual(row.quantity, 10) + def create_production_plan(**args): """ @@ -945,6 +989,7 @@ def create_production_plan(**args): "include_subcontracted_items": args.include_subcontracted_items or 0, "ignore_existing_ordered_qty": args.ignore_existing_ordered_qty or 0, "get_items_from": "Sales Order", + "skip_available_sub_assembly_item": args.skip_available_sub_assembly_item or 0, } ) @@ -958,6 +1003,7 @@ def create_production_plan(**args): "planned_qty": args.planned_qty or 1, "planned_start_date": args.planned_start_date or now_datetime(), "stock_uom": args.stock_uom or "Nos", + "warehouse": args.warehouse, }, ) diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json index 4eb6bf6ecfc..fde0404c019 100644 --- a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json +++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json @@ -28,7 +28,11 @@ "uom", "stock_uom", "column_break_22", - "description" + "description", + "section_break_4rxf", + "actual_qty", + "column_break_xfhm", + "projected_qty" ], "fields": [ { @@ -183,12 +187,34 @@ "fieldtype": "Datetime", "in_list_view": 1, "label": "Schedule Date" + }, + { + "fieldname": "section_break_4rxf", + "fieldtype": "Section Break" + }, + { + "fieldname": "actual_qty", + "fieldtype": "Float", + "label": "Actual Qty", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_xfhm", + "fieldtype": "Column Break" + }, + { + "fieldname": "projected_qty", + "fieldtype": "Float", + "label": "Projected Qty", + "no_copy": 1, + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-11-28 13:50:15.116082", + "modified": "2023-05-22 17:52:34.708879", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Sub Assembly Item", diff --git a/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py b/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py index e37f291233e..64170edb718 100644 --- a/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py +++ b/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py @@ -7,4 +7,6 @@ def execute(): frappe.reload_doc("manufacturing", "doctype", "work_order") frappe.reload_doc("manufacturing", "doctype", "work_order_item") - frappe.db.sql("""UPDATE `tabWork Order Item` SET amount = rate * required_qty""") + frappe.db.sql( + """UPDATE `tabWork Order Item` SET amount = ifnull(rate, 0.0) * ifnull(required_qty, 0.0)""" + ) diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index d943126018a..47b88a002bc 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -91,6 +91,12 @@ frappe.ui.form.on("Sales Invoice", { }); frappe.ui.form.on('Purchase Invoice', { + setup: (frm) => { + frm.make_methods = { + 'Landed Cost Voucher': function () { frm.trigger('create_landedcost_voucher') }, + } + }, + mode_of_payment: function(frm) { get_payment_mode_account(frm, frm.doc.mode_of_payment, function(account){ frm.set_value('cash_bank_account', account); @@ -99,6 +105,20 @@ frappe.ui.form.on('Purchase Invoice', { payment_terms_template: function() { cur_frm.trigger("disable_due_date"); + }, + + create_landedcost_voucher: function (frm) { + let lcv = frappe.model.get_new_doc('Landed Cost Voucher'); + lcv.company = frm.doc.company; + + let lcv_receipt = frappe.model.get_new_doc('Landed Cost Purchase Invoice'); + lcv_receipt.receipt_document_type = 'Purchase Invoice'; + lcv_receipt.receipt_document = frm.doc.name; + lcv_receipt.supplier = frm.doc.supplier; + lcv_receipt.grand_total = frm.doc.grand_total; + lcv.purchase_receipts = [lcv_receipt]; + + frappe.set_route("Form", lcv.doctype, lcv.name); } }); diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 693fc92ce93..61969fe8a91 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -288,7 +288,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): ) # sales team - for d in customer.get("sales_team"): + for d in customer.get("sales_team") or []: target.append( "sales_team", { diff --git a/erpnext/selling/form_tour/quotation/quotation.json b/erpnext/selling/form_tour/quotation/quotation.json index 2a2aa5e63e4..8c977009fb6 100644 --- a/erpnext/selling/form_tour/quotation/quotation.json +++ b/erpnext/selling/form_tour/quotation/quotation.json @@ -2,9 +2,11 @@ "creation": "2021-11-23 12:00:36.138824", "docstatus": 0, "doctype": "Form Tour", + "first_document": 0, "idx": 0, + "include_name_field": 0, "is_standard": 1, - "modified": "2021-11-23 12:02:48.010298", + "modified": "2023-05-23 12:51:48.684517", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", @@ -14,51 +16,43 @@ "steps": [ { "description": "Select a customer or lead for whom this quotation is being prepared. Let's select a Customer.", - "field": "", "fieldname": "quotation_to", "fieldtype": "Link", "has_next_condition": 0, "is_table_field": 0, "label": "Quotation To", - "parent_field": "", "position": "Right", "title": "Quotation To" }, { "description": "Select a specific Customer to whom this quotation will be sent.", - "field": "", "fieldname": "party_name", "fieldtype": "Dynamic Link", "has_next_condition": 0, "is_table_field": 0, "label": "Party", - "parent_field": "", "position": "Right", "title": "Party" }, { "child_doctype": "Quotation Item", "description": "Select an item for which you will be quoting a price.", - "field": "", "fieldname": "items", "fieldtype": "Table", "has_next_condition": 0, "is_table_field": 0, "label": "Items", - "parent_field": "", "parent_fieldname": "items", "position": "Bottom", "title": "Items" }, { "description": "You can select pre-populated Sales Taxes and Charges from here.", - "field": "", "fieldname": "taxes", "fieldtype": "Table", "has_next_condition": 0, "is_table_field": 0, "label": "Sales Taxes and Charges", - "parent_field": "", "position": "Bottom", "title": "Sales Taxes and Charges" } diff --git a/erpnext/setup/module_onboarding/home/home.json b/erpnext/setup/module_onboarding/home/home.json index 516f12229c5..7b0d77ff503 100644 --- a/erpnext/setup/module_onboarding/home/home.json +++ b/erpnext/setup/module_onboarding/home/home.json @@ -25,15 +25,12 @@ "documentation_url": "https://docs.erpnext.com/docs/v14/user/manual/en/setting-up/company-setup", "idx": 0, "is_complete": 0, - "modified": "2023-05-16 13:13:24.043792", + "modified": "2023-05-23 13:20:19.703506", "modified_by": "Administrator", "module": "Setup", "name": "Home", "owner": "Administrator", "steps": [ - { - "step": "Navigation Help" - }, { "step": "Create an Item" }, @@ -41,13 +38,10 @@ "step": "Create a Customer" }, { - "step": "Create a Supplier" - }, - { - "step": "Create a Quotation" + "step": "Create Your First Sales Invoice" } ], - "subtitle": "Item, Customer, Supplier, Navigation Help and Quotation", + "subtitle": "Item, Customer, Supplier and Quotation", "success_message": "You're ready to start your journey with ERPNext", "title": "Let's begin your journey with ERPNext" } \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json index e1a8f908663..dc075781221 100644 --- a/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json +++ b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2023-05-16 12:54:54.112364", + "modified": "2023-05-23 12:45:55.138580", "modified_by": "Administrator", "name": "Create a Customer", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json index ef493fe00d1..4ac26e2879c 100644 --- a/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json +++ b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2023-05-16 12:55:08.610113", + "modified": "2023-05-19 15:32:55.069257", "modified_by": "Administrator", "name": "Create a Supplier", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/create_an_item/create_an_item.json b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json index 15f36bec81d..4115196ffa1 100644 --- a/erpnext/setup/onboarding_step/create_an_item/create_an_item.json +++ b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json @@ -11,7 +11,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2023-05-16 12:56:40.355878", + "modified": "2023-05-23 12:43:08.484206", "modified_by": "Administrator", "name": "Create an Item", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/create_your_first_sales_invoice/create_your_first_sales_invoice.json b/erpnext/setup/onboarding_step/create_your_first_sales_invoice/create_your_first_sales_invoice.json new file mode 100644 index 00000000000..96fa68e8c90 --- /dev/null +++ b/erpnext/setup/onboarding_step/create_your_first_sales_invoice/create_your_first_sales_invoice.json @@ -0,0 +1,20 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 17:48:21.019019", + "description": "# All about sales invoice\n\nA Sales Invoice is a bill that you send to your Customers against which the Customer makes the payment. Sales Invoice is an accounting transaction. On submission of Sales Invoice, the system updates the receivable and books income against a Customer Account.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2023-05-22 21:20:15.589644", + "modified_by": "Administrator", + "name": "Create Your First Sales Invoice", + "owner": "Administrator", + "reference_document": "Sales Invoice", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Create Your First Sales Invoice ", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 46d6e9e7578..a9a9a1d6641 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -29,6 +29,7 @@ class PickList(Document): self.validate_for_qty() def before_save(self): + self.update_status() self.set_item_locations() # set percentage picked in SO @@ -89,20 +90,20 @@ class PickList(Document): self.update_reference_qty() self.update_sales_order_picking_status() - def update_status(self, status=None, update_modified=True): + def update_status(self, status=None): if not status: if self.docstatus == 0: status = "Draft" elif self.docstatus == 1: - if self.status == "Draft": - status = "Open" - elif target_document_exists(self.name, self.purpose): + if target_document_exists(self.name, self.purpose): status = "Completed" + else: + status = "Open" elif self.docstatus == 2: status = "Cancelled" if status: - frappe.db.set_value("Pick List", self.name, "status", status, update_modified=update_modified) + self.db_set("status", status) def update_reference_qty(self): packed_items = [] @@ -459,7 +460,7 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus) item_doc.qty if (docstatus == 1 and item_doc.stock_qty == 0) else item_doc.stock_qty ) - while remaining_stock_qty > 0 and available_locations: + while flt(remaining_stock_qty) > 0 and available_locations: item_location = available_locations.pop(0) item_location = frappe._dict(item_location) diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js index 7a170beec37..50a78a82588 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js @@ -33,5 +33,40 @@ frappe.query_reports["Stock and Account Value Comparison"] = { "fieldtype": "Date", "default": frappe.datetime.get_today(), }, - ] + ], + + get_datatable_options(options) { + return Object.assign(options, { + checkboxColumn: true, + }); + }, + + onload(report) { + report.page.add_inner_button(__("Create Reposting Entries"), function() { + let message = `
+

+ Reposting Entries will change the value of + accounts Stock In Hand, and Stock Expenses + in the Trial Balance report and will also change + the Balance Value in the Stock Balance report. +

+

Are you sure you want to create Reposting Entries?

+
+ `; + + frappe.confirm(__(message), () => { + let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows(); + let selected_rows = indexes.map(i => frappe.query_report.data[i]); + + frappe.call({ + method: "erpnext.stock.report.stock_and_account_value_comparison.stock_and_account_value_comparison.create_reposting_entries", + args: { + rows: selected_rows, + company: frappe.query_report.get_filter_values().company + } + }); + + }); + }); + } }; diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py index 106e877c4cd..b1da3ec1bd1 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.utils import get_link_to_form, parse_json import erpnext from erpnext.accounts.utils import get_currency_precision, get_stock_accounts @@ -134,3 +135,35 @@ def get_columns(filters): "width": "120", }, ] + + +@frappe.whitelist() +def create_reposting_entries(rows, company): + if isinstance(rows, str): + rows = parse_json(rows) + + entries = [] + for row in rows: + row = frappe._dict(row) + + try: + doc = frappe.get_doc( + { + "doctype": "Repost Item Valuation", + "based_on": "Transaction", + "status": "Queued", + "voucher_type": row.voucher_type, + "voucher_no": row.voucher_no, + "posting_date": row.posting_date, + "company": company, + "allow_nagative_stock": 1, + } + ).submit() + + entries.append(get_link_to_form("Repost Item Valuation", doc.name)) + except frappe.DuplicateEntryError: + pass + + if entries: + entries = ", ".join(entries) + frappe.msgprint(_(f"Reposting entries created: {entries}")) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index 45289b1dab5..4bf008ac406 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -76,26 +76,14 @@ frappe.ui.form.on('Subcontracting Receipt', { } }); - let batch_no_field = frm.get_docfield("items", "batch_no"); + let batch_no_field = frm.get_docfield('items', 'batch_no'); if (batch_no_field) { batch_no_field.get_route_options_for_new_doc = function(row) { return { - "item": row.doc.item_code + 'item': row.doc.item_code } }; } - - frappe.db.get_single_value('Buying Settings', 'backflush_raw_materials_of_subcontract_based_on').then(val => { - if (val == 'Material Transferred for Subcontract') { - frm.fields_dict['supplied_items'].grid.grid_rows.forEach((grid_row) => { - grid_row.docfields.forEach((df) => { - if (df.fieldname == 'consumed_qty') { - df.read_only = 0; - } - }); - }); - } - }); }, refresh: (frm) => { @@ -157,6 +145,8 @@ frappe.ui.form.on('Subcontracting Receipt', { } }); }, __('Get Items From')); + + frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty', 'read_only', frm.doc.__onload && frm.doc.__onload.backflush_based_on === 'BOM'); } }, diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 2c842622730..416f4f80a21 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -28,6 +28,14 @@ class SubcontractingReceipt(SubcontractingController): }, ] + def onload(self): + self.set_onload( + "backflush_based_on", + frappe.db.get_single_value( + "Buying Settings", "backflush_raw_materials_of_subcontract_based_on" + ), + ) + def update_status_updater_args(self): if cint(self.is_return): self.status_updater.extend(