Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
339b680
tests: init job card tests
lauty95 Apr 17, 2026
442116f
feat: job card workflow
lauty95 Apr 17, 2026
6a7aa91
fix: migrate to erpnext functions
lauty95 Apr 29, 2026
ccd8078
feat(tests): Manufacturing User
lauty95 Apr 29, 2026
8ba6070
feat: set current employee
lauty95 Apr 29, 2026
89662c9
feat: job card and job card time log types
lauty95 Apr 29, 2026
88ea0d8
feat: job card save
lauty95 Apr 29, 2026
869e8e3
feat: operation workflow
lauty95 Apr 29, 2026
566431e
doc: how to run backend and frontend test
lauty95 Apr 29, 2026
f92f85e
chore: pytest order scope module
lauty95 Apr 29, 2026
e2d322c
fix: department changed
lauty95 Apr 29, 2026
5257c8e
fix: linters
lauty95 Apr 29, 2026
bdc4474
fix: typing
lauty95 Apr 29, 2026
30553e0
feat: employees with simple password
lauty95 Apr 30, 2026
b6850f9
fix: startJobCard args
lauty95 Apr 30, 2026
314bdc9
test: operation basic tests
lauty95 Apr 30, 2026
e80baa5
fix: linters
lauty95 Apr 30, 2026
d5a1d29
fix: timeouts
lauty95 May 4, 2026
985ef41
feat: description on accordion
lauty95 May 7, 2026
7d7f982
feat: new user
lauty95 May 7, 2026
5253d49
feat: custom endpoints
lauty95 May 7, 2026
116e8e2
feat: locked_by_employee type
lauty95 May 7, 2026
4f80e2c
feat: custom endpoints
lauty95 May 7, 2026
f9ad71e
feat: operation description on an accordion
lauty95 May 7, 2026
3d71dcc
fix: router operationId
lauty95 May 7, 2026
e1d6061
fix: employee validation
lauty95 May 7, 2026
b5e1cfa
fix: path for tests
lauty95 May 7, 2026
f2c1034
fix: validation by pending qty
lauty95 May 7, 2026
a9b0690
fix(tests): new inline input for qty
lauty95 May 7, 2026
4d724a9
fix: linters
lauty95 May 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion beam/beam/demand/sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
155 changes: 155 additions & 0 deletions beam/beam/manufacturing.py
Original file line number Diff line number Diff line change
@@ -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)
52 changes: 43 additions & 9 deletions beam/tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,7 @@
},
"phone": "(704) 885-0542",
"roles": ["Stock Manager", "Item Manager"],
# "department": "Operations",
"department": "Management - APC",
"designation": "Bakery Manager",
},
{
Expand All @@ -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",
},
{
Expand All @@ -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",
},
{
Expand All @@ -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",
},
{
Expand All @@ -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",
},
{
Expand All @@ -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",
},
{
Expand All @@ -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",
},
{
Expand All @@ -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",
},
{
Expand All @@ -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",
},
]
Loading
Loading