diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 32e6881e..b2201a0f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,7 +53,7 @@ repos: additional_dependencies: ['flake8-bugbear'] - repo: https://github.com/agritheory/test_utils - rev: v1.20.1 + rev: v1.25.1 hooks: - id: update_pre_commit_config - id: validate_frappe_project diff --git a/README.md b/README.md index ac45bf9e..29f41944 100644 --- a/README.md +++ b/README.md @@ -81,11 +81,20 @@ For a complete database reset to re-run tests, run the following bench reinstall --yes --admin-password admin --mariadb-root-password admin && bench execute 'beam.tests.setup.before_test' ``` -To run mypy and pytest +To run backend tests + +```shell +source env/bin/activate +pytest ./apps/beam/beam/tests --ignore=./apps/beam/beam/tests/mobile/ --disable-warnings -s --tracing=retain-on-failure +``` + +To run frontend tests + +Start bench in a separate terminal, then run: + ```shell source env/bin/activate -mypy ./apps/beam/beam --ignore-missing-imports -pytest ./apps/beam/beam/tests -s --disable-warnings +pytest ./apps/beam/beam/tests/mobile --browser chromium --disable-warnings ``` ### Beam Portal setup diff --git a/beam/beam/demand/sqlite.py b/beam/beam/demand/sqlite.py index e01f1e70..378ffb94 100644 --- a/beam/beam/demand/sqlite.py +++ b/beam/beam/demand/sqlite.py @@ -3,6 +3,7 @@ import pathlib import sqlite3 +from typing import Any import frappe from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions @@ -153,7 +154,7 @@ def reset_receiving_db() -> None: cursor.execute("DELETE FROM receiving") -def dict_factory(cursor: sqlite3.Cursor, row: sqlite3.Row) -> frappe._dict: +def dict_factory(cursor: sqlite3.Cursor, row: tuple[Any, ...]) -> frappe._dict: _dict = frappe._dict() for idx, col in enumerate(cursor.description): _dict[col[0]] = row[idx] diff --git a/beam/beam/manufacturing.py b/beam/beam/manufacturing.py new file mode 100644 index 00000000..d695d903 --- /dev/null +++ b/beam/beam/manufacturing.py @@ -0,0 +1,155 @@ +# Copyright (c) 2026, AgriTheory and contributors +# For license information, please see license.txt + +import frappe + + +def _check_manufacturing_permission() -> None: + allowed_roles = {"Manufacturing User", "Manufacturing Manager", "System Manager"} + if not allowed_roles.intersection(frappe.get_roles()): + frappe.throw(frappe._("Not permitted"), frappe.PermissionError) + + +def _get_current_employee() -> str | None: + employees = frappe.get_all( + "Employee", + filters=[["user_id", "=", frappe.session.user], ["status", "=", "Active"]], + pluck="name", + limit=1, + ) + return employees[0] if employees else None + + +def _fetch_job_card(job_card_id: str): + frappe.flags.ignore_user_permissions = True + try: + doc = frappe.get_doc("Job Card", job_card_id) + finally: + frappe.flags.ignore_user_permissions = False + return doc + + +def _get_locked_by_employee(doc) -> str | None: + """Return the employee name holding an open time log, if it belongs to someone else.""" + current_employee = _get_current_employee() + for log in doc.time_logs: + if not log.to_time and log.employee and log.employee != current_employee: + return log.employee + return None + + +def _make_time_log(args: frappe._dict) -> None: + """ + Replicate ERPNext's make_time_log but with ignore_permissions on the doc so that + User Permissions on the employee link field don't block the save. + Business-logic validations (sequence, overlap, qty) are still executed by ERPNext. + """ + frappe.flags.ignore_user_permissions = True + try: + doc = frappe.get_doc("Job Card", args.job_card_id) + doc.flags.ignore_permissions = True + doc.validate_sequence_id() + _ensure_employee_on_job_card(doc, args) + doc.add_time_log(args) + finally: + frappe.flags.ignore_user_permissions = False + + +def _ensure_employee_on_job_card(doc, args: frappe._dict) -> None: + """ + ERPNext only populates the Job Card employee field when it is empty (first start). + When a second employee picks up paused work, we add them here so the field always + reflects every participant. + """ + employees = args.get("employees") or [] + if isinstance(employees, str): + import json + + employees = json.loads(employees) + + existing = {row.employee for row in doc.employee if row.employee} + for entry in employees: + emp = entry.get("employee") + if emp and emp not in existing: + doc.append("employee", {"employee": emp, "completed_qty": 0.0}) + existing.add(emp) + + +@frappe.whitelist() +def get_job_card(job_card_id: str) -> dict: + """ + Fetch a Job Card bypassing User Permissions on the employee link field. + + Manufacturing Users have role-level read access to all Job Cards, but Frappe's + User Permission system restricts access when the time log references an employee + that belongs to a different user. This endpoint enforces role-level permission + while bypassing the employee-link User Permission check so any Manufacturing User + can view (and continue) a job card started by a colleague. + """ + _check_manufacturing_permission() + doc = _fetch_job_card(job_card_id) + result = doc.as_dict() + result["locked_by_employee"] = _get_locked_by_employee(doc) + return result + + +@frappe.whitelist() +def start_job_card(job_card_id: str) -> dict: + _check_manufacturing_permission() + + current_employee = _get_current_employee() + if not current_employee: + frappe.throw( + frappe._("No active Employee is linked to the current user."), + frappe.PermissionError, + ) + + args = frappe._dict( + job_card_id=job_card_id, + start_time=frappe.utils.now_datetime(), + status="Work In Progress", + ) + args.employees = [{"employee": current_employee}] + + _make_time_log(args) + return get_job_card(job_card_id) + + +@frappe.whitelist() +def pause_job_card(job_card_id: str, completed_qty: float = 0) -> dict: + _check_manufacturing_permission() + + _make_time_log( + frappe._dict( + job_card_id=job_card_id, + complete_time=frappe.utils.now_datetime(), + status="On Hold", + completed_qty=frappe.utils.flt(completed_qty), + ) + ) + return get_job_card(job_card_id) + + +@frappe.whitelist() +def finish_job_card(job_card_id: str, completed_qty: float) -> dict: + _check_manufacturing_permission() + + _make_time_log( + frappe._dict( + job_card_id=job_card_id, + complete_time=frappe.utils.now_datetime(), + status="Complete", + completed_qty=frappe.utils.flt(completed_qty), + ) + ) + + frappe.flags.ignore_user_permissions = True + try: + doc = frappe.get_doc("Job Card", job_card_id) + if doc.docstatus == 0: + doc.flags.ignore_permissions = True + doc.submit() + finally: + frappe.flags.ignore_user_permissions = False + + return get_job_card(job_card_id) diff --git a/beam/tests/fixtures.py b/beam/tests/fixtures.py index 2a8f7a96..56f72ea2 100644 --- a/beam/tests/fixtures.py +++ b/beam/tests/fixtures.py @@ -728,7 +728,7 @@ }, "phone": "(704) 885-0542", "roles": ["Stock Manager", "Item Manager"], - # "department": "Operations", + "department": "Management - APC", "designation": "Bakery Manager", }, { @@ -745,7 +745,7 @@ "phone": "(658) 583-5499", "roles": ["Stock User", "BEAM Mobile User"], "reports_to": "Tristan Hawkins", - # "department": "Operations", + "department": "Management - APC", "designation": "Baker", }, { @@ -762,7 +762,7 @@ "phone": "(962) 762-5895", "roles": ["Stock User", "BEAM Mobile User"], "reports_to": "Tristan Hawkins", - # "department": "Operations", + "department": "Management - APC", "designation": "Baker", }, { @@ -779,7 +779,7 @@ "phone": "(366) 357-8223", "roles": ["Stock User", "BEAM Mobile User"], "reports_to": "Tristan Hawkins", - # "department": "Operations", + "department": "Management - APC", "designation": "Bakery Manager", }, { @@ -796,7 +796,7 @@ "phone": "(930) 920-4520", "roles": ["Stock User", "BEAM Mobile User"], "reports_to": "Tristan Hawkins", - # "department": "Operations", + "department": "Management - APC", "designation": "Baker", }, { @@ -813,7 +813,7 @@ "phone": "(054) 893-8970", "roles": ["Stock User", "BEAM Mobile User"], "reports_to": "Tristan Hawkins", - # "department": "Operations", + "department": "Management - APC", "designation": "Baker", }, { @@ -830,7 +830,7 @@ "phone": "(814) 677-9322", "roles": ["Stock User", "BEAM Mobile User"], "reports_to": "Tristan Hawkins", - # "department": "Operations", + "department": "Management - APC", "designation": "Baker", }, { @@ -847,7 +847,7 @@ "phone": "(133) 195-7828", "roles": ["Stock User", "BEAM Mobile User"], "reports_to": "Tristan Hawkins", - # "department": "Operations", + "department": "Management - APC", "designation": "Baker", }, { @@ -864,7 +864,41 @@ "phone": "(041) 000-2569", "roles": ["Stock User", "BEAM Mobile User"], "reports_to": "Tristan Hawkins", - # "department": "Operations", + "department": "Management - APC", + "designation": "Baker", + }, + { + "name": "Jordan Mills", + "gender": "Male", + "date_of_birth": "1996-04-15", + "date_of_joining": "2024-09-01", + "address": { + "address_line1": "440 Orchard Lane", + "city": "Nashua", + "state": "NH", + "postal_code": "03060", + }, + "phone": "(603) 555-0134", + "roles": ["BEAM Mobile User", "Employee", "Manufacturing User"], + "reports_to": "Tristan Hawkins", + "department": "Management - APC", + "designation": "Baker", + }, + { + "name": "Cassidy Reyes", + "gender": "Female", + "date_of_birth": "1993-11-28", + "date_of_joining": "2023-03-15", + "address": { + "address_line1": "217 Maple Street", + "city": "Concord", + "state": "NH", + "postal_code": "03301", + }, + "phone": "(603) 555-0271", + "roles": ["BEAM Mobile User", "Employee", "Manufacturing User"], + "reports_to": "Tristan Hawkins", + "department": "Management - APC", "designation": "Baker", }, ] diff --git a/beam/tests/mobile/test_job_card_operation.py b/beam/tests/mobile/test_job_card_operation.py new file mode 100644 index 00000000..934ffe97 --- /dev/null +++ b/beam/tests/mobile/test_job_card_operation.py @@ -0,0 +1,259 @@ +# Copyright (c) 2026, AgriTheory and contributors +# For license information, please see license.txt + +import re + +import frappe +import pytest +from playwright.sync_api import expect + +from beam.tests.test_utils import use_current_db_transaction + +# NOTE: any navigation tests should be done using `expect(page).to_have_url` since +# `page.expect_navigation()` won't work with Beam's hash-based routes + + +@pytest.fixture(autouse=True) +def login_as_jordan_mills(page): + base_url = frappe.utils.get_url() + page.context.clear_cookies() + page.goto(base_url) + email_field = page.get_by_role("textbox", name="Email") + expect(email_field).to_be_visible(timeout=1000) + email_field.fill("jmills@cfc.co") + + password_field = page.get_by_role("textbox", name="Password") + expect(password_field).to_be_visible(timeout=1000) + password_field.fill("admin") + + login_button = page.get_by_role("button", name="Login") + expect(login_button).to_be_visible(timeout=1000) + login_button.click() + expect(page).to_have_url(re.compile(r"beam#/"), timeout=1000) + yield + + +def open_first_operation_id_from_manufacture(page) -> str: + """Navigate Home -> Manufacture -> Work Order -> first Operation. + + Returns: + str: operation_id + """ + page.get_by_text("Manufacture").click() + expect(page).to_have_url(re.compile(r"#/manufacture"), timeout=1000) + + list_item = page.locator("css=.beam_list-item").first + expect(list_item).to_be_visible(timeout=1000) + list_item.click() + expect(page).to_have_url(re.compile(r"#/work_order/[^/]+/?$"), timeout=1000) + + operation_link = page.locator("a[href*='/operation/']").first + expect(operation_link).to_be_visible(timeout=1000) + operation_link.click() + expect(page).to_have_url(re.compile(r"#/work_order/[^/]+/operation/[^/?#]+"), timeout=1000) + + match = re.search(r"#/work_order/([^/]+)/operation/([^/?#]+)", page.url) + assert match, f"Could not parse operation route from URL: {page.url}" + return match.group(2) + + +def hms_to_seconds(value: str) -> int: + hours, minutes, seconds = (int(part) for part in value.split(":")) + return (hours * 3600) + (minutes * 60) + seconds + + +def get_operation_elements(page): + description_box = page.locator("css=.metadata-box").first + timer_text = page.locator("css=.timer-value").first + toggle_button = page.locator(".actions button").first + finish_button = page.get_by_role("button", name="Finish") + return description_box, timer_text, toggle_button, finish_button + + +def confirm_pause_with_qty(page, qty: str = "0"): + qty_input = page.locator(".qty-input-row input").first + expect(qty_input).to_be_visible(timeout=10000) + qty_input.fill(qty) + page.get_by_role("button", name="Confirm").click() + + +def pause_form_is_visible(page) -> bool: + return page.locator(".qty-input-row input").first.is_visible() + + +def ensure_operation_running(page, toggle_button): + current_label = toggle_button.inner_text().strip() + if current_label == "Pause": + return + if current_label != "Start": + pytest.fail(f"Unexpected toggle label before start: {current_label}") + + toggle_button.click() + if pause_form_is_visible(page): + confirm_pause_with_qty(page) + expect(toggle_button).to_contain_text("Start", timeout=10000) + toggle_button.click() + + expect(toggle_button).to_contain_text("Pause", timeout=10000) + + +@pytest.mark.order(1) +def test_operation_details_are_visible(page): + operation_id = open_first_operation_id_from_manufacture(page) + description_box, timer_text, toggle_button, finish_button = get_operation_elements(page) + + expect(description_box).to_be_visible(timeout=1000) + expect(timer_text).to_be_visible(timeout=1000) + expect(toggle_button).to_be_visible(timeout=1000) + expect(finish_button).to_be_visible(timeout=1000) + expect(toggle_button).to_contain_text(re.compile("Start|Pause"), timeout=1000) + + with use_current_db_transaction(): + operation = frappe.get_value( + "Work Order Operation", + operation_id, + ["name", "description"], + as_dict=True, + ) + + assert operation, f"Operation {operation_id} not found" + + if operation.description: + expect(description_box).to_contain_text(operation.description) + + elapsed = timer_text.inner_text().strip() + assert re.fullmatch(r"\d{2}:\d{2}:\d{2}", elapsed), f"Unexpected elapsed time format: {elapsed}" + + +@pytest.mark.order(2) +def test_start_operation_starts_timer_and_toggles_buttons(page): + operation_id = open_first_operation_id_from_manufacture(page) + _, timer_text, toggle_button, _ = get_operation_elements(page) + + expect(toggle_button).to_be_enabled(timeout=1000) + with use_current_db_transaction(): + job_card = frappe.get_value( + "Job Card", + {"operation_id": operation_id}, + ["name", "status"], + as_dict=True, + ) + + assert job_card and job_card.get( + "name" + ), f"Expected a Job Card linked to operation {operation_id}" + + # Determine expected initial label from Job Card status and latest time log + expected_label = "Start" + # check latest time log first — an open time log means the operation is running + last_logs = frappe.get_all( + "Job Card Time Log", + filters={"parent": job_card.get("name")}, + fields=["from_time", "to_time"], + order_by="creation desc", + limit=1, + ) + if last_logs and last_logs[0].get("from_time") and not last_logs[0].get("to_time"): + expected_label = "Pause" + # if the job card is explicitly On Hold, treat as paused (Start) + if job_card.get("status") == "On Hold": + expected_label = "Start" + + page.wait_for_timeout(1000) + with use_current_db_transaction(): + job_card = frappe.get_value( + "Job Card", + {"operation_id": operation_id}, + ["name", "status"], + as_dict=True, + ) + expected_label = "Start" + last_logs = frappe.get_all( + "Job Card Time Log", + filters={"parent": job_card.get("name")}, + fields=["from_time", "to_time"], + order_by="creation desc", + limit=1, + ) + if last_logs and last_logs[0].get("from_time") and not last_logs[0].get("to_time"): + expected_label = "Pause" + if job_card.get("status") == "On Hold": + expected_label = "Start" + label = toggle_button.inner_text().strip() + expect(toggle_button).to_contain_text(expected_label, timeout=1000) + + if label == "Pause": + toggle_button.click() + confirm_pause_with_qty(page) + expect(toggle_button).to_contain_text("Start", timeout=10000) + elif label != "Start": + pytest.fail(f"Unexpected toggle label before start: {label}") + + initial_elapsed = timer_text.inner_text().strip() + assert re.fullmatch(r"\d{2}:\d{2}:\d{2}", initial_elapsed) + + toggle_button.click() + expect(toggle_button).to_be_enabled(timeout=10000) + expect(toggle_button).to_contain_text("Pause", timeout=10000) + + page.wait_for_timeout(1500) + updated_elapsed = timer_text.inner_text().strip() + assert re.fullmatch(r"\d{2}:\d{2}:\d{2}", updated_elapsed) + assert hms_to_seconds(updated_elapsed) > hms_to_seconds(initial_elapsed) + + with use_current_db_transaction(): + job_card_name = frappe.get_value("Job Card", {"operation_id": operation_id}, "name") + + assert job_card_name, f"Expected a Job Card linked to operation {operation_id}" + + +@pytest.mark.order(3) +def test_stop_operation_stops_timer_and_records_time_log(page): + operation_id = open_first_operation_id_from_manufacture(page) + _, timer_text, toggle_button, _ = get_operation_elements(page) + + with use_current_db_transaction(): + job_card_name = frappe.get_value("Job Card", {"operation_id": operation_id}, "name") + initial_logs = frappe.get_all( + "Job Card Time Log", + filters={"parent": job_card_name}, + fields=["name", "from_time", "to_time", "time_in_mins"], + ) + + assert job_card_name, f"Expected a Job Card linked to operation {operation_id}" + initial_closed_logs = len([log for log in initial_logs if log.to_time]) + + ensure_operation_running(page, toggle_button) + + page.wait_for_timeout(1200) + timer_after_start = timer_text.inner_text().strip() + + toggle_button.click() + confirm_pause_with_qty(page) + expect(toggle_button).to_contain_text("Start", timeout=10000) + + page.wait_for_timeout(1500) + timer_after_stop = timer_text.inner_text().strip() + page.wait_for_timeout(1200) + timer_after_wait = timer_text.inner_text().strip() + + assert timer_after_stop == timer_after_wait + + start_sec = hms_to_seconds(timer_after_start) + stop_sec = hms_to_seconds(timer_after_stop) + assert ( + stop_sec >= start_sec - 1 + ), f"Timer decreased unexpectedly: {timer_after_start} -> {timer_after_stop}" + + with use_current_db_transaction(): + final_logs = frappe.get_all( + "Job Card Time Log", + filters={"parent": job_card_name}, + fields=["name", "from_time", "to_time", "time_in_mins"], + ) + + final_closed_logs = len([log for log in final_logs if log.to_time]) + assert final_closed_logs >= initial_closed_logs + 1 + assert any( + log.to_time for log in final_logs + ), "Expected the stopped job card to have a closed time log" diff --git a/beam/tests/mobile/test_manufacture.py b/beam/tests/mobile/test_manufacture.py index 12b1d171..2bfb7a36 100644 --- a/beam/tests/mobile/test_manufacture.py +++ b/beam/tests/mobile/test_manufacture.py @@ -50,7 +50,9 @@ def test_complete_partial_stock_entry(page): # navigate in the following order: Home -> Manufacture -> Work Order page.get_by_text("Manufacture").click() - page.locator("css=.beam_list-item").first.click() + work_order_item = page.locator("css=.beam_list-item").first + expect(work_order_item).to_be_visible(timeout=15000) + work_order_item.click() # get the selected Work Order order_id = page.url.split("/")[-1] @@ -65,9 +67,10 @@ def test_complete_partial_stock_entry(page): # find the first item in the list item = page.locator("css=.box .beam_list-item").first + expect(item).to_be_visible(timeout=15000) item_code, *others = item.inner_text().split("\n") item_count = page.locator("css=.box .beam_item-count").first - expect(item_count).to_have_text(re.compile("0/")) + expect(item_count).to_have_text(re.compile("0/"), timeout=15000) assert item_code == "Butter" @@ -82,7 +85,7 @@ def test_complete_partial_stock_entry(page): lambda request: request.headers.get("x-frappe-cmd") == "beam.beam.scan.scan" ): page.evaluate("barcode => scanner.simulate(window, barcode)", barcodes[0]) - expect(item_count).to_have_text(re.compile("1/")) + expect(item_count).to_have_text(re.compile("1/"), timeout=15000) # check that a draft Stock Entry is created page.get_by_text("SAVE", exact=True).click() diff --git a/beam/tests/mobile/test_mobile.py b/beam/tests/mobile/test_mobile.py index de57efa8..c7648ed1 100644 --- a/beam/tests/mobile/test_mobile.py +++ b/beam/tests/mobile/test_mobile.py @@ -21,13 +21,16 @@ def test_scan_item_barcode(page, route): # navigate in the following order: Home -> List -> Form page.get_by_text(route).click() - page.locator("css=.beam_list-item").first.click() + list_item = page.locator("css=.beam_list-item").first + expect(list_item).to_be_visible(timeout=15000) + list_item.click() # find the first item in the list item = page.locator("css=.box .beam_list-item").first + expect(item).to_be_visible(timeout=15000) item_name, *others = item.inner_text().split("\n") item_count = page.locator("css=.box .beam_item-count").first - expect(item_count).to_have_text(re.compile("0/")) + expect(item_count).to_have_text(re.compile("0/"), timeout=15000) # ensure that the item has barcodes barcodes = frappe.get_all( @@ -40,4 +43,4 @@ def test_scan_item_barcode(page, route): lambda request: request.headers.get("x-frappe-cmd") == "beam.beam.scan.scan" ): page.evaluate("barcode => scanner.simulate(window, barcode)", barcodes[0]) - expect(item_count).to_have_text(re.compile("1/")) + expect(item_count).to_have_text(re.compile("1/"), timeout=15000) diff --git a/beam/tests/mobile/test_receive.py b/beam/tests/mobile/test_receive.py index fac92732..069d0217 100644 --- a/beam/tests/mobile/test_receive.py +++ b/beam/tests/mobile/test_receive.py @@ -22,7 +22,9 @@ @pytest.mark.order(2) def test_scan_invalid_barcode(page): page.get_by_text("Receive").click() - page.locator("css=.beam_list-item").first.click() + receive_item = page.locator("css=.beam_list-item").first + expect(receive_item).to_be_visible(timeout=15000) + receive_item.click() # get the selected Purchase Order parsed_url = urlparse(page.url.replace("#", "")) @@ -32,6 +34,7 @@ def test_scan_invalid_barcode(page): # find all items in the list all_item_counts = page.locator("css=.box .beam_item-count") + expect(all_item_counts.first).to_be_visible(timeout=15000) # get all item counts before scanning invalid barcode initial_counts = [] @@ -83,7 +86,9 @@ def test_receive_without_scanning(page): """Test trying to receive without scanning any items""" # navigate to a Purchase Order page.get_by_text("Receive").click() - page.locator("css=.beam_list-item").first.click() + po_item = page.locator("css=.beam_list-item").first + expect(po_item).to_be_visible(timeout=15000) + po_item.click() # get the selected Purchase Order parsed_url = urlparse(page.url.replace("#", "")) @@ -92,6 +97,7 @@ def test_receive_without_scanning(page): assert order_id item = page.locator("css=.box .beam_list-item").first + expect(item).to_be_visible(timeout=15000) item_code, *others = item.inner_text().split("\n") # find all items in the list @@ -136,7 +142,9 @@ def test_receive_without_scanning(page): def test_complete_partial_receipt(page): # navigate in the following order: Home -> Receive -> Purchase Order page.get_by_text("Receive").click() - page.locator("css=.beam_list-item").first.click() + po_item = page.locator("css=.beam_list-item").first + expect(po_item).to_be_visible(timeout=15000) + po_item.click() # get the selected Purchase Order # NOTE: URL format changed: the id lives in the path after the hash (e.g. #/purchase-receipt/PUR-ORD-...) @@ -150,9 +158,10 @@ def test_complete_partial_receipt(page): # find the first item in the list item = page.locator("css=.box .beam_list-item").first + expect(item).to_be_visible(timeout=15000) item_code, *others = item.inner_text().split("\n") item_count = page.locator("css=.box .beam_item-count").first - expect(item_count).to_have_text(re.compile("0/")) + expect(item_count).to_have_text(re.compile("0/"), timeout=15000) assert item_code == "Cloudberry" @@ -218,7 +227,9 @@ def test_rapid_barcode_scanning(page): """Test scanning multiple barcodes quickly""" # navigate to a Purchase Order page.get_by_text("Receive").click() - page.locator("css=.beam_list-item").first.click() + po_item = page.locator("css=.beam_list-item").first + expect(po_item).to_be_visible(timeout=15000) + po_item.click() # get the selected Purchase Order parsed_url = urlparse(page.url.replace("#", "")) @@ -228,9 +239,10 @@ def test_rapid_barcode_scanning(page): # find the first item in the list item = page.locator("css=.box .beam_list-item").first + expect(item).to_be_visible(timeout=15000) item_code, *others = item.inner_text().split("\n") item_count = page.locator("css=.box .beam_item-count").first - expect(item_count).to_have_text(re.compile("0/")) + expect(item_count).to_have_text(re.compile("0/"), timeout=15000) # get barcode for the item with use_current_db_transaction(): diff --git a/beam/tests/setup.py b/beam/tests/setup.py index c17f8369..5973450d 100644 --- a/beam/tests/setup.py +++ b/beam/tests/setup.py @@ -802,10 +802,12 @@ def create_employees(settings, only_create=None): user.email = f"{empl.first_name[0].lower()}{empl.last_name.lower()}@cfc.co" user.first_name = empl.first_name user.last_name = empl.last_name + user.new_password = "admin" user.send_welcome_email = 0 user.enabled = 1 user.language = settings.language user.time_zone = settings.time_zone + user.flags.ignore_password_policy = True for r in employee.get("roles", []): user.append("roles", {"role": r}) diff --git a/beam/www/beam/pages/JobCard.vue b/beam/www/beam/pages/JobCard.vue index 452a0f4d..29e93fcf 100644 --- a/beam/www/beam/pages/JobCard.vue +++ b/beam/www/beam/pages/JobCard.vue @@ -1,6 +1,6 @@