Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
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 @@ -55,7 +55,7 @@ repos:
- tomli

- repo: https://github.com/agritheory/test_utils
rev: v1.0.0
rev: v1.14.0
hooks:
- id: update_pre_commit_config
- id: validate_copyright
Expand Down
28 changes: 19 additions & 9 deletions beam/beam/demand/receiving.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Copyright (c) 2024, AgriTheory and contributors
# For license information, please see license.txt

from typing import TYPE_CHECKING, Optional, Union
from typing import TYPE_CHECKING, Optional

import frappe
from frappe.query_builder import DocType
Expand All @@ -21,9 +21,6 @@
if TYPE_CHECKING:
from sqlite3 import Cursor

from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import PurchaseInvoice
from erpnext.buying.doctype.purchase_order.purchase_order import PurchaseOrder


def _get_receiving_demand(
name: str | None = None, item_code: str | None = None
Expand Down Expand Up @@ -139,13 +136,24 @@ def _get_receiving_demand(


@validate_demand_enabled
def modify_receiving(
doc: Union["PurchaseOrder", "PurchaseInvoice"], method: str | None = None
) -> None:
def modify_receiving(doc, method: str | None = None) -> None:
"""Update receiving table for Purchase Orders, Purchase Invoices, and Purchase Receipts"""
if method == "on_submit":
add_receiving(doc.name)
if doc.doctype == "Purchase Receipt":
purchase_orders = {item.purchase_order for item in doc.items if item.purchase_order}
for po_name in purchase_orders:
remove_receiving(po_name)
add_receiving(po_name)
else:
add_receiving(doc.name)
elif method == "on_cancel":
remove_receiving(doc.name)
if doc.doctype == "Purchase Receipt":
purchase_orders = {item.purchase_order for item in doc.items if item.purchase_order}
for po_name in purchase_orders:
remove_receiving(po_name)
add_receiving(po_name)
else:
remove_receiving(doc.name)


def add_receiving(name: str) -> None:
Expand Down Expand Up @@ -282,6 +290,8 @@ def get_receiving_demand(*args, **kwargs) -> list[Receiving]:
for r_filter in r_filters:
receiving_query = receiving_query.where(*r_filter)

receiving_query = receiving_query.where(receiving.received_qty < receiving.stock_qty)

record_offset = records_per_page * (page - 1)

query = f"{receiving_query} LIMIT {records_per_page} OFFSET {record_offset}"
Expand Down
4 changes: 4 additions & 0 deletions beam/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@
"on_submit": ["beam.beam.demand.receiving.modify_receiving"],
"on_cancel": ["beam.beam.demand.receiving.modify_receiving"],
},
"Purchase Receipt": {
"on_submit": ["beam.beam.demand.receiving.modify_receiving"],
"on_cancel": ["beam.beam.demand.receiving.modify_receiving"],
},
"Purchase Invoice": {
"on_submit": [
"beam.beam.demand.receiving.modify_receiving",
Expand Down
76 changes: 76 additions & 0 deletions beam/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,81 @@ def create_beam_mobile_user_role():
role.insert(ignore_permissions=True)


def setup_beam_mobile_user_permissions():
"""Grant necessary permissions to BEAM Mobile User role for mobile app functionality"""
role = "BEAM Mobile User"

# Core doctypes - READ only (for reference data)
read_only_doctypes = [
"Address",
"Contact",
"Company",
"Currency",
"Customer",
"Supplier",
"Item",
"Warehouse",
"UOM",
"Price List",
"Item Price",
"Batch",
"Serial No",
"Item Group",
"Brand",
"UOM Conversion Detail",
]

# Source doctypes - READ only (for mapping to new documents)
source_doctypes = [
"Purchase Order",
"Sales Order",
"Work Order",
]

# Target doctypes - Full CRUD permissions
crud_doctypes = [
"Purchase Receipt",
"Delivery Note",
"Stock Entry",
]

# Add READ permissions
for doctype in read_only_doctypes + source_doctypes:
if not frappe.db.exists("Custom DocPerm", {"parent": doctype, "role": role, "permlevel": 0}):
frappe.get_doc(
{
"doctype": "Custom DocPerm",
"parent": doctype,
"parenttype": "DocType",
"parentfield": "permissions",
"role": role,
"read": 1,
"permlevel": 0,
}
).insert(ignore_permissions=True)

# Add CRUD permissions for transactional doctypes
for doctype in crud_doctypes:
if not frappe.db.exists("Custom DocPerm", {"parent": doctype, "role": role, "permlevel": 0}):
frappe.get_doc(
{
"doctype": "Custom DocPerm",
"parent": doctype,
"parenttype": "DocType",
"parentfield": "permissions",
"role": role,
"read": 1,
"write": 1,
"create": 1,
"submit": 1,
"cancel": 1,
"permlevel": 0,
}
).insert(ignore_permissions=True)

frappe.db.commit()


def after_install():
load_customizations()
print("Setting up Handling Unit Inventory Dimension")
Expand Down Expand Up @@ -63,4 +138,5 @@ def after_install():
build_demand_allocation_map()
reset_build_receiving_map()
create_beam_mobile_user_role()
setup_beam_mobile_user_permissions()
execute()
106 changes: 106 additions & 0 deletions beam/tests/mobile/test_attach_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Copyright (c) 2024, AgriTheory and contributors
# For license information, please see license.txt

import frappe
import pytest
from playwright.sync_api import expect

from beam.tests.test_utils import use_current_db_transaction


@pytest.mark.order(10)
def test_upload_photo_to_purchase_receipt(page, setup):
page.add_init_script(
"""
Object.defineProperty(navigator, 'mediaDevices', {
value: {
getUserMedia: async (constraints) => {
const canvas = document.createElement('canvas');
canvas.width = 640;
canvas.height = 480;
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'blue';
ctx.fillRect(0, 0, 640, 480);
return canvas.captureStream(30);
}
},
writable: false,
configurable: true
});
"""
)

page.get_by_text("Receive").click()
page.wait_for_url("**/beam#/receive")
page.wait_for_timeout(1000)

page.locator("css=.beam_list-item").first.click()
page.wait_for_timeout(1000)

order_id = page.url.split("/")[-1]
assert order_id

camera_button = page.locator("button:has-text('Take photo')")
expect(camera_button).to_be_visible()

camera_button.click()
page.wait_for_timeout(1000)

video_element = page.locator("video.camera-video")
expect(video_element).to_be_visible()

# Click capture button to take photo
capture_button = page.locator(".capture-btn")
capture_button.click()
page.wait_for_timeout(1000)

# Verify photo preview appears
photo_preview = page.locator(".photos-preview .photo-item")
expect(photo_preview).to_have_count(1)

item = page.locator("css=.box .beam_list-item").first
item_code = item.inner_text().split("\n")[0]

with use_current_db_transaction():
barcodes = frappe.get_all(
"Item Barcode", filters={"parenttype": "Item", "parent": item_code}, pluck="barcode"
)

assert len(barcodes) > 0, f"No barcodes found for item {item_code}"

# Scan the barcode to add item to receipt
page.evaluate("barcode => scanner.simulate(window, barcode)", barcodes[0])
page.wait_for_timeout(1000)

# Click SAVE button to create Purchase Receipt with photo
save_button = page.locator("button:has-text('SAVE')")
save_button.click()
page.wait_for_timeout(1500)

with use_current_db_transaction():
receipts = frappe.get_all(
"Purchase Receipt",
filters={"docstatus": 0},
fields=["name"],
order_by="creation desc",
limit=1,
)

assert len(receipts) > 0, "No Purchase Receipt was created"
receipt_name = receipts[0]["name"]

with use_current_db_transaction():
files = frappe.get_all(
"File",
filters={"attached_to_doctype": "Purchase Receipt", "attached_to_name": receipt_name},
fields=["name", "file_name", "file_url"],
)

assert len(files) == 1, f"Expected 1 attached file, found {len(files)}"
assert files[0]["file_name"].startswith(
"photo_"
), f"File name should start with 'photo_', got {files[0]['file_name']}"
assert files[0]["file_name"].endswith(
".jpg"
), f"File should be a .jpg, got {files[0]['file_name']}"
assert files[0]["file_url"], "File URL should not be empty"
103 changes: 103 additions & 0 deletions beam/tests/mobile/test_camera.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Copyright (c) 2024, AgriTheory and contributors
# For license information, please see license.txt

import pytest
from playwright.sync_api import expect


@pytest.mark.order(7)
def test_camera_button_visible(page, setup):
page.add_init_script(
"""
Object.defineProperty(navigator, 'mediaDevices', {
value: {
enumerateDevices: async () => {
return [
{
kind: 'videoinput',
deviceId: 'mock-camera-1',
label: 'Mock Camera',
groupId: 'mock-group'
}
];
},
getUserMedia: async (constraints) => {
const canvas = document.createElement('canvas');
canvas.width = 640;
canvas.height = 480;
return canvas.captureStream(30);
}
},
});
"""
)

page.get_by_text("Receive").click()
page.locator("css=.beam_list-item").first.click()

page.wait_for_timeout(1000)

camera_button = page.locator("button:has-text('Take photo')")
expect(camera_button).to_be_visible()


@pytest.mark.order(8)
def test_camera_component_hidden(page, setup):
page.add_init_script(
"""
Object.defineProperty(navigator, 'mediaDevices', {
value: undefined,
writable: false,
configurable: true
});
"""
)

page.get_by_text("Receive").click()
page.locator("css=.beam_list-item").first.click()

page.wait_for_timeout(1000)

camera_component = page.locator(".camera-component")
expect(camera_component).to_have_count(0)


@pytest.mark.order(9)
def test_camera_opens_and_closes(page, setup):
page.add_init_script(
"""
Object.defineProperty(navigator, 'mediaDevices', {
value: {
getUserMedia: async (constraints) => {
const canvas = document.createElement('canvas');
canvas.width = 640;
canvas.height = 480;
return canvas.captureStream(30);
}
},
writable: false,
configurable: true
});
"""
)

page.get_by_text("Receive").click()
page.locator("css=.beam_list-item").first.click()

page.wait_for_timeout(1000)

camera_button = page.locator("button:has-text('Take photo')")
camera_button.click()
page.wait_for_timeout(1000)

video_element = page.locator("video.camera-video")
expect(video_element).to_be_visible()

close_camera_button = page.locator(".camera-btn:has-text('Close')")
expect(close_camera_button).to_be_visible()

close_camera_button.click()
page.wait_for_timeout(1000)

expect(close_camera_button).not_to_be_visible()
expect(camera_button).to_be_visible()
1 change: 1 addition & 0 deletions beam/tests/mobile/test_manufacture.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def test_complete_partial_stock_entry(page):

# navigate in the following order: Home -> Manufacture -> Work Order
page.get_by_text("Manufacture").click()
page.wait_for_url("**/beam#/manufacture")
page.locator("css=.beam_list-item").first.click()

# get the selected Work Order
Expand Down
2 changes: 2 additions & 0 deletions beam/tests/mobile/test_receive.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ def test_rapid_barcode_scanning(page):
order_id = path_parts[-1] if path_parts else None
assert order_id

page.wait_for_timeout(1500)

# find the first item in the list
item = page.locator("css=.box .beam_list-item").first
item_code, *others = item.inner_text().split("\n")
Expand Down
Loading
Loading