diff --git a/library_management/__init__.py b/library_management/__init__.py index e69de29..b5cb49c 100644 --- a/library_management/__init__.py +++ b/library_management/__init__.py @@ -0,0 +1 @@ +__version__ = '0.0.1' diff --git a/library_management/library_management/doctype/library_books/__init__.py b/library_management/library_management/doctype/library_books/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/library_management/library_management/doctype/library_books/library_books.js b/library_management/library_management/doctype/library_books/library_books.js new file mode 100644 index 0000000..46fb929 --- /dev/null +++ b/library_management/library_management/doctype/library_books/library_books.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Frappe and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Library Books", { +// refresh(frm) { + +// }, +// }); diff --git a/library_management/library_management/doctype/library_books/library_books.json b/library_management/library_management/doctype/library_books/library_books.json new file mode 100644 index 0000000..f775415 --- /dev/null +++ b/library_management/library_management/doctype/library_books/library_books.json @@ -0,0 +1,189 @@ +{ + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "B.######", + "creation": "2025-04-24 16:17:30.685181", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "isbn", + "book_name", + "accession_number", + "author", + "status", + "branch", + "column_break_pxyi", + "pages", + "publisher", + "language", + "price", + "source_of_book", + "image", + "column_break_frzw", + "quantity", + "available_quantity", + "bill_no_and_date", + "call_no", + "edition", + "year_of_publication", + "remarks", + "take_home" + ], + "fields": [ + { + "fieldname": "isbn", + "fieldtype": "Data", + "in_list_view": 1, + "label": "ISBN", + "reqd": 1 + }, + { + "fieldname": "book_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Book Name", + "reqd": 1 + }, + { + "fieldname": "accession_number", + "fieldtype": "Data", + "label": "Accession Number" + }, + { + "fieldname": "author", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Author", + "reqd": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Active\nInactive\nDiscontinued", + "reqd": 1 + }, + { + "fieldname": "branch", + "fieldtype": "Link", + "label": "Branch", + "options": "School", + "reqd": 1 + }, + { + "fieldname": "column_break_pxyi", + "fieldtype": "Column Break" + }, + { + "fieldname": "pages", + "fieldtype": "Data", + "label": "Pages" + }, + { + "fieldname": "publisher", + "fieldtype": "Data", + "label": "Publisher" + }, + { + "fieldname": "language", + "fieldtype": "Data", + "label": "Language" + }, + { + "fieldname": "price", + "fieldtype": "Data", + "label": "Price" + }, + { + "fieldname": "source_of_book", + "fieldtype": "Data", + "label": "Source of Book" + }, + { + "fieldname": "image", + "fieldtype": "Attach Image", + "label": "Image", + "options": "image" + }, + { + "fieldname": "column_break_frzw", + "fieldtype": "Column Break" + }, + { + "fieldname": "quantity", + "fieldtype": "Int", + "label": "Quantity", + "reqd": 1 + }, + { + "fieldname": "available_quantity", + "fieldtype": "Int", + "label": "Available Quantity", + "reqd": 1 + }, + { + "fieldname": "bill_no_and_date", + "fieldtype": "Data", + "label": "Bill No. and Date" + }, + { + "fieldname": "call_no", + "fieldtype": "Data", + "label": "Call No." + }, + { + "fieldname": "edition", + "fieldtype": "Data", + "label": "Edition" + }, + { + "fieldname": "year_of_publication", + "fieldtype": "Data", + "label": "Year of Publication" + }, + { + "fieldname": "remarks", + "fieldtype": "Data", + "label": "Remarks" + }, + { + "default": "0", + "fieldname": "take_home", + "fieldtype": "Check", + "label": "Take Home", + "reqd": 1 + } + ], + "grid_page_length": 50, + "image_field": "image", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-04-24 16:43:54.038751", + "modified_by": "Administrator", + "module": "Library Management", + "name": "Library Books", + "naming_rule": "Expression (old style)", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "show_title_field_in_link": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "book_name" +} \ No newline at end of file diff --git a/library_management/library_management/doctype/library_books/library_books.py b/library_management/library_management/doctype/library_books/library_books.py new file mode 100644 index 0000000..086c2ac --- /dev/null +++ b/library_management/library_management/doctype/library_books/library_books.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, Frappe and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class LibraryBooks(Document): + pass diff --git a/library_management/library_management/doctype/library_books/test_library_books.py b/library_management/library_management/doctype/library_books/test_library_books.py new file mode 100644 index 0000000..d19c8d2 --- /dev/null +++ b/library_management/library_management/doctype/library_books/test_library_books.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestLibraryBooks(FrappeTestCase): + pass diff --git a/library_management/library_management/doctype/library_books_student_table/__init__.py b/library_management/library_management/doctype/library_books_student_table/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/library_management/library_management/doctype/library_books_student_table/library_books_student_table.json b/library_management/library_management/doctype/library_books_student_table/library_books_student_table.json new file mode 100644 index 0000000..cee0074 --- /dev/null +++ b/library_management/library_management/doctype/library_books_student_table/library_books_student_table.json @@ -0,0 +1,109 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-04-24 16:57:59.073601", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "book_id", + "book_name", + "author", + "reference_number", + "column_break_dsnf", + "book_issue_date", + "book_return_date", + "reading_period", + "column_break_vyxp", + "book_status", + "due__days", + "take_home" + ], + "fields": [ + { + "fieldname": "book_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Book ID", + "options": "Library Books" + }, + { + "fieldname": "book_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Book Name", + "options": "Article" + }, + { + "fieldname": "author", + "fieldtype": "Data", + "label": "Author" + }, + { + "fieldname": "reference_number", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Reference Number" + }, + { + "fieldname": "column_break_dsnf", + "fieldtype": "Column Break" + }, + { + "fieldname": "book_issue_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Book Issue Date" + }, + { + "fieldname": "book_return_date", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Book Return Date" + }, + { + "fieldname": "reading_period", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Reading Period" + }, + { + "fieldname": "column_break_vyxp", + "fieldtype": "Column Break" + }, + { + "fieldname": "book_status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Book Status", + "options": "READING\nRETURNED\nLOST\nRENEWED" + }, + { + "fieldname": "due__days", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Due Days" + }, + { + "default": "0", + "fieldname": "take_home", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Take Home" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2025-05-22 15:46:30.463437", + "modified_by": "Administrator", + "module": "Library Management", + "name": "Library Books Student Table", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/library_management/library_management/doctype/library_books_student_table/library_books_student_table.py b/library_management/library_management/doctype/library_books_student_table/library_books_student_table.py new file mode 100644 index 0000000..afd5de7 --- /dev/null +++ b/library_management/library_management/doctype/library_books_student_table/library_books_student_table.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, Frappe and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class LibraryBooksStudentTable(Document): + pass diff --git a/library_management/library_management/doctype/library_transactions/__init__.py b/library_management/library_management/doctype/library_transactions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/library_management/library_management/doctype/library_transactions/library_transactions.js b/library_management/library_management/doctype/library_transactions/library_transactions.js new file mode 100644 index 0000000..95e66ad --- /dev/null +++ b/library_management/library_management/doctype/library_transactions/library_transactions.js @@ -0,0 +1,404 @@ +// Copyright (c) 2025, Frappe and contributors +// For license information, please see license.txt + + + +console.log("Library Transactions JS loaded"); +frappe.ui.form.on("Library Transactions", { + // Add flag to prevent double triggering + _is_fetching_book_details: false, + + refresh(frm) { + // Set field properties + set_field_properties(frm); + + // Auto-calculate due days on refresh + calculate_due_days(frm); + }, + + // Auto-populate student details when student is selected + student(frm) { + if (frm.doc.student) { + frappe.call({ + method: "frappe.client.get_value", + args: { + doctype: "Student", + fieldname: ["user", "program", "school"], + filters: { name: frm.doc.student } + }, + callback: function(r) { + if (r.message) { + // Auto-fill student details + frm.set_value("student_email", r.message.user || ""); + frm.set_value("classs", r.message.program || ""); + frm.set_value("branch", r.message.school || ""); + + } else { + frappe.msgprint({ + message: __("Could not fetch student details. Please check the student record."), + indicator: "orange" + }); + } + }, + error: function(err) { + frappe.msgprint({ + message: __("Error fetching student details: ") + (err.message || "Unknown error"), + indicator: "red" + }); + } + }); + } else { + // Clear dependent fields when student is cleared + frm.set_value("student_email", ""); + frm.set_value("classs", ""); + frm.set_value("branch", ""); + } + }, + + // Auto-populate book details when ISBN is entered + isbn(frm) { + // Prevent double triggering + if (frm._is_fetching_book_details) { + return; + } + + if (frm.doc.isbn && frm.doc.isbn.trim()) { + frm._is_fetching_book_details = true; + fetch_book_details_by_field(frm, "isbn", frm.doc.isbn.trim()); + } else { + clear_book_details(frm); + } + }, + + // Auto-populate book details when Accession Number is entered + accession_number(frm) { + // Prevent double triggering + if (frm._is_fetching_book_details) { + return; + } + + if (frm.doc.accession_number && frm.doc.accession_number.trim()) { + frm._is_fetching_book_details = true; + fetch_book_details_by_field(frm, "accession_number", frm.doc.accession_number.trim()); + } else if (!frm.doc.isbn) { + // Only clear if ISBN is also not present + clear_book_details(frm); + } + }, + + // Auto-calculate reading period when dates change + date_of_issue(frm) { + calculate_reading_period(frm); + }, + + return_date(frm) { + calculate_reading_period(frm); + calculate_due_days(frm); + }, + + book_status(frm) { + if (frm.doc.book_status === "RENEWED") { + frm.set_value("return_date", frappe.datetime.now_datetime()); + } + + calculate_due_days(frm); + if (frm.doc.book_status === "RENEWED") { + frm.set_df_property("return_date", "read_only", 1); + } else { + frm.set_df_property("return_date", "read_only", 0); + } + + + if (frm.doc.book_status === "RENEWED") { + frm.set_value("due_days", "0 days"); + frm.dashboard.clear_headline();} + + }, + + + + + + // Validation before save + before_save(frm) { + // Validate required fields + if (!frm.doc.student) { + frappe.msgprint(__("Please select a student")); + frappe.validated = false; + return; + } + + if (!frm.doc.isbn && !frm.doc.accession_number) { + frappe.msgprint(__("Please enter either ISBN or Accession Number")); + frappe.validated = false; + return; + } + + if (!frm.doc.book_name) { + frappe.msgprint(__("Book details not found. Please check ISBN or Accession Number")); + frappe.validated = false; + return; + } + + // Validate take_home permission + if (!frm.doc.take_home) { + frappe.msgprint(__("This book is not allowed for home reading. Only books with 'Take Home' permission can be issued.")); + frappe.validated = false; + return; + } + + // Validate dates + if (frm.doc.date_of_issue && frm.doc.return_date) { + if (frm.doc.return_date < frm.doc.date_of_issue) { + frappe.msgprint(__("Return date cannot be before issue date")); + frappe.validated = false; + return; + } + } + + // Check quantity + if (frm.doc.quantity_available <= 0 && frm.doc.book_status === "READING") { + frappe.confirm( + __("This book is out of stock. Do you still want to proceed?"), + function() { + // Continue with save + }, + function() { + frappe.validated = false; + } + ); + } + } +}); + +// Helper function to fetch book details by field (ISBN or Accession Number) +function fetch_book_details_by_field(frm, field_name, field_value) { + if (!field_value) { + frm._is_fetching_book_details = false; + return; + } + + // Show loading indicator + frm.dashboard.set_headline_alert("Fetching book details...", "blue"); + + frappe.call({ + method: "frappe.client.get_value", + args: { + doctype: "Library Books", + fieldname: [ + "book_name", + "author", + "publisher", + "available_quantity", + "take_home", + "branch", + "isbn", + "accession_number", + "status" + ], + filters: { [field_name]: field_value } + }, + callback: function(r) { + // Clear loading indicator + frm.dashboard.clear_headline(); + + if (r.message) { + let book = r.message; + + // Check if book is active + if (book.status !== "Active") { + frappe.msgprint({ + message: __("Warning: This book is not active in the system"), + indicator: "orange" + }); + } + + // Auto-fill book details + frm.set_value("book_name", book.book_name || ""); + frm.set_value("author", book.author || ""); + frm.set_value("publisher", book.publisher || ""); + frm.set_value("quantity_available", book.available_quantity || 0); + frm.set_value("take_home", book.take_home || 0); + + // Cross-populate ISBN and Accession Number WITHOUT triggering field events + if (field_name === "isbn" && book.accession_number && !frm.doc.accession_number) { + frm.doc.accession_number = book.accession_number; + frm.refresh_field("accession_number"); + } else if (field_name === "accession_number" && book.isbn && !frm.doc.isbn) { + frm.doc.isbn = book.isbn; + frm.refresh_field("isbn"); + } + + // Auto-set dates and calculate reading period + if (!frm.doc.date_of_issue) { + frm.set_value("date_of_issue", frappe.datetime.get_today()); + } + + if (!frm.doc.return_date) { + // Set return date to 7 days from today + let return_date = frappe.datetime.add_days(frappe.datetime.get_today(), 7); + frm.set_value("return_date", return_date); + } + + // Calculate reading period after setting dates + setTimeout(function() { + calculate_reading_period(frm); + }, 100); + + // Check take_home permission + if (!book.take_home) { + frappe.msgprint({ + message: __("Warning: This book is not allowed for home reading"), + indicator: "red" + }); + frm.dashboard.set_headline_alert("Book Not Available for Home Reading", "red"); + } + + // Check availability + if (book.available_quantity <= 0) { + frappe.msgprint({ + message: __("Warning: This book is currently out of stock"), + indicator: "red" + }); + frm.dashboard.set_headline_alert("Book Out of Stock", "red"); + } else if (book.available_quantity <= 2) { + frappe.msgprint({ + message: __("Notice: Only {0} copies available", [book.available_quantity]), + indicator: "orange" + }); + } + + // Update branch if different from student's branch + if (book.branch && frm.doc.branch && book.branch !== frm.doc.branch) { + frappe.msgprint({ + message: __("Note: Book belongs to {0} branch, but student is from {1} branch", + [book.branch, frm.doc.branch]), + indicator: "yellow" + }); + } + + // Success message + frappe.msgprint({ + message: __("Book details populated successfully"), + indicator: "green" + }); + + } else { + // Book not found + frappe.msgprint({ + message: __("No book found with {0}: {1}", [field_name.replace("_", " "), field_value]), + indicator: "red" + }); + + // Clear book details if not found + clear_book_details(frm, false); + } + + // Reset the flag after processing is complete + frm._is_fetching_book_details = false; + }, + error: function(err) { + frm.dashboard.clear_headline(); + frappe.msgprint({ + message: __("Error fetching book details: ") + (err.message || "Unknown error"), + indicator: "red" + }); + + // Reset the flag on error + frm._is_fetching_book_details = false; + } + }); +} + +// Helper function to clear book details +function clear_book_details(frm, clear_identifiers = true) { + frm.set_value("book_name", ""); + frm.set_value("author", ""); + frm.set_value("publisher", ""); + frm.set_value("quantity_available", ""); + frm.set_value("take_home", 0); + frm.set_value("date_of_issue", ""); + frm.set_value("return_date", ""); + frm.set_value("reading_period", ""); + frm.set_value("due_days", ""); + + if (clear_identifiers) { + frm.set_value("isbn", ""); + frm.set_value("accession_number", ""); + } + + frm.dashboard.clear_headline(); +} + +// Helper function to calculate reading period +function calculate_reading_period(frm) { + if (frm.doc.date_of_issue && frm.doc.return_date) { + let issue_date = frappe.datetime.str_to_obj(frm.doc.date_of_issue); + let return_date = frappe.datetime.str_to_obj(frm.doc.return_date); + let days = frappe.datetime.get_diff(return_date, issue_date); + + if (days >= 0) { + frm.set_value("reading_period", days + " days"); + } else { + frappe.msgprint({ + message: __("Return date cannot be before issue date"), + indicator: "red" + }); + } + } +} + +// Helper function to calculate due days +function calculate_due_days(frm) { + if (frm.doc.book_status === "READING" && frm.doc.return_date) { + let today = frappe.datetime.get_today(); + let return_date = frm.doc.return_date; + + if (today > return_date) { + let overdue_days = frappe.datetime.get_diff(today, return_date); + frm.set_value("due_days", overdue_days + " days"); + + // Show overdue warning + frm.dashboard.set_headline_alert(`Book Overdue by ${overdue_days} days`, "red"); + } else { + frm.set_value("due_days", "0 days"); + if (frm.doc.book_status === "READING") { + frm.dashboard.clear_headline(); + } + } + } else { + frm.set_value("due_days", "0 days"); + } +} + +// Child table event handler for Library Books Student Table +frappe.ui.form.on("Library Books Student Table", { + book_status: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + + if (row.book_status === "RENEWED") { + // Set book_return_date to current date and time when RENEWED is selected + frappe.model.set_value(cdt, cdn, "book_return_date", frappe.datetime.now_datetime()); + } + } +}); + +// Helper function to set field properties +function set_field_properties(frm) { + // Make certain fields read-only after book details are populated + if (frm.doc.book_name) { + frm.set_df_property("book_name", "read_only", 1); + frm.set_df_property("author", "read_only", 1); + frm.set_df_property("publisher", "read_only", 1); + frm.set_df_property("quantity_available", "read_only", 1); + } + + // Set field descriptions + frm.set_df_property("isbn", "description", "Enter ISBN to auto-populate book details"); + frm.set_df_property("accession_number", "description", "Enter Accession Number to auto-populate book details"); + + // Highlight take_home field if not checked + if (frm.doc.take_home === 0 && frm.doc.book_name) { + frm.set_df_property("take_home", "description", "⚠️ This book is not allowed for home reading"); + } +} \ No newline at end of file diff --git a/library_management/library_management/doctype/library_transactions/library_transactions.json b/library_management/library_management/doctype/library_transactions/library_transactions.json new file mode 100644 index 0000000..09089ed --- /dev/null +++ b/library_management/library_management/doctype/library_transactions/library_transactions.json @@ -0,0 +1,183 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "format:{student} {name} {#####}", + "creation": "2025-04-24 16:44:54.204848", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "student_details_section", + "student", + "classs", + "column_break_mtqw", + "student_email", + "branch", + "book_transaction_details_section", + "isbn", + "book_name", + "accession_number", + "column_break_nplj", + "author", + "publisher", + "return_date", + "quantity_available", + "column_break_duyr", + "date_of_issue", + "reading_period", + "due_days", + "book_status", + "take_home" + ], + "fields": [ + { + "fieldname": "student_details_section", + "fieldtype": "Section Break", + "label": "Student Details" + }, + { + "fieldname": "student", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Student", + "options": "Student", + "reqd": 1 + }, + { + "fetch_from": "student.program", + "fieldname": "classs", + "fieldtype": "Data", + "label": "Class" + }, + { + "fieldname": "column_break_mtqw", + "fieldtype": "Column Break" + }, + { + "fetch_from": "student.school", + "fieldname": "branch", + "fieldtype": "Data", + "label": "Branch" + }, + { + "fieldname": "book_transaction_details_section", + "fieldtype": "Section Break", + "label": "Book Transaction Details" + }, + { + "fieldname": "isbn", + "fieldtype": "Data", + "in_list_view": 1, + "label": "ISBN", + "reqd": 1 + }, + { + "fieldname": "book_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Book Name", + "reqd": 1 + }, + { + "fieldname": "accession_number", + "fieldtype": "Data", + "label": "Accession Number" + }, + { + "fieldname": "column_break_nplj", + "fieldtype": "Column Break" + }, + { + "fieldname": "author", + "fieldtype": "Data", + "label": "Author" + }, + { + "fieldname": "publisher", + "fieldtype": "Data", + "label": "Publisher" + }, + { + "fieldname": "return_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Return Date", + "reqd": 1 + }, + { + "fieldname": "quantity_available", + "fieldtype": "Int", + "label": "Quantity Available" + }, + { + "fieldname": "column_break_duyr", + "fieldtype": "Column Break" + }, + { + "fieldname": "date_of_issue", + "fieldtype": "Date", + "label": "Date of Issue" + }, + { + "fieldname": "reading_period", + "fieldtype": "Data", + "label": "Reading Period" + }, + { + "fieldname": "due_days", + "fieldtype": "Data", + "label": "Due Days" + }, + { + "fieldname": "book_status", + "fieldtype": "Select", + "label": "Book Status", + "options": "READING\nRETURNED\nLOST\nRENEWED" + }, + + { + "fieldname": "renewed_on", + "fieldtype": "Datetime", + "label": "Renewed On", + "read_only": 1 +}, + { + "default": "0", + "fieldname": "take_home", + "fieldtype": "Check", + "label": "Take Home" + }, + { + "fetch_from": "student.user", + "fieldname": "student_email", + "fieldtype": "Data", + "label": "Student Email" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-05-22 16:03:39.389444", + "modified_by": "Administrator", + "module": "Library Management", + "name": "Library Transactions", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/library_management/library_management/doctype/library_transactions/library_transactions.py b/library_management/library_management/doctype/library_transactions/library_transactions.py new file mode 100644 index 0000000..2500061 --- /dev/null +++ b/library_management/library_management/doctype/library_transactions/library_transactions.py @@ -0,0 +1,260 @@ +# Copyright (c) 2025, Frappe and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe.utils import getdate, today, add_days, date_diff, cint +from frappe import _ +from frappe.utils import now_datetime + +class LibraryTransactions(Document): + def validate(self): + """Validate document before saving""" + self.validate_required_fields() + self.validate_take_home_permission() + self.validate_student_details() + self.validate_book_details() + self.validate_stock_availability() + self.validate_dates() + self.auto_set_dates() + self.set_renewed_datetime() + self.calculate_reading_period() + self.calculate_due_days() + + def after_insert(self): + """After inserting the document - only for new records""" + if self.book_status == "READING": + self.update_student_library_books() + self.update_book_inventory() + + def on_update_after_submit(self): + """After updating submitted document""" + # Only update if this is a status change + if self.has_value_changed("book_status"): + self.update_student_library_books() + + def on_update(self): + """On document update - handle status changes""" + # Only update for existing records and if status changed + if not self.is_new() and self.has_value_changed("book_status"): + self.update_student_library_books() + + def validate_required_fields(self): + """Validate required fields""" + if not self.student: + frappe.throw(_("Student is required")) + + if not self.isbn and not self.accession_number: + frappe.throw(_("Either ISBN or Accession Number is required")) + + def validate_take_home_permission(self): + """Validate if book is allowed for home reading""" + if not self.take_home and self.book_status == "READING": + frappe.throw(_("This book is not allowed for home reading. Only books with 'Take Home' permission can be issued.")) + + def auto_set_dates(self): + """Auto-set dates if not provided""" + if not self.date_of_issue: + self.date_of_issue = today() + + if not self.return_date and self.date_of_issue: + self.return_date = add_days(self.date_of_issue, 7) + + def validate_student_details(self): + """Validate and auto-populate student details""" + if self.student: + student = frappe.get_value("Student", self.student, + ["user", "program", "school", "reference_number"], as_dict=True) + + if student: + if not self.student_email: + self.student_email = student.user + if not self.classs: + self.classs = student.program + if not self.branch: + self.branch = student.school + + def validate_book_details(self): + """Validate and auto-populate book details""" + if (self.isbn or self.accession_number) and not self.book_name: + search_field = "isbn" if self.isbn else "accession_number" + search_value = self.isbn if self.isbn else self.accession_number + + book = frappe.get_value("Library Books", + {search_field: search_value}, + ["book_name", "author", "publisher", "available_quantity", + "take_home", "branch", "isbn", "accession_number", "status"], + as_dict=True) + + if book: + if book.status != "Active": + frappe.msgprint(_("Warning: This book is not active in the system")) + + self.book_name = book.book_name + self.author = book.author + self.publisher = book.publisher + self.quantity_available = book.available_quantity + self.take_home = book.take_home + + if search_field == "isbn" and book.accession_number and not self.accession_number: + self.accession_number = book.accession_number + elif search_field == "accession_number" and book.isbn and not self.isbn: + self.isbn = book.isbn + + if cint(book.available_quantity) <= 0: + frappe.msgprint(_("Warning: This book is currently out of stock")) + + else: + frappe.throw(_("No book found with {0}: {1}") + .format(search_field.replace("_", " "), search_value)) + + def validate_stock_availability(self): + """Validate stock availability for book issuance""" + if self.book_status == "READING" and (self.isbn or self.accession_number): + search_field = "isbn" if self.isbn else "accession_number" + search_value = self.isbn if self.isbn else self.accession_number + + current_stock = frappe.get_value("Library Books", + {search_field: search_value}, + "available_quantity") + + if current_stock is not None and cint(current_stock) <= 0: + frappe.throw(_("Cannot issue this book. Current available quantity is {0}. Please check book inventory.").format(current_stock)) + + def validate_dates(self): + """Validate date fields""" + if self.date_of_issue and self.return_date: + if getdate(self.return_date) < getdate(self.date_of_issue): + frappe.throw(_("Return date cannot be before issue date")) + + def calculate_reading_period(self): + """Calculate reading period based on issue and return dates""" + if self.date_of_issue and self.return_date: + days = date_diff(self.return_date, self.date_of_issue) + self.reading_period = f"{days} days" + + def calculate_due_days(self): + """Calculate overdue days if applicable""" + if (self.book_status == "READING" and + self.return_date and + getdate(self.return_date) < getdate(today())): + overdue_days = date_diff(today(), self.return_date) + self.due_days = f"{overdue_days} days" + else: + self.due_days = "0 days" + + def update_student_library_books(self): + """Update student's custom_library_books child table""" + if not self.student or not self.book_name: + return + + # Add flag to prevent multiple calls in same request + if hasattr(frappe.local, 'library_update_processed') and frappe.local.library_update_processed: + return + + try: + student_doc = frappe.get_doc("Student", self.student) + + # Create unique identifier for this book transaction + book_identifier = f"{self.book_name}_{self.date_of_issue}_{self.isbn or self.accession_number}" + + # Find existing entry by checking multiple criteria + existing_entry = None + for book in student_doc.custom_library_books: + existing_identifier = f"{book.book_name}_{book.book_issue_date}_{book.book_id}" + + if existing_identifier == book_identifier: + existing_entry = book + break + + update_made = False + + if existing_entry: + # Update existing entry + existing_entry.book_return_date = str(self.return_date) if self.return_date else "" + existing_entry.reading_period = self.reading_period or "" + existing_entry.book_status = self.book_status or "READING" + existing_entry.due__days = self.due_days or "0 days" + existing_entry.take_home = self.take_home or 0 + existing_entry.author = self.author or "" + + update_made = True + action_message = _("Updated existing book entry in student record") + + else: + # Add new entry only if book status is READING (new issue) + if self.book_status == "READING": + library_book = student_doc.append("custom_library_books", {}) + library_book.book_id = self.isbn or self.accession_number or "" + library_book.book_name = self.book_name or "" + library_book.reference_number = self.accession_number or "" + library_book.book_issue_date = str(self.date_of_issue) if self.date_of_issue else str(today()) + library_book.book_return_date = str(self.return_date) if self.return_date else "" + library_book.reading_period = self.reading_period or "" + library_book.book_status = self.book_status or "READING" + library_book.take_home = self.take_home or 0 + library_book.due__days = self.due_days or "0 days" + library_book.author = self.author or "" + + update_made = True + action_message = _("Added new book entry to student record") + + if update_made: + # Update number of books issued - COUNT ONLY READING STATUS BOOKS + reading_books_count = 0 + for book in student_doc.custom_library_books: + if book.book_status == "READING": + reading_books_count += 1 + + student_doc.number_of_books_issued = str(reading_books_count) + + # Save student document + student_doc.save(ignore_permissions=True) + + # Show single consolidated message + frappe.msgprint(_("{0}. Current reading books: {1}").format(action_message, reading_books_count)) + + # Set flag to prevent duplicate calls + frappe.local.library_update_processed = True + + except Exception as e: + frappe.log_error(f"Error updating student library books: {str(e)}") + frappe.msgprint(_("Warning: Could not update student library records")) + + def update_book_inventory(self): + """Update book inventory when book is issued""" + if (self.book_status == "READING" and + (self.isbn or self.accession_number) and + self.is_new()): + + search_field = "isbn" if self.isbn else "accession_number" + search_value = self.isbn if self.isbn else self.accession_number + + try: + book_doc = frappe.get_doc("Library Books", {search_field: search_value}) + if book_doc and book_doc.available_quantity > 0: + book_doc.available_quantity -= 1 + book_doc.save(ignore_permissions=True) + frappe.msgprint(_("Book inventory updated - Available quantity: {0}").format(book_doc.available_quantity)) + + except Exception as e: + frappe.log_error(f"Error updating book inventory: {str(e)}") + +# Scheduled function to update due days for all reading books +def update_all_due_days(): + """Scheduled function to update due days for all books with status READING""" + try: + reading_transactions = frappe.get_all("Library Transactions", + filters={"book_status": "READING"}, + fields=["name", "return_date"]) + + for transaction in reading_transactions: + if transaction.return_date and getdate(transaction.return_date) < getdate(today()): + overdue_days = date_diff(today(), transaction.return_date) + frappe.db.set_value("Library Transactions", transaction.name, + "due_days", f"{overdue_days} days") + + frappe.db.commit() + + except Exception as e: + frappe.log_error(f"Error in update_all_due_days: {str(e)}") \ No newline at end of file diff --git a/library_management/library_management/doctype/library_transactions/test_library_transactions.py b/library_management/library_management/doctype/library_transactions/test_library_transactions.py new file mode 100644 index 0000000..273abb0 --- /dev/null +++ b/library_management/library_management/doctype/library_transactions/test_library_transactions.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestLibraryTransactions(FrappeTestCase): + pass