Add PO number support and enhance permissions for order management

- Introduced `po` property in `orders_o` for handling Purchase Order (PO) numbers, including API integration for retrieval and validation.
- Enhanced customer permissions to allow limited order editing (`po` updates) and attachment downloads for their own orders.
- Added new helper methods to `users_o` for attributes like `showPricesOnBookingPage` and `usePONumbers`.
- Improved order item listing logic with distinct permissions for customers' own orders and price visibility.
- Implemented numeric value casting in filters within `db_object_t`.
This commit is contained in:
Jeppe Bundgaard
2025-10-22 11:38:35 +02:00
parent b78d9e0e63
commit 30b94e89d7
6 changed files with 117 additions and 5 deletions
@@ -143,6 +143,10 @@ class economic_invoice_draft
$parsed_date = date('d/m/Y H:i', strtotime($order->created_at->value()));
// Add the text line to the draft invoice
self::addTextLine("[ " . $parsed_date . ' ' . $department_name . ' #' . $order->id . " ]");
// If there's a PO number, add it to the invoice
if ($order->po->value() !== '') {
self::addTextLine('PO: ' . $order->po->value());
}
// If there's a reference, add it to the invoice
if ($order->reference->value() !== '') {
self::addTextLine('Reference:');
+12
View File
@@ -34,6 +34,7 @@ class orders_o extends db
public object_property $booking_id;
public object_property $wash_id; // The XL Vask Wash ID, if any
public object_property $lane; // The lane used for the order, if any
public object_property $po; // The (optional) PO number, filled by the customer.
public object_property $using_hand_held; // Whether the order is being processed using a handheld device
/**
@@ -88,6 +89,7 @@ class orders_o extends db
$this->booking_id = new object_property($this->table, $this->id, 'booking_id', 'int', false);
$this->wash_id = new object_property($this->table, $this->id, 'wash_id', 'string', false);
$this->lane = new object_property($this->table, $this->id, 'lane', 'string', false);
$this->po = new object_property($this->table, $this->id, 'po', 'string', false);
$this->using_hand_held = new object_property($this->table, $this->id, 'using_hand_held', 'bool', false);
}
@@ -563,6 +565,7 @@ class orders_o extends db
'booking_id' => (int)$this->booking_id->value(),
'wash_id' => $this->wash_id->value(),
'lane' => $this->lane->value(),
'po' => $this->po->value(),
'closed_at' => (int)$this->invoice_collection_id->value() ? (new collected_order_invoices_o())->select((int)$this->invoice_collection_id->value())->closed_at->value() : null,
'pending_handheld' => $this->isPendingHandheld(),
];
@@ -1334,4 +1337,13 @@ class orders_o extends db
return $post_discount;
}, $order_items));
}
/**
* @throws Exception
*/
public function isOwnOrder(int $customer_id): bool
{
self::requireSelected();
return (int)$this->customer_id->value() === $customer_id;
}
}
+27
View File
@@ -460,6 +460,24 @@ class users_o extends db
return $this->doesUserHaveAttribute('restrictTankCleaning');
}
/**
* If the customer is shown prices on the booking page
* @return bool
*/
public function showPricesOnBookingPage(): bool
{
return $this->doesUserHaveAttribute('showPricesOnBookingPage');
}
/**
* If the customer uses PO numbers
* @return bool
*/
public function usePONumbers(): bool
{
return $this->doesUserHaveAttribute('usePONumbers');
}
public function includeIncludes(array $includes = []): users_o
{
global /** @var response $response */
@@ -519,6 +537,15 @@ class users_o extends db
while ($row = $result->fetch_assoc()) {
$perms[] = $row['permission'];
}
// Add the attributes as `has_attribute_key_{attribute_key}`
$attributes = array_map(function ($attribute_arr) {
return $attribute_arr['attribute'];
}, (new users_o())->getUserAttributes($this->id));
// Remove all duplicates
$attributes = array_unique($attributes, SORT_REGULAR);
foreach ($attributes as $attribute) {
$perms[] = 'has_attribute_' . $attribute;
}
$this->permissions = $perms;
}
+15 -2
View File
@@ -93,9 +93,18 @@ class orderItemsRoute
$this->get('/order/items', function () {
// Require the user to be logged in
global $response;
$this->requirePermission('list_order_items');
// Get the user object
// Check if the user is requesting their own order items
$isCustomerAccess = ($this->hasPermission('user') && !($this->hasPermission('list_order_items')));
$user = (new authentication())->get_user();
if ($isCustomerAccess) {
$hasPermission = $this->hasPermission('list_own_order_items');
$hasAttribute = $user->showPricesOnBookingPage(); // Check if the user has the attribute to show prices on the booking page
if (!$hasPermission && !$hasAttribute) {
$response->error('You do not have permission to list order items, neither your own nor all orders', 403);
}
} else {
$this->requirePermission('list_order_items');
}
// Check if the request was successful
if ($user) {
// Get the post data
@@ -113,6 +122,10 @@ class orderItemsRoute
$response->error('Order not found', 404);
}
$order = (new orders_o())->getOrderById((int)$data['order_id']);
// If the user is requesting their own order items, check if the order belongs to them
if ($isCustomerAccess && !$order->isOwnOrder((int)$user->customer_number->value())) {
$response->error('Order does not belong to the user', 400);
};
// Apply the departments unique pricing
$orderItems = $order->getOrderItems($order->id);
$orderItems = $order->applyDepartmentPrices($orderItems, $order->department_id->value());
+54 -3
View File
@@ -93,6 +93,8 @@ class ordersRoute
$order['user_id'] = (int)$tmp_customer->id;
$order['pending_handheld'] = $order_obj->isPendingHandheld();
$order['attachments'] = $order_obj->listAttachments();
$order['po'] = $order['po'] ?? null;
$order['lane'] = $order['lane'] ?? null;
/** @var array $order */
return $order;
},
@@ -208,7 +210,13 @@ class ordersRoute
$this->put('/orders', function () {
// Require the user to be logged in
global $response;
$this->requirePermission('edit_order');
// Check if the user has permission to partially edit the order
$isCustomerAccess = ((new authentication())->get_user()->hasPermission('user') && !((new authentication())->get_user()->hasPermission('edit_order')));
if (!$isCustomerAccess) {
$this->requirePermission('edit_order');
} else {
$this->requirePermission('user'); // This is used to allow the user to edit their own order
}
// Get the user object
$user = (new authentication())->get_user();
// Check if the request was successful
@@ -225,6 +233,33 @@ class ordersRoute
if (!$order->exists()) {
$response->error('Order not found', 400);
}
// Check if the user has customer edit access
if ($isCustomerAccess) {
if ($order->customer_id->value() !== $user->customer_number->value()) {
$response->error('You do not have permission to edit this order', 400);
}
// Allowed to edit list
$allowed_to_edit = [
// Include the order ID (Even though it is not editable)
'id',
'po',
];
// Check if the $data contains any non-allowed keys
foreach ($data as $key => $value) {
if (!in_array($key, $allowed_to_edit)) {
$response->error('You do not have permission to edit this order field (key: ' . $key . ')', 400);
break;
}
};
// PO
if (isset($data['po'])) {
$order->po->set((string)$data['po']);
}
$order->objectChanged();
// Return a success message
$response->success($order->asArray());
}
/** Departmental access */
// If the customer ID is set, validate it
if (isset($data['customer_id'])) {
if (!(new users_o())->getCustomerByIdOrCustomerNumber((int)$data['customer_id'])->exists() || empty($data['customer_id'])) {
@@ -252,6 +287,10 @@ class ordersRoute
if (isset($data['reg_3'])) {
$order->reg_3->set($data['reg_3']);
}
// If the PO is set, validate it
if (isset($data['po'])) {
$order->po->set((string)$data['po']);
}
// If the lane is set, validate it
if (isset($data['lane'])) {
$order->lane->set((int)$data['lane']);
@@ -338,7 +377,12 @@ class ordersRoute
$this->get('/orders/attachments/download', function () {
// Require the user to be logged in
global $response;
$this->requirePermission('download_order_attachments');
$isCustomerAccess = ($this->hasPermission('user') && !($this->hasPermission('download_order_attachments')));
if (!$isCustomerAccess) {
$this->requirePermission('download_order_attachments');
} else {
$this->requirePermission('download_order_attachments_own');
}
// Get the user object
$user = (new authentication())->get_user();
// Check if the request was successful
@@ -362,6 +406,12 @@ class ordersRoute
if (!$order->exists()) {
$response->error('Order not found', 400);
}
// Check if the user has customer edit access
if ($isCustomerAccess) {
if ($order->isOwnOrder($user->customer_number->value()) === false) {
$response->error('You do not have permission to download this order', 400);
}
}
// Get the attachment
$attachment = $order->getAttachment((int)$attachment_id);
if (!$attachment->exists()) {
@@ -387,7 +437,8 @@ class ordersRoute
}
},
[
'download_order_attachments' => 'Download attachments for an order'
'download_order_attachments' => 'Download attachments for an order',
'download_order_attachments_own' => 'Download attachments for an order (Only for own orders)'
]
);
@@ -728,6 +728,11 @@ trait db_object_t
}
continue;
}
// If the values can be changed to integers, convert them to integers
if (is_numeric($value) && is_numeric($filters[$field])) {
$value = (int)$value;
$filters[$field] = (int)$filters[$field];
}
// Check if the user tries to search for a different value than the one provided
if ($filters[$field] !== $value) {
throw new Exception('Invalid filter: ' . $field . ' - ' . $filters[$field] . '. Permitted value: ' . $value);