3 コミット

作成者 SHA1 メッセージ 日付
  Harry Groves e51e48969a Delete obsolete demo data seeder 3週間前
  Harry Groves e28df6afee Anonymise full prod db copy instead of seeding demo data 3週間前
  Harry Groves 4867f4d70e EN support for scheduling page 3週間前
34個のファイルの変更、1251行の追加、2191行の削除
分割表示
  1. +18
    -21
      docs/DEMO_BRANCH_SETUP.md
  2. +947
    -0
      scripts/anonymize_demo_copy.sql
  3. +0
    -94
      src/main/java/com/ffii/fpsms/modules/common/demo/DemoCoverageSeeder.kt
  4. +2
    -304
      src/main/java/com/ffii/fpsms/modules/common/demo/DemoDateKeeper.kt
  5. +26
    -0
      src/main/java/com/ffii/fpsms/modules/common/demo/DemoFloors.kt
  6. +13
    -1043
      src/main/java/com/ffii/fpsms/modules/common/demo/DemoSandboxSeeder.kt
  7. +2
    -1
      src/main/java/com/ffii/fpsms/modules/common/internalSetup/SetupController.kt
  8. +28
    -27
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt
  9. +21
    -19
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoFloorSupplierSettingsService.kt
  10. +3
    -2
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderAssignmentService.kt
  11. +7
    -6
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderQueryService.kt
  12. +16
    -19
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt
  13. +38
    -37
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt
  14. +7
    -6
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchDopoAssignmentService.kt
  15. +22
    -21
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt
  16. +10
    -9
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt
  17. +5
    -4
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/TruckRoutingSummaryService.kt
  18. +5
    -4
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/WorkbenchTruckRoutingSummaryService.kt
  19. +2
    -1
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/AssignByStoreRequest.kt
  20. +4
    -26
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchPickConstants.kt
  21. +12
    -11
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt
  22. +2
    -1
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt
  23. +2
    -1
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt
  24. +2
    -4
      src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt
  25. +3
    -2
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/HierarchicalFgPayloadAssembler.kt
  26. +13
    -12
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt
  27. +2
    -1
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckLaneScheduleExcelSupport.kt
  28. +17
    -16
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckService.kt
  29. +2
    -1
      src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt
  30. +2
    -1
      src/main/java/com/ffii/fpsms/modules/report/service/StockTakeVarianceReportService.kt
  31. +11
    -10
      src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt
  32. +3
    -2
      src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotWorkbenchService.kt
  33. +2
    -2
      src/main/resources/application-db-demo.yml
  34. +2
    -483
      src/main/resources/demo/demo-coverage.sql

+ 18
- 21
docs/DEMO_BRANCH_SETUP.md ファイルの表示

@@ -1,6 +1,6 @@
# Demo branch setup (`debrand`)

This branch is a white-label demo. It uses a separate database, `fpsms_demo`, filled with fictional data. It must not read or write the live company database `fpsmsdb`, and it must not call the live M18 system.
This branch is a white-label demo. `fpsms_demo` is a full copy of the live database `fpsmsdb`, then rewritten in place so names, codes, and other identifying values become placeholders. Row counts, ids, statuses, quantities, and dates stay. It must not read or write `fpsmsdb`, and it must not call the live M18 system.

Check out `debrand` in both repositories:

@@ -21,36 +21,36 @@ Do not merge this branch. Do not start it with `db-local`, `prod`, or `db-toa`.
| Also | `demo.stock` / `Demo2026!` (倉務), `demo.prod` / `Demo2026!` (出品) |


`demo` has ADMIN, STOCK, PROD, PACK, and REPORT_MGMT. The UI language switch still works. Item names and other master data are stored in Chinese.

On the report page, leave the dates empty for most downloads. For 「庫存盤點報告」 choose round 「示範盤點輪次」. For 「送貨訂單與倉存單位不符報告」 use today’s date.
`demo` is an existing admin user, renamed. Every copied password is reset to `Demo2026!`. `demo.stock` and `demo.prod` are renamed only when a stock-only user and a production-only user already exist. Floors are `A/F` (was 2/F) and `B/F` (was 4/F). Item codes look like `ITEM-12`, shops like `SHOP-3`, lots like `LOT-9`.

## 1. Stop anything already on port 8090

The frontend on this branch calls `http://localhost:8090/api`. If a normal backend is already listening there, it is the live database. Stop that process before starting the demo.

## 2. Copy the table structure into `fpsms_demo`
## 2. Copy production into `fpsms_demo`, then replace identifying values

Liquibase changesets in this repo name the schema `fpsmsdb`. Running them against an empty database, as `root`, would change the live database. Do not do that.
Liquibase changesets in this repo name the schema `fpsmsdb`. Do not run them against the live database.

Use the local `root` account from `application-db-local.yml` only for this one-time copy. Create an empty schema, load the structure (no rows), then copy the Liquibase history so startup treats the schema as already up to date.
Use the local `root` account from `application-db-local.yml` only for this copy. Load structure and rows, including Liquibase history, then run the anonymize script. The script updates existing rows. It does not delete them. It refuses to run unless the current database is `fpsms_demo`, and it refuses to run again after `settings.name = DEMO.anonymized.v1` is set.

```bash
mysql -h127.0.0.1 -P3306 -uroot -p -e \
"CREATE DATABASE IF NOT EXISTS fpsms_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"

mysqldump --no-data --routines --triggers -h127.0.0.1 -P3306 -uroot -p fpsmsdb \
> /tmp/fpsms_demo_schema.sql
"DROP DATABASE IF EXISTS fpsms_demo; CREATE DATABASE fpsms_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"

mysql -h127.0.0.1 -P3306 -uroot -p fpsms_demo < /tmp/fpsms_demo_schema.sql
mysqldump --routines --triggers -h127.0.0.1 -P3306 -uroot -p fpsmsdb \
> /tmp/fpsms_demo_copy.sql

mysqldump -h127.0.0.1 -P3306 -uroot -p fpsmsdb DATABASECHANGELOG DATABASECHANGELOGLOCK \
> /tmp/fpsms_demo_changelog.sql
mysql -h127.0.0.1 -P3306 -uroot -p fpsms_demo < /tmp/fpsms_demo_copy.sql

mysql -h127.0.0.1 -P3306 -uroot -p fpsms_demo < /tmp/fpsms_demo_changelog.sql
mysql -h127.0.0.1 -P3306 -uroot -p fpsms_demo \
< scripts/anonymize_demo_copy.sql
```

`fpsms_demo` should now have tables and Liquibase history, and no company rows.
In MySQL Workbench the default query timeout is 30 seconds. A full copy takes longer, and Workbench then reports error 2013 even though the server may still be working. Before running the script: **MySQL Workbench → Settings → SQL Editor → DBMS connection read timeout interval** = `3600`. Reconnect, select `fpsms_demo`, and run the whole file.

Dropping `fpsms_demo` removes a previous demo copy only. Do not point that `DROP` at `fpsmsdb`.

After the script, a sample of `items.code`, `shop.name`, `inventory_lot.lotNo`, `user.username`, and floor `store_id` values should be placeholders, and `SELECT COUNT(*)` on those tables should match the production copy. Every user password is `Demo2026!`.

## 3. Create a login that cannot see `fpsmsdb`

@@ -91,11 +91,7 @@ JDK 17. From `FPSMS-backend`, on branch `debrand`:
./gradlew bootRun --args='--spring.profiles.active=db-demo'
```

The first boot inserts the fictional sandbox (`DemoSandboxSeeder`) and the extra report rows (`src/main/resources/demo/demo-coverage.sql`). Later boots skip both when user `demo` and setting `DEMO.coverage.v1` already exist.

`DemoDateKeeper` then keeps those sample dates current. It runs at startup, every day at 00:10, and when the truck board is opened. It moves `DEMO-` expiry, delivery, order, pick, job, and stock-take dates forward by the number of days since the last run (stored as setting `DEMO.dates.anchor`). Lots stay unexpired, and screens that default to today still show the sample orders. You do not need to re-seed or set the date filter back to the original seed day. Quarterly stock-ledger samples stay in January, April, July, and September, and roll into the new year.

On the first run it also fills 「送貨訂單」 so a search for today returns a row on each tab: 2/F and 4/F (pending, receiving, completed), Truck X, 加單, and 補貨. Those rows use `DEMO-` codes, so the same daily shift keeps their dates on today. The marker is setting `DEMO.do.showcase.v1`.
Startup does not insert sample rows. It refuses to start unless `DEMO.anonymized.v1` is present, so a raw production copy cannot be served. Dates stay on the days they had in the copy.

In the log, expect:

@@ -126,6 +122,7 @@ Sign in as `demo` / `Demo2026!`.
| Can read live schema `fpsmsdb` | The datasource user is too powerful. Recreate `fpsms_demo` as in step 3. Do not bypass the check. |
| Access denied for `fpsms_demo` | The user or password does not match `application-db-demo.yml`, or the grants were not flushed. |
| Liquibase tries to alter `fpsmsdb` | The changelog tables were not copied into `fpsms_demo`. Repeat step 2. With the restricted login this should fail closed, not change live data. |
| has not been anonymized | Run `scripts/anonymize_demo_copy.sql` against `fpsms_demo`. Do not start the app on the raw copy. |
| Port 8090 already in use | Stop the other backend. Do not point the demo frontend at it. |



+ 947
- 0
scripts/anonymize_demo_copy.sql ファイルの表示

@@ -0,0 +1,947 @@
-- Rewrite identifying values in fpsms_demo. Does not delete rows.
-- Ids, foreign keys, statuses, quantities, and dates stay.
--
-- Run only against a disposable full copy of production:
-- mysql -h127.0.0.1 -P3306 -uroot -p fpsms_demo < scripts/anonymize_demo_copy.sql
--
-- Refuses to run unless the current database is fpsms_demo.
-- Refuses to run again once settings.name = DEMO.anonymized.v1 exists.
-- Password for every user, including demo / demo.stock / demo.prod, is Demo2026!.
--
-- MySQL Workbench stops a query after 30 seconds and reports error 2013.
-- Before running: Preferences, SQL Editor, DBMS connection read timeout = 3600.
-- Reconnect, select fpsms_demo, then run this whole file.

SET NAMES utf8mb4;
SET SESSION sql_safe_updates = 0;
SET SESSION group_concat_max_len = 1000000;

DROP PROCEDURE IF EXISTS demo_exec_if_table;
DROP PROCEDURE IF EXISTS demo_apply_map;
DROP PROCEDURE IF EXISTS demo_apply_pair;
DROP PROCEDURE IF EXISTS demo_add_missing;
DROP PROCEDURE IF EXISTS demo_blank_columns;
DROP PROCEDURE IF EXISTS demo_replace_floors;
DROP PROCEDURE IF EXISTS demo_remap_shop_csv;
DROP PROCEDURE IF EXISTS demo_drop_maps;
DROP PROCEDURE IF EXISTS anonymize_demo_copy;

DELIMITER $$

CREATE PROCEDURE demo_drop_maps()
BEGIN
DROP TABLE IF EXISTS _demo_map_item;
DROP TABLE IF EXISTS _demo_map_shop;
DROP TABLE IF EXISTS _demo_map_lot;
DROP TABLE IF EXISTS _demo_map_loc;
DROP TABLE IF EXISTS _demo_map_truck;
DROP TABLE IF EXISTS _demo_map_do;
DROP TABLE IF EXISTS _demo_map_po;
DROP TABLE IF EXISTS _demo_map_jo;
DROP TABLE IF EXISTS _demo_map_pk;
DROP TABLE IF EXISTS _demo_map_si;
DROP TABLE IF EXISTS _demo_map_st;
DROP TABLE IF EXISTS _demo_map_pp;
DROP TABLE IF EXISTS _demo_map_user;
DROP TABLE IF EXISTS _demo_map_username;
DROP TABLE IF EXISTS _demo_map_person;
DROP TABLE IF EXISTS _demo_map_equip;
DROP TABLE IF EXISTS _demo_map_stage;
END$$

CREATE PROCEDURE demo_exec_if_table(IN p_table VARCHAR(64), IN p_sql TEXT)
BEGIN
DECLARE n INT DEFAULT 0;
SELECT COUNT(*) INTO n
FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = p_table;
IF n > 0 THEN
SET @demo_sql = p_sql;
PREPARE demo_stmt FROM @demo_sql;
EXECUTE demo_stmt;
DEALLOCATE PREPARE demo_stmt;
END IF;
END$$

CREATE PROCEDURE demo_apply_map(IN p_table VARCHAR(64), IN p_column VARCHAR(64), IN p_map VARCHAR(64))
BEGIN
DECLARE n INT DEFAULT 0;
SELECT COUNT(*) INTO n
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = p_table
AND column_name = p_column;
IF n > 0 THEN
SET @demo_sql = CONCAT(
'UPDATE `', p_table, '` t INNER JOIN `', p_map, '` m ',
'ON t.`', p_column, '` COLLATE utf8mb4_unicode_ci = m.old_value ',
'SET t.`', p_column, '` = m.new_value'
);
PREPARE demo_stmt FROM @demo_sql;
EXECUTE demo_stmt;
DEALLOCATE PREPARE demo_stmt;
END IF;
END$$

CREATE PROCEDURE demo_apply_pair(
IN p_table VARCHAR(64),
IN p_code VARCHAR(64),
IN p_name VARCHAR(64),
IN p_map VARCHAR(64)
)
BEGIN
DECLARE ncode INT DEFAULT 0;
DECLARE nname INT DEFAULT 0;
SELECT COUNT(*) INTO ncode
FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = p_table AND column_name = p_code;
SELECT COUNT(*) INTO nname
FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = p_table AND column_name = p_name;
IF ncode > 0 AND nname > 0 THEN
SET @demo_sql = CONCAT(
'UPDATE `', p_table, '` t INNER JOIN `', p_map, '` m ',
'ON t.`', p_code, '` COLLATE utf8mb4_unicode_ci = m.old_value ',
'SET t.`', p_code, '` = m.new_value, t.`', p_name, '` = m.new_name'
);
PREPARE demo_stmt FROM @demo_sql;
EXECUTE demo_stmt;
DEALLOCATE PREPARE demo_stmt;
ELSEIF ncode > 0 THEN
CALL demo_apply_map(p_table, p_code, p_map);
END IF;
END$$

CREATE PROCEDURE demo_add_missing(
IN p_map VARCHAR(64),
IN p_table VARCHAR(64),
IN p_column VARCHAR(64),
IN p_prefix VARCHAR(24)
)
demo_add_missing_body: BEGIN
DECLARE n INT DEFAULT 0;
SELECT COUNT(*) INTO n
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = p_table
AND column_name = p_column;
IF n = 0 THEN
LEAVE demo_add_missing_body;
END IF;

DELETE FROM _demo_map_stage;
SET @demo_sql = CONCAT(
'INSERT IGNORE INTO _demo_map_stage (old_value) ',
'SELECT DISTINCT `', p_column, '` FROM `', p_table, '` ',
'WHERE `', p_column, '` IS NOT NULL AND `', p_column, '` <> '''''
);
PREPARE demo_stmt FROM @demo_sql;
EXECUTE demo_stmt;
DEALLOCATE PREPARE demo_stmt;

SET @demo_sql = CONCAT(
'DELETE s FROM _demo_map_stage s INNER JOIN `', p_map, '` m ',
'ON m.old_value = s.old_value'
);
PREPARE demo_stmt FROM @demo_sql;
EXECUTE demo_stmt;
DEALLOCATE PREPARE demo_stmt;

SET @demo_sql = CONCAT(
'INSERT INTO `', p_map, '` (old_value, new_value) ',
'SELECT old_value, CONCAT(''', p_prefix, ''', rn) FROM (',
' SELECT old_value, ROW_NUMBER() OVER (ORDER BY old_value) AS rn FROM _demo_map_stage',
') numbered'
);
PREPARE demo_stmt FROM @demo_sql;
EXECUTE demo_stmt;
DEALLOCATE PREPARE demo_stmt;
END$$

CREATE PROCEDURE demo_blank_columns()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE tname VARCHAR(64);
DECLARE cname VARCHAR(64);
DECLARE cur CURSOR FOR
SELECT table_name, column_name
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND data_type IN ('varchar', 'char', 'text', 'tinytext', 'mediumtext', 'longtext')
AND table_name NOT LIKE '\_demo\_map\_%'
AND table_name NOT IN ('settings', 'DATABASECHANGELOG', 'DATABASECHANGELOGLOCK', 'authority', 'uom_conversion', 'currency')
AND LOWER(column_name) IN (
'remarks', 'remark', 'reason', 'issue_remark', 'errormessage',
'addr1', 'addr2', 'addr3', 'addr4',
'contactno', 'contactemail', 'contactname', 'brno', 'district',
'phone1', 'phone2', 'staffno', 'department', 'title',
'sendermail', 'tomail', 'subject', 'content',
'districtreference', 'countryoforigin',
'dnno', 'invoiceno',
'subjectcht', 'subjecteng', 'contentcht', 'contenteng'
);
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;

OPEN cur;
blank_loop: LOOP
FETCH cur INTO tname, cname;
IF done = 1 THEN
LEAVE blank_loop;
END IF;
SET @demo_sql = CONCAT(
'UPDATE `', tname, '` SET `', cname, '` = '''' WHERE `', cname, '` IS NOT NULL AND `', cname, '` <> '''''
);
PREPARE demo_stmt FROM @demo_sql;
EXECUTE demo_stmt;
DEALLOCATE PREPARE demo_stmt;
END LOOP;
CLOSE cur;
END$$

CREATE PROCEDURE demo_replace_floors()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE tname VARCHAR(64);
DECLARE cname VARCHAR(64);
DECLARE cset VARCHAR(64);
DECLARE bincol VARCHAR(64);
DECLARE cur CURSOR FOR
SELECT table_name, column_name, character_set_name
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND data_type IN ('varchar', 'char', 'text', 'tinytext', 'mediumtext', 'longtext')
AND table_name NOT LIKE '\_demo\_map\_%'
AND table_name NOT IN ('DATABASECHANGELOG', 'DATABASECHANGELOGLOCK', 'authority');
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;

OPEN cur;
floor_loop: LOOP
FETCH cur INTO tname, cname, cset;
IF done = 1 THEN
LEAVE floor_loop;
END IF;
SET bincol = IF(cset = 'utf8mb4', 'utf8mb4_bin', 'utf8mb3_bin');

SET @demo_sql = CONCAT(
'UPDATE `', tname, '` SET `', cname, '` = CASE ',
'WHEN UPPER(TRIM(`', cname, '`)) COLLATE ', bincol, ' = ''2/F'' THEN ''A/F'' ',
'WHEN UPPER(TRIM(`', cname, '`)) COLLATE ', bincol, ' = ''4/F'' THEN ''B/F'' ',
'WHEN UPPER(TRIM(`', cname, '`)) COLLATE ', bincol, ' = ''1/F'' THEN ''C/F'' ',
'WHEN UPPER(TRIM(`', cname, '`)) COLLATE ', bincol, ' = ''3/F'' THEN ''D/F'' ',
'WHEN UPPER(TRIM(`', cname, '`)) COLLATE ', bincol, ' = ''2F'' THEN ''AF'' ',
'WHEN UPPER(TRIM(`', cname, '`)) COLLATE ', bincol, ' = ''4F'' THEN ''BF'' ',
'WHEN UPPER(TRIM(`', cname, '`)) COLLATE ', bincol, ' = ''1F'' THEN ''CF'' ',
'WHEN UPPER(TRIM(`', cname, '`)) COLLATE ', bincol, ' = ''3F'' THEN ''DF'' ',
'ELSE `', cname, '` END ',
'WHERE UPPER(TRIM(`', cname, '`)) COLLATE ', bincol, ' IN (''2/F'',''4/F'',''1/F'',''3/F'',''2F'',''4F'',''1F'',''3F'')'
);
PREPARE demo_stmt FROM @demo_sql;
EXECUTE demo_stmt;
DEALLOCATE PREPARE demo_stmt;

SET @demo_sql = CONCAT(
'UPDATE `', tname, '` SET `', cname, '` = ',
'REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(`', cname, '`, ',
'''-2F-'', ''-AF-''), ''-4F-'', ''-BF-''), ''-1F-'', ''-CF-''), ''-3F-'', ''-DF-''), ',
'''-2/F-'', ''-A/F-''), ''-4/F-'', ''-B/F-''), ''-1/F-'', ''-C/F-''), ''-3/F-'', ''-D/F-'') ',
'WHERE `', cname, '` COLLATE ', bincol, ' LIKE ''%-2F-%'' OR `', cname, '` COLLATE ', bincol, ' LIKE ''%-4F-%'' ',
'OR `', cname, '` COLLATE ', bincol, ' LIKE ''%-1F-%'' OR `', cname, '` COLLATE ', bincol, ' LIKE ''%-3F-%'' ',
'OR `', cname, '` COLLATE ', bincol, ' LIKE ''%-2/F-%'' OR `', cname, '` COLLATE ', bincol, ' LIKE ''%-4/F-%'' ',
'OR `', cname, '` COLLATE ', bincol, ' LIKE ''%-1/F-%'' OR `', cname, '` COLLATE ', bincol, ' LIKE ''%-3/F-%'''
);
PREPARE demo_stmt FROM @demo_sql;
EXECUTE demo_stmt;
DEALLOCATE PREPARE demo_stmt;
END LOOP;
CLOSE cur;
END$$

CREATE PROCEDURE demo_remap_shop_csv(IN p_setting VARCHAR(255))
demo_remap_shop_csv_body: BEGIN
DECLARE done INT DEFAULT 0;
DECLARE v LONGTEXT;
DECLARE oldv VARCHAR(255);
DECLARE newv VARCHAR(255);
DECLARE setting_rows INT DEFAULT 0;
DECLARE cur CURSOR FOR
SELECT old_value, new_value
FROM _demo_map_shop
ORDER BY CHAR_LENGTH(old_value) DESC, old_value;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;

SELECT COUNT(*) INTO setting_rows
FROM settings
WHERE name = p_setting COLLATE utf8mb4_0900_ai_ci;
IF setting_rows = 0 THEN
LEAVE demo_remap_shop_csv_body;
END IF;
SELECT IFNULL(value, '') INTO v
FROM settings
WHERE name = p_setting COLLATE utf8mb4_0900_ai_ci
LIMIT 1;
IF v IS NULL OR v COLLATE utf8mb4_0900_ai_ci = '' THEN
LEAVE demo_remap_shop_csv_body;
END IF;

SET v = CONCAT(',', REPLACE(v, ' ', ''), ',');
OPEN cur;
shop_csv_loop: LOOP
FETCH cur INTO oldv, newv;
IF done = 1 THEN
LEAVE shop_csv_loop;
END IF;
SET v = REPLACE(
v COLLATE utf8mb4_bin,
CONCAT(',', oldv COLLATE utf8mb4_bin, ','),
CONCAT(',', newv COLLATE utf8mb4_bin, ',')
);
END LOOP;
CLOSE cur;
SET v = TRIM(BOTH ',' FROM v);
UPDATE settings
SET value = IFNULL(v, '')
WHERE name = p_setting COLLATE utf8mb4_0900_ai_ci;
END$$

CREATE PROCEDURE anonymize_demo_copy()
BEGIN
DECLARE marker_count INT DEFAULT 0;
DECLARE demo_id BIGINT DEFAULT NULL;
DECLARE stock_id BIGINT DEFAULT NULL;
DECLARE prod_id BIGINT DEFAULT NULL;
DECLARE pwd_hash VARCHAR(80) DEFAULT '$2a$10$9eqxnNGtxFADAW9AbbjRIevr8YWgBqkLr//23U6qYOWs5U9figE2u';

DECLARE CONTINUE HANDLER FOR NOT FOUND SET demo_id = demo_id;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
CALL demo_drop_maps();
RESIGNAL;
END;

IF DATABASE() IS NULL OR DATABASE() <> 'fpsms_demo' THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Refusing to anonymize: current database is not fpsms_demo';
END IF;

SELECT COUNT(*) INTO marker_count FROM settings WHERE name = 'DEMO.anonymized.v1';
IF marker_count > 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Refusing to anonymize: DEMO.anonymized.v1 already exists';
END IF;

CALL demo_drop_maps();

CREATE TABLE _demo_map_stage (
old_value VARCHAR(512) NOT NULL,
PRIMARY KEY (old_value)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE _demo_map_item (
old_value VARCHAR(255) NOT NULL,
new_value VARCHAR(255) NOT NULL,
new_name VARCHAR(255) NOT NULL,
PRIMARY KEY (old_value)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE _demo_map_shop (
old_value VARCHAR(255) NOT NULL,
new_value VARCHAR(255) NOT NULL,
new_name VARCHAR(255) NOT NULL,
PRIMARY KEY (old_value)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE _demo_map_lot (
old_value VARCHAR(512) NOT NULL,
new_value VARCHAR(64) NOT NULL,
PRIMARY KEY (old_value)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE _demo_map_loc (
old_value VARCHAR(512) NOT NULL,
new_value VARCHAR(64) NOT NULL,
PRIMARY KEY (old_value)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE _demo_map_truck (
old_value VARCHAR(255) NOT NULL,
new_value VARCHAR(64) NOT NULL,
PRIMARY KEY (old_value)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE _demo_map_do (
old_value VARCHAR(255) NOT NULL,
new_value VARCHAR(64) NOT NULL,
PRIMARY KEY (old_value)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE _demo_map_po (
old_value VARCHAR(255) NOT NULL,
new_value VARCHAR(64) NOT NULL,
PRIMARY KEY (old_value)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE _demo_map_jo (
old_value VARCHAR(255) NOT NULL,
new_value VARCHAR(64) NOT NULL,
PRIMARY KEY (old_value)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE _demo_map_pk (
old_value VARCHAR(255) NOT NULL,
new_value VARCHAR(64) NOT NULL,
PRIMARY KEY (old_value)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE _demo_map_si (
old_value VARCHAR(255) NOT NULL,
new_value VARCHAR(64) NOT NULL,
PRIMARY KEY (old_value)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE _demo_map_st (
old_value VARCHAR(255) NOT NULL,
new_value VARCHAR(64) NOT NULL,
PRIMARY KEY (old_value)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE _demo_map_pp (
old_value VARCHAR(255) NOT NULL,
new_value VARCHAR(64) NOT NULL,
PRIMARY KEY (old_value)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE _demo_map_equip (
old_value VARCHAR(255) NOT NULL,
new_value VARCHAR(64) NOT NULL,
PRIMARY KEY (old_value)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE _demo_map_user (
user_id BIGINT NOT NULL,
old_username VARCHAR(255) NULL,
new_username VARCHAR(255) NOT NULL,
old_name VARCHAR(255) NULL,
new_name VARCHAR(255) NOT NULL,
PRIMARY KEY (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE _demo_map_username (
old_value VARCHAR(255) NOT NULL,
new_value VARCHAR(255) NOT NULL,
PRIMARY KEY (old_value)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE _demo_map_person (
old_value VARCHAR(255) NOT NULL,
new_value VARCHAR(255) NOT NULL,
PRIMARY KEY (old_value)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

SELECT u.id INTO demo_id
FROM `user` u
JOIN user_authority ua ON ua.userId = u.id
JOIN authority a ON a.id = ua.authId
WHERE a.authority = 'ADMIN'
ORDER BY u.deleted, u.id
LIMIT 1;

IF demo_id IS NULL THEN
SELECT id INTO demo_id FROM `user` ORDER BY deleted, id LIMIT 1;
END IF;

SELECT u.id INTO stock_id
FROM `user` u
WHERE u.id <> IFNULL(demo_id, -1)
AND EXISTS (
SELECT 1 FROM user_authority ua
JOIN authority a ON a.id = ua.authId
WHERE ua.userId = u.id AND a.authority = 'STOCK'
)
AND NOT EXISTS (
SELECT 1 FROM user_authority ua
JOIN authority a ON a.id = ua.authId
WHERE ua.userId = u.id AND a.authority = 'ADMIN'
)
ORDER BY u.deleted, u.id
LIMIT 1;

SELECT u.id INTO prod_id
FROM `user` u
WHERE u.id <> IFNULL(demo_id, -1)
AND u.id <> IFNULL(stock_id, -1)
AND EXISTS (
SELECT 1 FROM user_authority ua
JOIN authority a ON a.id = ua.authId
WHERE ua.userId = u.id AND a.authority = 'PROD'
)
AND NOT EXISTS (
SELECT 1 FROM user_authority ua
JOIN authority a ON a.id = ua.authId
WHERE ua.userId = u.id AND a.authority = 'ADMIN'
)
ORDER BY u.deleted, u.id
LIMIT 1;

INSERT INTO _demo_map_user (user_id, old_username, new_username, old_name, new_name)
SELECT id,
NULLIF(username, ''),
CASE
WHEN id = demo_id THEN 'demo'
WHEN id = stock_id THEN 'demo.stock'
WHEN id = prod_id THEN 'demo.prod'
ELSE CONCAT('USER-', id)
END,
NULLIF(name, ''),
CASE
WHEN id = demo_id THEN 'Demo'
WHEN id = stock_id THEN 'Demo Stock'
WHEN id = prod_id THEN 'Demo Prod'
ELSE CONCAT('User ', id)
END
FROM `user`;

INSERT INTO _demo_map_username (old_value, new_value)
SELECT old_username, MIN(new_username)
FROM _demo_map_user
WHERE old_username IS NOT NULL
GROUP BY old_username;

INSERT INTO _demo_map_person (old_value, new_value)
SELECT old_name, MIN(new_name)
FROM _demo_map_user
WHERE old_name IS NOT NULL
GROUP BY old_name;

INSERT INTO _demo_map_item (old_value, new_value, new_name)
SELECT code, CONCAT('ITEM-', MIN(id)), CONCAT('Item ', MIN(id))
FROM items
WHERE code IS NOT NULL AND code <> ''
GROUP BY code;

INSERT INTO _demo_map_shop (old_value, new_value, new_name)
SELECT code, CONCAT('SHOP-', MIN(id)), CONCAT('Shop ', MIN(id))
FROM shop
WHERE code IS NOT NULL AND code <> ''
GROUP BY code;

INSERT INTO _demo_map_lot (old_value, new_value)
SELECT lotNo, CONCAT('LOT-', MIN(id))
FROM inventory_lot
WHERE lotNo IS NOT NULL AND lotNo <> ''
GROUP BY lotNo;

CALL demo_add_missing('_demo_map_lot', 'stock_in_line', 'lotNo', 'LOT-S');
CALL demo_add_missing('_demo_map_lot', 'stock_in_line', 'productLotNo', 'LOT-P');
CALL demo_add_missing('_demo_map_lot', 'delivery_order_line', 'm18Lot', 'LOT-M');
CALL demo_add_missing('_demo_map_lot', 'stocktakerecord', 'lotNo', 'LOT-T');
CALL demo_add_missing('_demo_map_lot', 'stock_adjustment_record', 'lotNo', 'LOT-A');
CALL demo_add_missing('_demo_map_lot', 'stock_transfer_record', 'lotNo', 'LOT-R');
CALL demo_add_missing('_demo_map_lot', 'item_lot_trace_log', 'lotNo', 'LOT-L');
CALL demo_add_missing('_demo_map_lot', 'bag_lot_line', 'lotNo', 'LOT-B');
CALL demo_add_missing('_demo_map_lot', 'pick_execution_issue', 'lot_no', 'LOT-I');

INSERT INTO _demo_map_loc (old_value, new_value)
SELECT code,
CASE code
WHEN '2F-W202-01-00' THEN 'LOC-HOLD'
WHEN '2F-W200-#A-00' THEN 'LOC-WIP'
ELSE CONCAT('LOC-', MIN(id))
END
FROM warehouse
WHERE code IS NOT NULL AND code <> ''
AND UPPER(REPLACE(code, ' ', '')) NOT IN ('2/F', '2F', '4/F', '4F', '1/F', '1F', '3/F', '3F')
GROUP BY code;

CALL demo_add_missing('_demo_map_loc', 'items', 'LocationCode', 'LOC-I');
CALL demo_add_missing('_demo_map_loc', 'stock_transfer_record', 'startLocation', 'LOC-S');
CALL demo_add_missing('_demo_map_loc', 'stock_transfer_record', 'targetLocation', 'LOC-T');
CALL demo_add_missing('_demo_map_loc', 'pick_execution_issue', 'store_location', 'LOC-P');

DELETE FROM _demo_map_loc
WHERE UPPER(REPLACE(old_value, ' ', '')) IN ('2/F', '2F', '4/F', '4F', '1/F', '1F', '3/F', '3F');

INSERT INTO _demo_map_loc (old_value, new_value) VALUES
('2F-W202-01-00', 'LOC-HOLD'),
('2F-W200-#A-00', 'LOC-WIP'),
('4F-W402-01-00', 'LOC-X01'),
('4F-W402-02-00', 'LOC-X02'),
('4F-W402-03-00', 'LOC-X03'),
('4F-W402-04-00', 'LOC-X04'),
('4F-W402-05-00', 'LOC-X05'),
('4F-W402-#A-00', 'LOC-X06'),
('4F-W402-#B-00', 'LOC-X07'),
('4F-W402-#C-00', 'LOC-X08'),
('4F-W402-#D-00', 'LOC-X09'),
('4F-W402-#E-00', 'LOC-X10'),
('4F-W402-#F-00', 'LOC-X11'),
('4F-W402-#G-00', 'LOC-X12'),
('4F-W402-#H-00', 'LOC-X13'),
('4F-W402-#I-00', 'LOC-X14'),
('4F-W402-#J-00', 'LOC-X15'),
('4F-W402-#K-00', 'LOC-X16'),
('4F-W402-#L-00', 'LOC-X17'),
('4F-W402-#M-00', 'LOC-X18'),
('4F-W402-#N-00', 'LOC-X19'),
('4F-W402-#O-00', 'LOC-X20'),
('4F-W402-#P-00', 'LOC-X21'),
('4F-W402-#Q-00', 'LOC-X22'),
('4F-W402-#R-00', 'LOC-X23'),
('4F-W402-#S-00', 'LOC-X24')
ON DUPLICATE KEY UPDATE new_value = VALUES(new_value);

INSERT INTO _demo_map_truck (old_value, new_value)
SELECT TruckLanceCode, CONCAT('TRUCK-', MIN(id))
FROM truck
WHERE TruckLanceCode IS NOT NULL AND TruckLanceCode <> ''
GROUP BY TruckLanceCode;

CALL demo_add_missing('_demo_map_truck', 'truck_lane_version', 'truckLanceCode', 'TRUCK-V');
CALL demo_add_missing('_demo_map_truck', 'truck_lane_version_line', 'truckLanceCode', 'TRUCK-L');
CALL demo_add_missing('_demo_map_truck', 'truck_lane_schedule_line', 'fromTruckLanceCode', 'TRUCK-F');
CALL demo_add_missing('_demo_map_truck', 'truck_lane_schedule_line', 'toTruckLanceCode', 'TRUCK-T');
CALL demo_add_missing('_demo_map_truck', 'delivery_order_pick_order', 'truckLanceCode', 'TRUCK-D');
CALL demo_add_missing('_demo_map_truck', 'do_pick_order', 'TruckLanceCode', 'TRUCK-O');
CALL demo_add_missing('_demo_map_truck', 'do_pick_order_record', 'TruckLanceCode', 'TRUCK-R');
CALL demo_add_missing('_demo_map_truck', 'do_replenishment', 'truckLaneCode', 'TRUCK-P');

INSERT INTO _demo_map_do (old_value, new_value)
SELECT code, CONCAT('DO-', MIN(id)) FROM delivery_order
WHERE code IS NOT NULL AND code <> '' GROUP BY code;

INSERT INTO _demo_map_po (old_value, new_value)
SELECT code, CONCAT('PO-', MIN(id)) FROM purchase_order
WHERE code IS NOT NULL AND code <> '' GROUP BY code;

INSERT INTO _demo_map_jo (old_value, new_value)
SELECT code, CONCAT('JO-', MIN(id)) FROM job_order
WHERE code IS NOT NULL AND code <> '' GROUP BY code;

INSERT INTO _demo_map_pk (old_value, new_value)
SELECT code, CONCAT('PK-', MIN(id)) FROM pick_order
WHERE code IS NOT NULL AND code <> '' GROUP BY code;

INSERT INTO _demo_map_si (old_value, new_value)
SELECT code, CONCAT('SI-', MIN(id)) FROM stock_in
WHERE code IS NOT NULL AND code <> '' GROUP BY code;

INSERT INTO _demo_map_st (old_value, new_value)
SELECT code, CONCAT('ST-', MIN(id)) FROM stock_take
WHERE code IS NOT NULL AND code <> '' GROUP BY code;

INSERT INTO _demo_map_pp (old_value, new_value)
SELECT productprocesscode, CONCAT('PP-', MIN(id)) FROM productprocess
WHERE productprocesscode IS NOT NULL AND productprocesscode <> ''
GROUP BY productprocesscode;

INSERT INTO _demo_map_equip (old_value, new_value)
SELECT code, CONCAT('EQ-', MIN(id)) FROM equipment
WHERE code IS NOT NULL AND code <> ''
GROUP BY code;

START TRANSACTION;

UPDATE items i
LEFT JOIN _demo_map_item m ON i.code COLLATE utf8mb4_unicode_ci = m.old_value
SET i.code = COALESCE(m.new_value, CONCAT('ITEM-', i.id)),
i.name = COALESCE(m.new_name, CONCAT('Item ', i.id)),
i.description = COALESCE(m.new_name, CONCAT('Item ', i.id)),
i.remarks = '',
i.countryOfOrigin = '';

UPDATE shop s
JOIN _demo_map_shop m ON s.code COLLATE utf8mb4_unicode_ci = m.old_value
SET s.code = m.new_value,
s.name = m.new_name,
s.brNo = '',
s.contactNo = '',
s.contactEmail = '',
s.contactName = '',
s.addr1 = '',
s.addr2 = '',
s.addr3 = '',
s.addr4 = '',
s.district = '';

UPDATE shop
SET code = CONCAT('SHOP-', id),
name = CONCAT('Shop ', id),
brNo = '',
contactNo = '',
contactEmail = '',
contactName = '',
addr1 = '',
addr2 = '',
addr3 = '',
addr4 = '',
district = ''
WHERE code IS NULL OR code = '';

CALL demo_apply_pair('truck', 'ShopCode', 'ShopName', '_demo_map_shop');
CALL demo_apply_pair('truck_lane_schedule_line', 'shopCode', 'shopName', '_demo_map_shop');
CALL demo_apply_pair('delivery_order_pick_order', 'shopCode', 'shopName', '_demo_map_shop');
CALL demo_apply_pair('do_pick_order', 'ShopCode', 'ShopName', '_demo_map_shop');
CALL demo_apply_pair('do_pick_order_record', 'ShopCode', 'ShopName', '_demo_map_shop');
CALL demo_apply_pair('do_replenishment', 'shopCode', 'shopName', '_demo_map_shop');
CALL demo_apply_pair('truck_lane_version_line', 'shopCode', 'branchName', '_demo_map_shop');
CALL demo_apply_map('truck', 'ShopCode', '_demo_map_shop');
CALL demo_apply_map('do_replenishment', 'shopCode', '_demo_map_shop');

CALL demo_apply_pair('do_replenishment', 'itemNo', 'itemName', '_demo_map_item');
CALL demo_apply_pair('stock_transfer_record', 'itemCode', 'itemName', '_demo_map_item');
CALL demo_apply_pair('stocktakerecord', 'itemCode', 'itemName', '_demo_map_item');
CALL demo_apply_pair('bag', 'itemCode', 'itemName', '_demo_map_item');
CALL demo_apply_map('purchase_order_line', 'itemNo', '_demo_map_item');
CALL demo_apply_map('delivery_order_line', 'itemNo', '_demo_map_item');
CALL demo_apply_map('stock_in_line', 'itemNo', '_demo_map_item');
CALL demo_apply_map('do_replenishment', 'itemNo', '_demo_map_item');
CALL demo_apply_map('stock_ledger', 'itemCode', '_demo_map_item');
CALL demo_apply_map('stocktakerecord', 'itemCode', '_demo_map_item');
CALL demo_apply_map('stock_adjustment_record', 'itemCode', '_demo_map_item');
CALL demo_apply_map('stock_transfer_record', 'itemCode', '_demo_map_item');
CALL demo_apply_map('item_lot_trace_log', 'itemCode', '_demo_map_item');
CALL demo_apply_map('item_default_shelf_life', 'itemCode', '_demo_map_item');
CALL demo_apply_map('coffee_or_tea', 'itemCode', '_demo_map_item');
CALL demo_apply_map('onpack_template_file', 'itemCode', '_demo_map_item');
CALL demo_apply_map('onpack_expiry_item_code', 'itemCode', '_demo_map_item');
CALL demo_apply_map('bag', 'itemCode', '_demo_map_item');
CALL demo_apply_map('jo_pick_order', 'itemCode', '_demo_map_item');
CALL demo_apply_map('jo_pick_order_record', 'itemCode', '_demo_map_item');
CALL demo_apply_map('pick_execution_issue', 'item_code', '_demo_map_item');
CALL demo_apply_map('m18_cunit', 'itemNo', '_demo_map_item');

CALL demo_apply_map('inventory_lot', 'lotNo', '_demo_map_lot');
CALL demo_apply_map('stock_in_line', 'lotNo', '_demo_map_lot');
CALL demo_apply_map('stock_in_line', 'productLotNo', '_demo_map_lot');
CALL demo_apply_map('delivery_order_line', 'm18Lot', '_demo_map_lot');
CALL demo_apply_map('stocktakerecord', 'lotNo', '_demo_map_lot');
CALL demo_apply_map('stock_adjustment_record', 'lotNo', '_demo_map_lot');
CALL demo_apply_map('stock_transfer_record', 'lotNo', '_demo_map_lot');
CALL demo_apply_map('item_lot_trace_log', 'lotNo', '_demo_map_lot');
CALL demo_apply_map('bag_lot_line', 'lotNo', '_demo_map_lot');
CALL demo_apply_map('pick_execution_issue', 'lot_no', '_demo_map_lot');

UPDATE warehouse w
LEFT JOIN _demo_map_loc m ON w.code COLLATE utf8mb4_unicode_ci = m.old_value
SET w.code = COALESCE(m.new_value, CONCAT('LOC-', w.id)),
w.name = LEFT(COALESCE(m.new_value, CONCAT('LOC-', w.id)), 30),
w.description = LEFT(COALESCE(m.new_value, CONCAT('LOC-', w.id)), 30),
w.warehouse = NULL,
w.area = NULL,
w.slot = NULL,
w.stockTakeSection = NULL,
w.stockTakeSectionDescription = NULL;

CALL demo_apply_map('items', 'LocationCode', '_demo_map_loc');
CALL demo_apply_map('stock_transfer_record', 'startLocation', '_demo_map_loc');
CALL demo_apply_map('stock_transfer_record', 'targetLocation', '_demo_map_loc');
CALL demo_apply_map('pick_execution_issue', 'store_location', '_demo_map_loc');

UPDATE items
SET warehouse = NULL, area = NULL, slot = NULL;

UPDATE warehouse
SET `order` = CASE
WHEN `order` REGEXP '^2F-[0-9]+$' THEN CONCAT('AF-', SUBSTRING(`order`, 4))
WHEN `order` REGEXP '^4F-[0-9]+$' THEN CONCAT('BF-', SUBSTRING(`order`, 4))
WHEN `order` REGEXP '^1F-[0-9]+$' THEN CONCAT('CF-', SUBSTRING(`order`, 4))
WHEN `order` REGEXP '^3F-[0-9]+$' THEN CONCAT('DF-', SUBSTRING(`order`, 4))
ELSE `order`
END;

CALL demo_apply_map('truck', 'TruckLanceCode', '_demo_map_truck');
CALL demo_apply_map('truck_lane_version', 'truckLanceCode', '_demo_map_truck');
CALL demo_apply_map('truck_lane_version_line', 'truckLanceCode', '_demo_map_truck');
CALL demo_apply_map('truck_lane_schedule_line', 'fromTruckLanceCode', '_demo_map_truck');
CALL demo_apply_map('truck_lane_schedule_line', 'toTruckLanceCode', '_demo_map_truck');
CALL demo_apply_map('delivery_order_pick_order', 'truckLanceCode', '_demo_map_truck');
CALL demo_apply_map('do_pick_order', 'TruckLanceCode', '_demo_map_truck');
CALL demo_apply_map('do_pick_order_record', 'TruckLanceCode', '_demo_map_truck');
CALL demo_apply_map('do_replenishment', 'truckLaneCode', '_demo_map_truck');

CALL demo_apply_map('delivery_order', 'code', '_demo_map_do');
CALL demo_apply_map('do_pick_order', 'delivery_order_code', '_demo_map_do');
CALL demo_apply_map('do_pick_order_record', 'delivery_order_code', '_demo_map_do');
CALL demo_apply_map('do_pick_order_line', 'delivery_order_code', '_demo_map_do');
CALL demo_apply_map('do_pick_order_line_record', 'delivery_order_code', '_demo_map_do');
CALL demo_apply_map('stock_out', 'deliveryOrderCode', '_demo_map_do');
CALL demo_apply_map('do_replenishment', 'sourceDoCode', '_demo_map_do');
CALL demo_apply_map('do_replenishment', 'targetDoCode', '_demo_map_do');

CALL demo_apply_map('purchase_order', 'code', '_demo_map_po');
CALL demo_apply_map('job_order', 'code', '_demo_map_jo');
CALL demo_apply_map('jo_pick_order', 'jobOrderCode', '_demo_map_jo');
CALL demo_apply_map('jo_pick_order_record', 'jobOrderCode', '_demo_map_jo');
CALL demo_apply_map('jo_bag_consumption', 'jobOrderCode', '_demo_map_jo');

CALL demo_apply_map('pick_order', 'code', '_demo_map_pk');
CALL demo_apply_map('do_pick_order', 'pick_order_code', '_demo_map_pk');
CALL demo_apply_map('do_pick_order_record', 'pick_order_code', '_demo_map_pk');
CALL demo_apply_map('do_pick_order_line', 'pick_order_code', '_demo_map_pk');
CALL demo_apply_map('do_pick_order_line_record', 'pick_order_code', '_demo_map_pk');
CALL demo_apply_map('jo_pick_order', 'pickOrderCode', '_demo_map_pk');
CALL demo_apply_map('jo_pick_order_record', 'pickOrderCode', '_demo_map_pk');
CALL demo_apply_map('pick_execution_issue', 'pick_order_code', '_demo_map_pk');
CALL demo_apply_map('stock_out', 'consoPickOrderCode', '_demo_map_pk');

CALL demo_apply_map('stock_in', 'code', '_demo_map_si');
CALL demo_apply_map('stock_take', 'code', '_demo_map_st');
CALL demo_apply_map('productprocess', 'productprocesscode', '_demo_map_pp');

UPDATE pick_order
SET consoCode = CONCAT('CS-', id)
WHERE consoCode IS NOT NULL AND consoCode <> '';

UPDATE do_replenishment
SET code = CONCAT('DR-', id)
WHERE code IS NOT NULL AND code <> '';

UPDATE do_pick_order_record
SET deliveryNoteCode = CONCAT('DN-', id)
WHERE deliveryNoteCode IS NOT NULL AND deliveryNoteCode <> '';

UPDATE delivery_order_pick_order
SET deliveryNoteCode = CONCAT('DN-P', id)
WHERE deliveryNoteCode IS NOT NULL AND deliveryNoteCode <> '';

CALL demo_exec_if_table('purchase_quotation',
'UPDATE purchase_quotation SET code = CONCAT(''PQ-'', id) WHERE code IS NOT NULL');
CALL demo_exec_if_table('purchase_quotation_line',
'UPDATE purchase_quotation_line SET code = CONCAT(''PQL-'', id) WHERE code IS NOT NULL');

UPDATE bom
SET code = CONCAT('BOM-', id),
name = CONCAT('Bom ', id),
description = CONCAT('Bom ', id);

CALL demo_exec_if_table('company',
'UPDATE company SET code = CONCAT(''CO-'', id), name = CONCAT(''Company '', id), brNo = '''', contactNo = '''', email = '''', addr1 = '''', addr2 = '''', addr3 = '''', addr4 = ''''');
CALL demo_exec_if_table('process',
'UPDATE `process` SET code = CONCAT(''PROC-'', id), name = LEFT(CONCAT(''Process '', id), 30), description = LEFT(CONCAT(''Process '', id), 30), remarks = ''''');
CALL demo_exec_if_table('equipment',
'UPDATE equipment SET code = CONCAT(''EQ-'', id), name = LEFT(CONCAT(''Equip '', id), 30), description = CONCAT(''Equip '', id)');
CALL demo_apply_map('equipment_detail', 'equipmentCode', '_demo_map_equip');
CALL demo_exec_if_table('equipment_detail',
'UPDATE equipment_detail SET code = CONCAT(''EQD-'', id), name = CONCAT(''Equip '', id), repairAndMaintenanceRemarks = ''''');
CALL demo_exec_if_table('item_category',
'UPDATE item_category SET code = CONCAT(''CAT-'', id), parent = '''', sub = ''''');
CALL demo_exec_if_table('qc_item',
'UPDATE qc_item SET code = CONCAT(''QC-'', id), name = CONCAT(''Qc '', id), description = ''''');
CALL demo_exec_if_table('qc_category',
'UPDATE qc_category SET code = CONCAT(''QCC-'', id), name = CONCAT(''Qc '', id), description = CONCAT(''Qc '', id)');
CALL demo_exec_if_table('pick_order_group',
'UPDATE pick_order_group SET name = CONCAT(''POG-'', id)');
CALL demo_exec_if_table('printer',
'UPDATE printer SET code = CONCAT(''PRINTER-'', id), name = CASE name WHEN ''Label機 2F A+B'' THEN ''LABEL-A'' WHEN ''Label機 4F 乾貨 C, D'' THEN ''LABEL-B'' ELSE CONCAT(''Printer '', id) END, description = '''', brand = '''', ip = ''''');
CALL demo_exec_if_table('logistic',
'UPDATE logistic SET logisticName = CONCAT(''Logistic '', id), carPlate = CONCAT(''PLATE-'', id), driverName = CONCAT(''Driver '', id), driverNumber = id');
CALL demo_exec_if_table('group',
'UPDATE `group` SET name = CONCAT(''~g'', id)');
CALL demo_exec_if_table('group',
'UPDATE `group` SET name = CONCAT(''GROUP-'', id), description = ''''');

CALL demo_exec_if_table('onpack_template_file',
'UPDATE onpack_template_file SET fileName = CONCAT(''file-'', id)');
CALL demo_exec_if_table('onpack_expiry_item_code',
'UPDATE onpack_expiry_item_code SET printName = itemCode');

CALL demo_exec_if_table('m18_data_log',
'UPDATE m18_data_log SET dataLog = JSON_OBJECT()');
CALL demo_exec_if_table('grn_send_log',
'UPDATE grn_send_log SET data = JSON_OBJECT()');
CALL demo_exec_if_table('m18_goods_receipt_note_log',
'UPDATE m18_goods_receipt_note_log SET grn_code = CONCAT(''GRN-'', id)');
CALL demo_exec_if_table('mail_template',
'UPDATE mail_template SET `to` = '''', cc = '''', description = CONCAT(''Template '', id), subjectCht = '''', subjectEng = '''', contentCht = '''', contentEng = ''''');
CALL demo_exec_if_table('mail_send_log',
'UPDATE mail_send_log SET senderMail = '''', subject = '''', content = '''', tempDesc = ''Mail'', errorMessage = ''''');
CALL demo_exec_if_table('mail_send_log_line',
'UPDATE mail_send_log_line SET toMail = '''', errorMessage = ''''');

CALL demo_apply_map('item_lot_trace_log', 'username', '_demo_map_username');
CALL demo_apply_map('stocktakerecord', 'createdBy', '_demo_map_username');
CALL demo_apply_map('pick_execution_issue', 'createdBy', '_demo_map_username');
CALL demo_apply_map('pick_execution_issue', 'modifiedBy', '_demo_map_username');
CALL demo_apply_map('pick_execution_issue', 'handled_by', '_demo_map_username');
CALL demo_apply_map('do_pick_order', 'createdBy', '_demo_map_username');
CALL demo_apply_map('do_pick_order', 'modifiedBy', '_demo_map_username');
CALL demo_apply_map('do_pick_order_record', 'createdBy', '_demo_map_username');
CALL demo_apply_map('do_pick_order_record', 'modifiedBy', '_demo_map_username');
CALL demo_apply_map('delivery_order_pick_order', 'createdBy', '_demo_map_username');
CALL demo_apply_map('delivery_order_pick_order', 'modifiedBy', '_demo_map_username');

CALL demo_apply_map('stocktakerecord', 'approverName', '_demo_map_person');
CALL demo_apply_map('stocktakerecord', 'stockTakerName', '_demo_map_person');
CALL demo_apply_map('do_pick_order', 'handler_name', '_demo_map_person');
CALL demo_apply_map('do_pick_order_record', 'handler_name', '_demo_map_person');
CALL demo_apply_map('delivery_order_pick_order', 'handlerName', '_demo_map_person');
CALL demo_apply_map('pick_execution_issue', 'picker_name', '_demo_map_person');
CALL demo_exec_if_table('pick_execution_issue',
'UPDATE pick_execution_issue SET item_description = ''''');

UPDATE `user` u
JOIN _demo_map_user m ON m.user_id = u.id
SET u.username = CONCAT('~u', u.id);

UPDATE `user` u
JOIN _demo_map_user m ON m.user_id = u.id
SET u.username = m.new_username,
u.password = pwd_hash,
u.name = m.new_name,
u.fullname = m.new_name,
u.firstname = 'User',
u.lastname = CAST(u.id AS CHAR),
u.email = CASE m.new_username
WHEN 'demo' THEN '[email protected]'
WHEN 'demo.stock' THEN '[email protected]'
WHEN 'demo.prod' THEN '[email protected]'
ELSE CONCAT('user', u.id, '@example.com')
END,
u.phone1 = '',
u.phone2 = '',
u.department = '',
u.title = '',
u.remarks = '',
u.staffNo = '';

CALL demo_blank_columns();

UPDATE settings SET name = 'DO.floor.suppliers.AF' WHERE name = 'DO.floor.suppliers.2F';
UPDATE settings SET name = 'DO.floor.suppliers.BF' WHERE name = 'DO.floor.suppliers.4F';
CALL demo_remap_shop_csv('DO.floor.suppliers.AF');
CALL demo_remap_shop_csv('DO.floor.suppliers.BF');

UPDATE settings
SET value = ''
WHERE name IN (
'LASER_PRINT.host',
'LASER_PRINT.itemCodes',
'LASER_PRINT.lastReceiveSuccess'
)
OR name LIKE 'MAIL.smtp.%'
OR name LIKE 'REPORT.dailyMaint.recipients%';

CALL demo_replace_floors();

INSERT INTO settings (name, value, category, type)
VALUES ('DEMO.anonymized.v1', '1', 'DEMO', 'string');

COMMIT;
CALL demo_drop_maps();
END$$

DELIMITER ;

CALL anonymize_demo_copy();

DROP PROCEDURE IF EXISTS demo_exec_if_table;
DROP PROCEDURE IF EXISTS demo_apply_map;
DROP PROCEDURE IF EXISTS demo_apply_pair;
DROP PROCEDURE IF EXISTS demo_add_missing;
DROP PROCEDURE IF EXISTS demo_blank_columns;
DROP PROCEDURE IF EXISTS demo_replace_floors;
DROP PROCEDURE IF EXISTS demo_remap_shop_csv;
DROP PROCEDURE IF EXISTS demo_drop_maps;
DROP PROCEDURE IF EXISTS anonymize_demo_copy;

SELECT name, value FROM settings WHERE name = 'DEMO.anonymized.v1';

+ 0
- 94
src/main/java/com/ffii/fpsms/modules/common/demo/DemoCoverageSeeder.kt ファイルの表示

@@ -1,94 +0,0 @@
package com.ffii.fpsms.modules.common.demo

import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.context.annotation.Profile
import org.springframework.core.annotation.Order
import org.springframework.core.io.ClassPathResource
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.stereotype.Component
import java.nio.charset.StandardCharsets
import java.sql.Connection
import javax.sql.DataSource

/**
* Extra fictional rows so demo screens and report downloads have something to show.
* Runs only after the base sandbox exists, and only against fpsms_demo.
*/
@Component
@Profile("db-demo")
@Order(2)
class DemoCoverageSeeder(
@Value("\${spring.datasource.jdbc-url}") private val jdbcUrl: String,
private val dataSource: DataSource,
private val jdbcTemplate: JdbcTemplate,
) : ApplicationRunner {

private val log = LoggerFactory.getLogger(DemoCoverageSeeder::class.java)

override fun run(args: ApplicationArguments) {
val configuredName = databaseName(jdbcUrl)
val connectedName = dataSource.connection.use { it.catalog }
if (configuredName != DEMO_DATABASE || !DEMO_DATABASE.equals(connectedName, ignoreCase = true)) {
throw IllegalStateException(
"Demo coverage refused to start. Configured database is '$configuredName', connected database is '$connectedName'.",
)
}
val already = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM settings WHERE name = ?",
Int::class.java,
MARKER,
) ?: 0
if (already > 0) {
log.info("Demo coverage data already present.")
return
}
val script = ClassPathResource(SCRIPT).inputStream.use { it.readBytes().toString(StandardCharsets.UTF_8) }
dataSource.connection.use { connection ->
connection.autoCommit = false
try {
executeScript(connection, script)
connection.commit()
} catch (ex: Exception) {
connection.rollback()
throw IllegalStateException("Demo coverage data failed to load.", ex)
}
}
log.info("Demo coverage data loaded.")
}

private fun executeScript(connection: Connection, script: String) {
val statement = StringBuilder()
connection.createStatement().use { jdbc ->
for (rawLine in script.lineSequence()) {
val line = rawLine.trim()
if (line.isEmpty() || line.startsWith("--")) continue
statement.append(rawLine).append('\n')
if (line.endsWith(";")) {
val sql = statement.toString().trim().removeSuffix(";").trim()
statement.clear()
if (sql.isNotEmpty()) jdbc.execute(sql)
}
}
}
val tail = statement.toString().trim().removeSuffix(";").trim()
if (tail.isNotEmpty()) {
connection.createStatement().use { it.execute(tail) }
}
}

private fun databaseName(jdbcUrl: String): String? {
val path = jdbcUrl.substringBefore('?').substringAfter("://")
val slash = path.indexOf('/')
if (slash < 0 || slash == path.lastIndex) return null
return path.substring(slash + 1).trim().ifEmpty { null }
}

companion object {
private const val DEMO_DATABASE = "fpsms_demo"
private const val MARKER = "DEMO.coverage.v1"
private const val SCRIPT = "demo/demo-coverage.sql"
}
}

+ 2
- 304
src/main/java/com/ffii/fpsms/modules/common/demo/DemoDateKeeper.kt ファイルの表示

@@ -1,314 +1,12 @@
package com.ffii.fpsms.modules.common.demo

import org.slf4j.LoggerFactory
import org.springframework.beans.factory.ObjectProvider
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.context.annotation.Profile
import org.springframework.core.annotation.Order
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
import org.springframework.transaction.support.TransactionTemplate
import java.time.LocalDate
import java.time.temporal.ChronoUnit
import javax.sql.DataSource

/**
* Keeps fictional sandbox dates lined up with today.
*
* Seed data is inserted once, with expiry and delivery dates relative to that day.
* Without a later shift, lots expire and list screens that default to today stop showing the samples.
* This only runs on the db-demo profile, and only against fpsms_demo, and only rows coded DEMO-.
* Date shifting is retired. Demo dates come from the production copy and stay as copied.
* The shift helper remains for the existing unit test.
*/
@Component
@Profile("db-demo")
@Order(20)
class DemoDateKeeper(
@Value("\${spring.datasource.jdbc-url}") private val jdbcUrl: String,
private val dataSource: DataSource,
private val jdbcTemplate: JdbcTemplate,
private val transactionTemplate: TransactionTemplate,
private val sandboxSeeder: ObjectProvider<DemoSandboxSeeder>,
) : ApplicationRunner {

private val log = LoggerFactory.getLogger(DemoDateKeeper::class.java)

override fun run(args: ApplicationArguments) {
refresh()
}

/** Catch a process that stays up past midnight. Startup and the truck board also call [refresh]. */
@Scheduled(cron = "\${fpsms.demo.date-refresh-cron:0 10 0 * * *}")
fun scheduledRefresh() {
try {
refresh()
} catch (ex: Exception) {
log.error("Demo date refresh failed", ex)
}
}

@Synchronized
fun refresh() {
assertDemoDatabase()
transactionTemplate.executeWithoutResult {
val stored = readAnchor()
val anchor = stored ?: inferAnchorFromPurchaseOrder()
val today = LocalDate.now()
if (anchor == null) {
saveAnchor(today)
log.warn("Demo date keeper found no DEMO-PO-001 order date. Marked {} without shifting.", today)
return@executeWithoutResult
}
when (val shift = demoDateShiftDays(anchor, today)) {
is DemoDateShift.TooLarge -> {
log.error(
"Demo date anchor {} is {} days before today, over the {} day limit. Dates were not moved.",
anchor,
shift.days,
DEMO_DATE_SHIFT_MAX_DAYS,
)
return@executeWithoutResult
}
is DemoDateShift.Forward -> {
val updated = shiftDemoDates(shift.days.toInt())
// Truck tickets are also pinned to today/tomorrow by the seeder. Re-pin after the
// shift so a board that was already on today is not pushed further ahead.
sandboxSeeder.getIfAvailable()?.realignDashboardTrucks()
log.info(
"Demo dates moved forward {} day(s), {} row update(s). Anchor {} -> {}.",
shift.days,
updated,
anchor,
today,
)
}
DemoDateShift.None -> Unit
}
val rolled = rollQuarterlyLedgerIntoCurrentYear()
if (rolled > 0) {
log.info("Rolled {} demo stock-ledger sample row(s) into the current year.", rolled)
}
if (stored != today) {
saveAnchor(today)
}
// After the shift, so a multi-day catch-up does not push brand-new "today" rows into the future.
sandboxSeeder.getIfAvailable()?.ensureDeliveryShowcase()
}
}

private fun assertDemoDatabase() {
val configuredName = databaseName(jdbcUrl)
val connectedName = dataSource.connection.use { it.catalog }
if (configuredName != DEMO_DATABASE || !DEMO_DATABASE.equals(connectedName, ignoreCase = true)) {
throw IllegalStateException(
"Demo date keeper refused to run. Configured database is '$configuredName', connected database is '$connectedName'.",
)
}
}

private fun readAnchor(): LocalDate? {
val value = jdbcTemplate.query(
"SELECT value FROM settings WHERE name = ? ORDER BY id DESC LIMIT 1",
{ rs, _ -> rs.getString("value") },
ANCHOR_SETTING,
).firstOrNull() ?: return null
return runCatching { LocalDate.parse(value) }.getOrNull()
}

/** DEMO-PO-001 is inserted with orderDate = the seed day, so an existing sandbox can catch up once. */
private fun inferAnchorFromPurchaseOrder(): LocalDate? {
return jdbcTemplate.query(
"SELECT DATE(orderDate) AS anchorDate FROM purchase_order WHERE code = 'DEMO-PO-001' LIMIT 1",
{ rs, _ -> rs.getDate("anchorDate")?.toLocalDate() },
).firstOrNull()
}

private fun saveAnchor(today: LocalDate) {
val value = today.toString()
val updated = jdbcTemplate.update(
"UPDATE settings SET value = ? WHERE name = ?",
value,
ANCHOR_SETTING,
)
if (updated == 0) {
jdbcTemplate.update(
"""
INSERT INTO settings (name, value, category, type)
SELECT ?, ?, 'DEMO', 'date'
WHERE NOT EXISTS (SELECT 1 FROM settings WHERE name = ?)
""".trimIndent(),
ANCHOR_SETTING,
value,
ANCHOR_SETTING,
)
}
}

private fun shiftDemoDates(days: Int): Int {
val statements = listOf(
"""
UPDATE purchase_order
SET orderDate = DATE_ADD(orderDate, INTERVAL $days DAY),
estimatedArrivalDate = DATE_ADD(estimatedArrivalDate, INTERVAL $days DAY),
completeDate = DATE_ADD(completeDate, INTERVAL $days DAY)
WHERE code LIKE 'DEMO-%'
""",
"""
UPDATE delivery_order
SET orderDate = DATE_ADD(orderDate, INTERVAL $days DAY),
estimatedArrivalDate = DATE_ADD(estimatedArrivalDate, INTERVAL $days DAY),
completeDate = DATE_ADD(completeDate, INTERVAL $days DAY)
WHERE code LIKE 'DEMO-%'
""",
"""
UPDATE delivery_order_pick_order
SET requiredDeliveryDate = DATE_ADD(requiredDeliveryDate, INTERVAL $days DAY),
ticketReleaseTime = DATE_ADD(ticketReleaseTime, INTERVAL $days DAY),
ticketCompleteDateTime = DATE_ADD(ticketCompleteDateTime, INTERVAL $days DAY)
WHERE ticketNo LIKE 'DEMO-%'
""",
"""
UPDATE do_pick_order_record
SET RequiredDeliveryDate = DATE_ADD(RequiredDeliveryDate, INTERVAL $days DAY),
ticket_release_time = DATE_ADD(ticket_release_time, INTERVAL $days DAY),
ticketCompleteDateTime = DATE_ADD(ticketCompleteDateTime, INTERVAL $days DAY)
WHERE ticket_no LIKE 'DEMO-%'
OR delivery_order_code LIKE 'DEMO-%'
OR pick_order_code LIKE 'DEMO-%'
""",
"""
UPDATE do_replenishment
SET deliveryDate = DATE_ADD(deliveryDate, INTERVAL $days DAY)
WHERE code LIKE 'DEMO-%'
""",
"""
UPDATE job_order
SET planStart = DATE_ADD(planStart, INTERVAL $days DAY),
actualStart = DATE_ADD(actualStart, INTERVAL $days DAY),
planEnd = DATE_ADD(planEnd, INTERVAL $days DAY),
actualEnd = DATE_ADD(actualEnd, INTERVAL $days DAY)
WHERE code LIKE 'DEMO-%'
""",
"""
UPDATE productprocess
SET startTime = DATE_ADD(startTime, INTERVAL $days DAY),
endTime = DATE_ADD(endTime, INTERVAL $days DAY),
date = DATE_ADD(date, INTERVAL $days DAY)
WHERE productprocesscode LIKE 'DEMO-%'
""",
"""
UPDATE pick_order
SET targetDate = DATE_ADD(targetDate, INTERVAL $days DAY),
completeDate = DATE_ADD(completeDate, INTERVAL $days DAY),
releasedDate = DATE_ADD(releasedDate, INTERVAL $days DAY)
WHERE code LIKE 'DEMO-%'
""",
"""
UPDATE stock_in
SET orderDate = DATE_ADD(orderDate, INTERVAL $days DAY),
estimatedCompleteDate = DATE_ADD(estimatedCompleteDate, INTERVAL $days DAY),
completeDate = DATE_ADD(completeDate, INTERVAL $days DAY)
WHERE code LIKE 'DEMO-%'
""",
"""
UPDATE stock_in_line sil
JOIN stock_in si ON si.id = sil.stockInId
SET sil.expiryDate = DATE_ADD(sil.expiryDate, INTERVAL $days DAY),
sil.receiptDate = DATE_ADD(sil.receiptDate, INTERVAL $days DAY),
sil.productionDate = DATE_ADD(sil.productionDate, INTERVAL $days DAY),
sil.dnDate = DATE_ADD(sil.dnDate, INTERVAL $days DAY)
WHERE si.code LIKE 'DEMO-%'
OR sil.lotNo LIKE 'DEMO-LOT-%'
OR sil.itemNo LIKE 'DEMO-%'
""",
"""
UPDATE inventory_lot
SET expiryDate = DATE_ADD(expiryDate, INTERVAL $days DAY),
stockInDate = DATE_ADD(stockInDate, INTERVAL $days DAY),
productionDate = DATE_ADD(productionDate, INTERVAL $days DAY)
WHERE lotNo LIKE 'DEMO-LOT-%'
""",
"""
UPDATE stock_out
SET completeDate = DATE_ADD(completeDate, INTERVAL $days DAY)
WHERE deliveryOrderCode LIKE 'DEMO-%'
OR consoPickOrderCode LIKE 'DEMO-%'
""",
"""
UPDATE stock_out_line sol
JOIN stock_out so ON so.id = sol.stockOutId
SET sol.pickTime = DATE_ADD(sol.pickTime, INTERVAL $days DAY),
sol.startTime = DATE_ADD(sol.startTime, INTERVAL $days DAY),
sol.endTime = DATE_ADD(sol.endTime, INTERVAL $days DAY)
WHERE so.deliveryOrderCode LIKE 'DEMO-%'
OR so.consoPickOrderCode LIKE 'DEMO-%'
""",
"""
UPDATE stock_take
SET planStart = DATE_ADD(planStart, INTERVAL $days DAY),
planEnd = DATE_ADD(planEnd, INTERVAL $days DAY),
actualStart = DATE_ADD(actualStart, INTERVAL $days DAY),
actualEnd = DATE_ADD(actualEnd, INTERVAL $days DAY)
WHERE code LIKE 'DEMO-%'
""",
"""
UPDATE stocktakerecord r
JOIN stock_take st ON st.id = r.stockTakeId
SET r.date = DATE_ADD(r.date, INTERVAL $days DAY),
r.approverTime = DATE_ADD(r.approverTime, INTERVAL $days DAY),
r.stockTakeStartTime = DATE_ADD(r.stockTakeStartTime, INTERVAL $days DAY),
r.stockTakeEndTime = DATE_ADD(r.stockTakeEndTime, INTERVAL $days DAY),
r.expiredDate = DATE_ADD(r.expiredDate, INTERVAL $days DAY)
WHERE st.code LIKE 'DEMO-%'
OR r.itemCode LIKE 'DEMO-%'
OR r.lotNo LIKE 'DEMO-LOT-%'
""",
"""
UPDATE stock_ledger
SET date = DATE_ADD(date, INTERVAL $days DAY),
modified = DATE_ADD(modified, INTERVAL $days DAY)
WHERE itemCode LIKE 'DEMO-%'
AND (type IS NULL OR type <> 'OUT')
""",
)
return statements.sumOf { jdbcTemplate.update(it.trimIndent()) }
}

/**
* January / April / July / September ledger samples are fixed calendar days, not offsets from the seed day.
* Leave them in those months, and move them into the new year when the calendar rolls over.
*/
private fun rollQuarterlyLedgerIntoCurrentYear(): Int {
return jdbcTemplate.update(
"""
UPDATE stock_ledger
SET date = DATE_ADD(date, INTERVAL (YEAR(CURDATE()) - YEAR(date)) YEAR),
modified = CASE
WHEN modified IS NULL THEN NULL
ELSE DATE_ADD(modified, INTERVAL (YEAR(CURDATE()) - YEAR(modified)) YEAR)
END
WHERE itemCode LIKE 'DEMO-%'
AND type = 'OUT'
AND date IS NOT NULL
AND YEAR(date) < YEAR(CURDATE())
""".trimIndent(),
)
}

private fun databaseName(jdbcUrl: String): String? {
val path = jdbcUrl.substringBefore('?').substringAfter("://")
val slash = path.indexOf('/')
if (slash < 0 || slash == path.lastIndex) return null
return path.substring(slash + 1).trim().ifEmpty { null }
}

companion object {
private const val DEMO_DATABASE = "fpsms_demo"
const val ANCHOR_SETTING = "DEMO.dates.anchor"
}
}

internal const val DEMO_DATE_SHIFT_MAX_DAYS = 2000L



+ 26
- 0
src/main/java/com/ffii/fpsms/modules/common/demo/DemoFloors.kt ファイルの表示

@@ -0,0 +1,26 @@
package com.ffii.fpsms.modules.common.demo

/**
* Floor and bin labels stored in the anonymized demo database.
* A/F was 2/F, B/F was 4/F, C/F was 1/F, D/F was 3/F.
*/
object DemoFloors {
const val FLOOR_A = "A/F"
const val FLOOR_A_KEY = "AF"
const val FLOOR_B = "B/F"
const val FLOOR_B_KEY = "BF"
const val FLOOR_C = "C/F"
const val FLOOR_C_KEY = "CF"
const val FLOOR_D = "D/F"
const val FLOOR_D_KEY = "DF"

const val LOC_HOLD = "LOC-HOLD"
const val LOC_WIP = "LOC-WIP"
const val LOC_PREFIX = "LOC-"

const val LABEL_A = "LABEL-A"
const val LABEL_B = "LABEL-B"

const val SETTING_SUPPLIERS_A = "DO.floor.suppliers.AF"
const val SETTING_SUPPLIERS_B = "DO.floor.suppliers.BF"
}

+ 13
- 1043
src/main/java/com/ffii/fpsms/modules/common/demo/DemoSandboxSeeder.kt
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 2
- 1
src/main/java/com/ffii/fpsms/modules/common/internalSetup/SetupController.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.common.internalSetup

import com.ffii.fpsms.modules.common.demo.DemoFloors
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
@@ -283,7 +284,7 @@ fun printLotStockInLabelsByStoreIds(
val printQty = (request["printQty"] as? Number)?.toInt() ?: 1
val fromIndex = (request["fromIndex"] as? Number)?.toInt()
val toIndex = (request["toIndex"] as? Number)?.toInt()
// storeIds: ["2F","4F"]
// storeIds: [DemoFloors.FLOOR_A_KEY,DemoFloors.FLOOR_B_KEY]
val storeIds = (request["storeIds"] as? List<*>)?.mapNotNull { it?.toString() } ?: emptyList()

if (printerId == null) {


+ 28
- 27
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.deliveryOrder.service

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.core.utils.PdfUtils
import org.springframework.context.annotation.Lazy
import com.ffii.core.utils.ZebraPrinterUtil
@@ -847,7 +848,7 @@ open class DeliveryOrderService(
val result = jdbcDao.queryForList(sql, mapOf("lotId" to lotId))
if (result.isNotEmpty()) {
val orderString = result.first()["warehouseOrder"] as? String
// Extract the last 3 digits from format like "2F-001"
// Extract the last 3 digits from format like "${DemoFloors.FLOOR_A_KEY}-001"
orderString?.let { order ->
// Try to extract numeric part after the last "-"
val parts = order.split("-")
@@ -1107,10 +1108,10 @@ open class DeliveryOrderService(
println(" DEBUG: Found ${trucks.size} trucks for shop $shopId")

val preferredStoreId = when (preferredFloor) {
"2F" -> "2F"
"4F" -> "4F"
"3F" -> "3F"
else -> "2F"
DemoFloors.FLOOR_A_KEY -> DemoFloors.FLOOR_A_KEY
DemoFloors.FLOOR_B_KEY -> DemoFloors.FLOOR_B_KEY
DemoFloors.FLOOR_D_KEY -> DemoFloors.FLOOR_D_KEY
else -> DemoFloors.FLOOR_A_KEY
}
// 优先选择匹配的 truck
val selectedTruck = if (trucks.size > 1) {
@@ -1127,10 +1128,10 @@ open class DeliveryOrderService(
// 检查 truck 和 preferredFloor 是否匹配
val truckStoreId = truck?.storeId
val expectedStoreId = when (preferredFloor) {
"2F" -> "2F"
"4F" -> "4F"
"3F" -> "3F"
else -> "2F"
DemoFloors.FLOOR_A_KEY -> DemoFloors.FLOOR_A_KEY
DemoFloors.FLOOR_B_KEY -> DemoFloors.FLOOR_B_KEY
DemoFloors.FLOOR_D_KEY -> DemoFloors.FLOOR_D_KEY
else -> DemoFloors.FLOOR_A_KEY
}

val defaultTruckId = 5577L
@@ -1453,13 +1454,13 @@ open class DeliveryOrderService(
}
val sortedLines = when (doStoreFloorKey) {
"2F" -> exportLines.sortedWith(
DemoFloors.FLOOR_A_KEY -> exportLines.sortedWith(
compareBy(
{ row -> itemsById[row.line.itemId]?.item_Order ?: Int.MAX_VALUE },
{ row -> row.line.itemNo },
),
)
"4F" -> exportLines.sortedBy { row ->
DemoFloors.FLOOR_B_KEY -> exportLines.sortedBy { row ->
row.line.itemId?.let { getWarehouseOrderByItemId(it) } ?: Int.MAX_VALUE
}
else -> exportLines.sortedBy { row ->
@@ -2233,10 +2234,10 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
val truck = deliveryOrder.shop?.id?.let { shopId ->
val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId)
val preferredStoreId = when (preferredFloor) {
"2F" -> "2F"
"4F" -> "4F"
"3F" -> "3F"
else -> "2F"
DemoFloors.FLOOR_A_KEY -> DemoFloors.FLOOR_A_KEY
DemoFloors.FLOOR_B_KEY -> DemoFloors.FLOOR_B_KEY
DemoFloors.FLOOR_D_KEY -> DemoFloors.FLOOR_D_KEY
else -> DemoFloors.FLOOR_A_KEY
}

val matchedTrucks = trucks.filter { it.storeId == preferredStoreId }
@@ -2244,7 +2245,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
if (matchedTrucks.isEmpty()) {
null
} else {
if (preferredStoreId == "4F" && matchedTrucks.size > 1) {
if (preferredStoreId == DemoFloors.FLOOR_B_KEY && matchedTrucks.size > 1) {
deliveryOrder.estimatedArrivalDate?.let { estimatedArrivalDate ->
val targetDate = estimatedArrivalDate.toLocalDate()
val dayOfWeek = targetDate.dayOfWeek
@@ -2260,7 +2261,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
}

println(" DEBUG: DO ${deliveryOrder.id} - Target date: $targetDate ($dayAbbr), Shop: $shopId")
println(" DEBUG: Found ${matchedTrucks.size} matched 4F trucks")
println(" DEBUG: Found ${matchedTrucks.size} matched ${DemoFloors.FLOOR_B_KEY} trucks")

val dayMatchedTrucks = matchedTrucks.filter {
it.truckLanceCode?.contains(dayAbbr, ignoreCase = true) == true
@@ -2398,17 +2399,17 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
val truck = deliveryOrder.shop?.id?.let { shopId ->
val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId)
val preferredStoreId = when (preferredFloor) {
"2F" -> "2F"
"4F" -> "4F"
"3F" -> "3F"
else -> "2F"
DemoFloors.FLOOR_A_KEY -> DemoFloors.FLOOR_A_KEY
DemoFloors.FLOOR_B_KEY -> DemoFloors.FLOOR_B_KEY
DemoFloors.FLOOR_D_KEY -> DemoFloors.FLOOR_D_KEY
else -> DemoFloors.FLOOR_A_KEY
}

val matchedTrucks = trucks.filter { it.storeId == preferredStoreId }
if (matchedTrucks.isEmpty()) {
null
} else {
if (preferredStoreId == "4F" && matchedTrucks.size > 1) {
if (preferredStoreId == DemoFloors.FLOOR_B_KEY && matchedTrucks.size > 1) {
deliveryOrder.estimatedArrivalDate?.let { estimatedArrivalDate ->
val targetDate2 = estimatedArrivalDate.toLocalDate()
val dayAbbr = when (targetDate2.dayOfWeek) {
@@ -2570,7 +2571,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
preferredFloor: String,
dayAbbr: String,
): Truck? {
if (preferredFloor == "2F") {
if (preferredFloor == DemoFloors.FLOOR_A_KEY) {
return truckRepository.findByShopIdAndDeletedFalse(shopId)
.filter { it.storeId == preferredFloor }
.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
@@ -2607,8 +2608,8 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
// 1) Check if this DO is a 4F order (your “all items on 4F, no 2F” rule)
val sql = """
SELECT
COUNT(DISTINCT CASE WHEN i.store_id = '4F' THEN dol.itemId END) AS count_4f,
COUNT(DISTINCT CASE WHEN i.store_id = '2F' THEN dol.itemId END) AS count_2f,
COUNT(DISTINCT CASE WHEN i.store_id = '${DemoFloors.FLOOR_B_KEY}' THEN dol.itemId END) AS count_4f,
COUNT(DISTINCT CASE WHEN i.store_id = '${DemoFloors.FLOOR_A_KEY}' THEN dol.itemId END) AS count_2f,
COUNT(DISTINCT dol.itemId) AS total_items
FROM delivery_order_line dol
INNER JOIN items i ON i.id = dol.itemId AND i.deleted = 0
@@ -2628,12 +2629,12 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }

// 2) Find 4F trucks for that shop on that weekday
val dayAbbr = getDayOfWeekAbbr(eta)
val trucks = truckRepository.findByShopIdAndStoreIdAndDayOfWeek(shopId, "4F", dayAbbr)
val trucks = truckRepository.findByShopIdAndStoreIdAndDayOfWeek(shopId, DemoFloors.FLOOR_B_KEY, dayAbbr)

if (trucks.isEmpty()) {
// Problem DO: 4F order but no matching 4F truck that day
val availableTrucksAll4F = truckRepository.findByShopIdAndDeletedFalse(shopId)
.filter { it.storeId == "4F" }
.filter { it.storeId == DemoFloors.FLOOR_B_KEY }

val truckDtos = availableTrucksAll4F.map { t ->
TruckInfoDto(


+ 21
- 19
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoFloorSupplierSettingsService.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.deliveryOrder.service

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.fpsms.modules.settings.entity.SettingsRepository
import org.springframework.stereotype.Service
import java.util.Locale
@@ -10,11 +11,11 @@ open class DoFloorSupplierSettingsService(
private val settingsRepository: SettingsRepository,
) {
companion object {
private const val SETTING_DO_FLOOR_SUPPLIERS_2F = "DO.floor.suppliers.2F"
private const val SETTING_DO_FLOOR_SUPPLIERS_4F = "DO.floor.suppliers.4F"
private const val SETTING_DO_FLOOR_SUPPLIERS_2F = DemoFloors.SETTING_SUPPLIERS_A
private const val SETTING_DO_FLOOR_SUPPLIERS_4F = DemoFloors.SETTING_SUPPLIERS_B

private val DEFAULT_SUPPLIERS_2F = listOf("DEMO-SUP")
private val DEFAULT_SUPPLIERS_4F = listOf("DEMO-SUP")
private val DEFAULT_SUPPLIERS_2F = emptyList<String>()
private val DEFAULT_SUPPLIERS_4F = emptyList<String>()
}

open fun supplierCodesFromSetting(settingName: String, defaultList: List<String>): List<String> {
@@ -37,8 +38,8 @@ open class DoFloorSupplierSettingsService(
val (codes2F, codes4F) = loadDoFloorSupplierLists()
return when {
f.isEmpty() || f == "ALL" || f == "All" -> (codes2F + codes4F).distinct()
f == "2F" -> codes2F
f == "4F" -> codes4F
f == DemoFloors.FLOOR_A_KEY -> codes2F
f == DemoFloors.FLOOR_B_KEY -> codes4F
else -> (codes2F + codes4F).distinct()
}
}
@@ -50,10 +51,10 @@ open class DoFloorSupplierSettingsService(
suppliers4F: List<String>,
): String {
val code = supplierCode?.trim().orEmpty()
if (code.isEmpty()) return "2F"
if (suppliers4F.contains(code)) return "4F"
if (suppliers2F.contains(code)) return "2F"
return "2F"
if (code.isEmpty()) return DemoFloors.FLOOR_A_KEY
if (suppliers4F.contains(code)) return DemoFloors.FLOOR_B_KEY
if (suppliers2F.contains(code)) return DemoFloors.FLOOR_A_KEY
return DemoFloors.FLOOR_A_KEY
}

/** DO 揀貨建議:名單外供應商不限制 2F/4F。 */
@@ -64,8 +65,8 @@ open class DoFloorSupplierSettingsService(
): String? {
val code = supplierCode?.trim().orEmpty()
if (code.isEmpty()) return null
if (suppliers4F.contains(code)) return "4F"
if (suppliers2F.contains(code)) return "2F"
if (suppliers4F.contains(code)) return DemoFloors.FLOOR_B_KEY
if (suppliers2F.contains(code)) return DemoFloors.FLOOR_A_KEY
return null
}

@@ -79,16 +80,16 @@ open class DoFloorSupplierSettingsService(
val raw = dbStoreId?.trim()?.takeIf { it.isNotEmpty() }
if (raw != null) {
return when (raw.replace("/", "").uppercase(Locale.ROOT)) {
"2F" -> "2/F"
"4F" -> "4/F"
DemoFloors.FLOOR_A_KEY -> DemoFloors.FLOOR_A
DemoFloors.FLOOR_B_KEY -> DemoFloors.FLOOR_B
else -> raw
}
}
val (s2, s4) = loadDoFloorSupplierLists()
val any4F = supplierCodes.any { code ->
preferredStoreFloorForSupplier(code, s2, s4) == "4F"
preferredStoreFloorForSupplier(code, s2, s4) == DemoFloors.FLOOR_B_KEY
}
return if (any4F) "4/F" else "2/F"
return if (any4F) DemoFloors.FLOOR_B else DemoFloors.FLOOR_A
}

data class SqlPreferredFloorCases(
@@ -106,12 +107,13 @@ open class DoFloorSupplierSettingsService(
val in4 = joinSqlInList(s4f)
val in2 = joinSqlInList(s2f)
val floor =
"CASE WHEN $codeExpr IN ($in4) THEN '4F' WHEN $codeExpr IN ($in2) THEN '2F' ELSE NULL END"
"CASE WHEN $codeExpr IN ($in4) THEN '${DemoFloors.FLOOR_B_KEY}' WHEN $codeExpr IN ($in2) THEN '${DemoFloors.FLOOR_A_KEY}' ELSE NULL END"
val storeId =
"CASE WHEN $codeExpr IN ($in4) THEN 4 WHEN $codeExpr IN ($in2) THEN 2 ELSE NULL END"
"CASE WHEN $codeExpr IN ($in4) THEN '${DemoFloors.FLOOR_B_KEY}' WHEN $codeExpr IN ($in2) THEN '${DemoFloors.FLOOR_A_KEY}' ELSE NULL END"
return SqlPreferredFloorCases(floorStringCase = floor, storeIdNumericCase = storeId)
}

private fun joinSqlInList(codes: List<String>): String =
codes.joinToString(", ") { "'" + it.replace("'", "''") + "'" }
if (codes.isEmpty()) "NULL"
else codes.joinToString(", ") { "'" + it.replace("'", "''") + "'" }
}

+ 3
- 2
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderAssignmentService.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.deliveryOrder.service

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository
@@ -33,8 +34,8 @@ class DoPickOrderAssignmentService(
// 转换 storeId 格式
val actualStoreId = when (request.storeId) {
"2/F" -> "2/F"
"4/F" -> "4/F"
DemoFloors.FLOOR_A -> DemoFloors.FLOOR_A
DemoFloors.FLOOR_B -> DemoFloors.FLOOR_B
else -> request.storeId
}
println(" DEBUG: assignByLane - Converting storeId from '${request.storeId}' to '$actualStoreId'")


+ 7
- 6
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderQueryService.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.deliveryOrder.service

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus
@@ -24,8 +25,8 @@ class DoPickOrderQueryService(
val targetDate = requiredDate ?: LocalDate.now()
val actualStoreId = when (storeId) {
"2/F" -> "2/F"
"4/F" -> "4/F"
DemoFloors.FLOOR_A -> DemoFloors.FLOOR_A
DemoFloors.FLOOR_B -> DemoFloors.FLOOR_B
else -> storeId
}
@@ -79,7 +80,7 @@ class DoPickOrderQueryService(
DoPickOrderSummaryItem(
truckDepartureTime = it.truckDepartureTime,
truckLanceCode = it.truckLanceCode,
loadingSequence = if (actualStoreId == "4/F") it.loadingSequence else null,
loadingSequence = if (actualStoreId == DemoFloors.FLOOR_B) it.loadingSequence else null,
handledBy = it.handledBy,
handlerName = it.handlerName
)
@@ -87,7 +88,7 @@ class DoPickOrderQueryService(
DoPickOrderSummaryItem(
truckDepartureTime = it.truckDepartureTime,
truckLanceCode = it.truckLanceCode,
loadingSequence = if (actualStoreId == "4/F") it.loadingSequence else null,
loadingSequence = if (actualStoreId == DemoFloors.FLOOR_B) it.loadingSequence else null,
handledBy = it.handledBy,
handlerName = null
)
@@ -97,7 +98,7 @@ class DoPickOrderQueryService(
val defaultTruck = truckRepository.findById(defaultTruckId).orElse(null)
val defaultTruckLaneCode = defaultTruck?.truckLanceCode ?: ""
val grouped = if (actualStoreId == "4/F") {
val grouped = if (actualStoreId == DemoFloors.FLOOR_B) {
allRecords.groupBy { Triple(it.truckDepartureTime, it.truckLanceCode, it.loadingSequence) }
} else {
allRecords.groupBy { Pair(it.truckDepartureTime, it.truckLanceCode) }
@@ -110,7 +111,7 @@ class DoPickOrderQueryService(
LaneBtn(
truckLanceCode = list.first().truckLanceCode ?: "",
loadingSequence = if (actualStoreId == "4/F") list.first().loadingSequence else null,
loadingSequence = if (actualStoreId == DemoFloors.FLOOR_B) list.first().loadingSequence else null,
unassigned = list.count { it.handledBy == null },
total = list.size,
handlerName = mergedHandlerNames.ifBlank { null }


+ 16
- 19
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.deliveryOrder.service

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.fpsms.m18.entity.M18DataLogRepository
import com.ffii.fpsms.m18.service.M18DataLogService
import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrder
@@ -45,10 +46,8 @@ import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecordRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecord
import org.springframework.beans.factory.ObjectProvider
import org.springframework.context.annotation.Lazy
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLineRepository
import com.ffii.fpsms.modules.common.demo.DemoSandboxSeeder
import com.ffii.fpsms.modules.deliveryOrder.web.models.TicketReleaseTableResponse
import com.ffii.fpsms.modules.deliveryOrder.web.models.TruckScheduleDashboardResponse
import com.ffii.fpsms.modules.deliveryOrder.web.models.SearchDeliveryOrderInfoRequest
@@ -67,7 +66,6 @@ open class DoPickOrderService(
@Lazy private val deliveryOrderRepository: DeliveryOrderRepository,
private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository,
private val pickOrderLineRepository: PickOrderLineRepository,
private val demoSandboxSeeder: ObjectProvider<DemoSandboxSeeder>,
) {
open fun findReleasedDoPickOrders(): List<DoPickOrder> {
return doPickOrderRepository.findByTicketStatusIn(
@@ -385,8 +383,8 @@ open class DoPickOrderService(
//println(" DEBUG: Getting summary for store=$storeId, date=$targetDate")
val actualStoreId = when (storeId) {
"2/F" -> "2/F"
"4/F" -> "4/F"
DemoFloors.FLOOR_A -> DemoFloors.FLOOR_A
DemoFloors.FLOOR_B -> DemoFloors.FLOOR_B
else -> storeId
}

@@ -525,8 +523,8 @@ open class DoPickOrderService(
// 转换 storeId 格式:'2/F' -> '2F/F', '4/F' -> '4F/F'
val actualStoreId = when (request.storeId) {
"2/F" -> "2/F" // 保持原格式
"4/F" -> "4/F" // 保持原格式
DemoFloors.FLOOR_A -> DemoFloors.FLOOR_A // 保持原格式
DemoFloors.FLOOR_B -> DemoFloors.FLOOR_B // 保持原格式
else -> request.storeId
}
println(" DEBUG: assignByLane - Converting storeId from '${request.storeId}' to '$actualStoreId'")
@@ -624,15 +622,15 @@ open class DoPickOrderService(

val results = jdbcDao.queryForList(sql, mapOf("doOrderId" to doOrderId))
return if (results.isNotEmpty()) {
val storeId = results.first()["store_id"] as? String ?: "2F"
val storeId = results.first()["store_id"] as? String ?: DemoFloors.FLOOR_A_KEY
when (storeId) {
"2F" -> "2/F"
"4F" -> "4/F"
"3F" -> "3/F"
else -> "2/F"
DemoFloors.FLOOR_A_KEY -> DemoFloors.FLOOR_A
DemoFloors.FLOOR_B_KEY -> DemoFloors.FLOOR_B
DemoFloors.FLOOR_D_KEY -> DemoFloors.FLOOR_D
else -> DemoFloors.FLOOR_A
}
} else {
"2/F" // 默认值
DemoFloors.FLOOR_A // 默认值
}
}

@@ -640,11 +638,11 @@ open class DoPickOrderService(
if (shopId == null) return null

val storeId= when (storeId) {
"2F" -> "2F"
"4F" -> "4F"
"3F" -> "3F"
"3/F" -> "3F"
else -> "2F"
DemoFloors.FLOOR_A_KEY -> DemoFloors.FLOOR_A_KEY
DemoFloors.FLOOR_B_KEY -> DemoFloors.FLOOR_B_KEY
DemoFloors.FLOOR_D_KEY -> DemoFloors.FLOOR_D_KEY
DemoFloors.FLOOR_D -> DemoFloors.FLOOR_D_KEY
else -> DemoFloors.FLOOR_A_KEY
}
val defaultTruckId = 5577L
val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId)
@@ -850,7 +848,6 @@ open class DoPickOrderService(
* Groups DoPickOrder and DoPickOrderRecord data to provide summary statistics.
*/
open fun getTruckScheduleDashboard(targetDate: LocalDate): List<TruckScheduleDashboardResponse> {
demoSandboxSeeder.getIfAvailable()?.alignDashboardTrucksToToday()
// Source of truth: delivery_order_pick_order (+ linked pick_order / pick_order_line)
//
// NOTE: delivery_order_pick_order 沒有 truckId 欄位;dashboard 的 truckId 目前僅作為展示/鍵值用途,


+ 38
- 37
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.deliveryOrder.service

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.fpsms.modules.deliveryOrder.web.models.ReleaseDoRequest
import com.ffii.fpsms.modules.master.web.models.MessageResponse
import org.springframework.stereotype.Service
@@ -163,18 +164,18 @@ class DoReleaseCoordinatorService(
pf.preferred_floor,
$dayOfWeekSql AS day_of_week_abbr,
CASE
WHEN (SELECT COUNT(*) FROM truck t WHERE t.shopId = do.shopId AND t.deleted = 0 AND t.Store_id = pf.preferred_store_id) > 1
AND pf.preferred_store_id = 4
WHEN (SELECT COUNT(*) FROM truck t WHERE t.shopId = do.shopId AND t.deleted = 0 AND REPLACE(IFNULL(t.Store_id, ''), '/', '') = pf.preferred_store_id) > 1
AND pf.preferred_store_id = '${DemoFloors.FLOOR_B_KEY}'
AND do.estimatedArrivalDate IS NOT NULL THEN
COALESCE(
(SELECT t.DepartureTime FROM truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND REPLACE(IFNULL(t.Store_id, ''), '/', '') = pf.preferred_store_id
AND t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%')
ORDER BY t.DepartureTime ASC LIMIT 1),
(SELECT t.DepartureTime FROM truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND REPLACE(IFNULL(t.Store_id, ''), '/', '') = pf.preferred_store_id
ORDER BY t.DepartureTime ASC LIMIT 1),
(SELECT t.DepartureTime FROM truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
@@ -185,7 +186,7 @@ class DoReleaseCoordinatorService(
COALESCE(
(SELECT t.DepartureTime FROM truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND REPLACE(IFNULL(t.Store_id, ''), '/', '') = pf.preferred_store_id
ORDER BY t.DepartureTime ASC LIMIT 1),
(SELECT t.DepartureTime FROM truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
@@ -201,18 +202,18 @@ class DoReleaseCoordinatorService(
)
END AS selected_departure_time,
CASE
WHEN (SELECT COUNT(*) FROM truck t WHERE t.shopId = do.shopId AND t.deleted = 0 AND t.Store_id = pf.preferred_store_id) > 1
AND pf.preferred_store_id = 4
WHEN (SELECT COUNT(*) FROM truck t WHERE t.shopId = do.shopId AND t.deleted = 0 AND REPLACE(IFNULL(t.Store_id, ''), '/', '') = pf.preferred_store_id) > 1
AND pf.preferred_store_id = '${DemoFloors.FLOOR_B_KEY}'
AND do.estimatedArrivalDate IS NOT NULL THEN
COALESCE(
(SELECT t.TruckLanceCode FROM truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND REPLACE(IFNULL(t.Store_id, ''), '/', '') = pf.preferred_store_id
AND t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%')
ORDER BY t.DepartureTime ASC LIMIT 1),
(SELECT t.TruckLanceCode FROM truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND REPLACE(IFNULL(t.Store_id, ''), '/', '') = pf.preferred_store_id
ORDER BY t.DepartureTime ASC LIMIT 1),
(SELECT t.TruckLanceCode FROM truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
@@ -223,7 +224,7 @@ class DoReleaseCoordinatorService(
COALESCE(
(SELECT t.TruckLanceCode FROM truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND REPLACE(IFNULL(t.Store_id, ''), '/', '') = pf.preferred_store_id
ORDER BY t.DepartureTime ASC LIMIT 1),
(SELECT t.TruckLanceCode FROM truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
@@ -241,13 +242,13 @@ class DoReleaseCoordinatorService(
COALESCE(
(SELECT t.LoadingSequence FROM truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND (pf.preferred_store_id != 4 OR do.estimatedArrivalDate IS NULL OR
AND REPLACE(IFNULL(t.Store_id, ''), '/', '') = pf.preferred_store_id
AND (pf.preferred_store_id != '${DemoFloors.FLOOR_B_KEY}' OR do.estimatedArrivalDate IS NULL OR
t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%'))
ORDER BY t.DepartureTime ASC LIMIT 1),
(SELECT t.LoadingSequence FROM truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND REPLACE(IFNULL(t.Store_id, ''), '/', '') = pf.preferred_store_id
ORDER BY t.DepartureTime ASC LIMIT 1),
(SELECT t.LoadingSequence FROM truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
@@ -276,7 +277,7 @@ class DoReleaseCoordinatorService(
LPAD(
ROW_NUMBER() OVER (
PARTITION BY DATE(dpo2.RequiredDeliveryDate),
IF(dpo2.store_id IS NULL OR TRIM(dpo2.store_id) = '', 'XF', COALESCE(ts.preferred_floor, '2F'))
IF(dpo2.store_id IS NULL OR TRIM(dpo2.store_id) = '', 'XF', COALESCE(ts.preferred_floor, '${DemoFloors.FLOOR_A_KEY}'))
ORDER BY COALESCE(ts.selected_departure_time, '23:59:59'),
COALESCE(ts.selected_truck_lance, 'ZZ'),
COALESCE(ts.loading_sequence, 999),
@@ -324,22 +325,22 @@ class DoReleaseCoordinatorService(
pf.preferred_store_id,
$dayOfWeekSql AS day_of_week_abbr,
CASE
WHEN (SELECT COUNT(*) FROM truck t WHERE t.shopId = do.shopId AND t.deleted = 0 AND t.Store_id = pf.preferred_store_id) > 1
AND pf.preferred_store_id = 4
WHEN (SELECT COUNT(*) FROM truck t WHERE t.shopId = do.shopId AND t.deleted = 0 AND REPLACE(IFNULL(t.Store_id, ''), '/', '') = pf.preferred_store_id) > 1
AND pf.preferred_store_id = '${DemoFloors.FLOOR_B_KEY}'
AND do.estimatedArrivalDate IS NOT NULL THEN
COALESCE(
(SELECT t.DepartureTime
FROM truck t
WHERE t.shopId = do.shopId
AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND REPLACE(IFNULL(t.Store_id, ''), '/', '') = pf.preferred_store_id
AND t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%')
ORDER BY t.DepartureTime ASC
LIMIT 1),
(SELECT t.DepartureTime
FROM truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND REPLACE(IFNULL(t.Store_id, ''), '/', '') = pf.preferred_store_id
ORDER BY t.DepartureTime ASC
LIMIT 1),
(SELECT t.DepartureTime
@@ -354,7 +355,7 @@ class DoReleaseCoordinatorService(
(SELECT t.DepartureTime
FROM truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND REPLACE(IFNULL(t.Store_id, ''), '/', '') = pf.preferred_store_id
ORDER BY t.DepartureTime ASC
LIMIT 1),
(SELECT t.DepartureTime
@@ -375,22 +376,22 @@ class DoReleaseCoordinatorService(
)
END AS selected_departure_time,
CASE
WHEN (SELECT COUNT(*) FROM truck t WHERE t.shopId = do.shopId AND t.deleted = 0 AND t.Store_id = pf.preferred_store_id) > 1
AND pf.preferred_store_id = 4
WHEN (SELECT COUNT(*) FROM truck t WHERE t.shopId = do.shopId AND t.deleted = 0 AND REPLACE(IFNULL(t.Store_id, ''), '/', '') = pf.preferred_store_id) > 1
AND pf.preferred_store_id = '${DemoFloors.FLOOR_B_KEY}'
AND do.estimatedArrivalDate IS NOT NULL THEN
COALESCE(
(SELECT t.TruckLanceCode
FROM truck t
WHERE t.shopId = do.shopId
AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND REPLACE(IFNULL(t.Store_id, ''), '/', '') = pf.preferred_store_id
AND t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%')
ORDER BY t.DepartureTime ASC
LIMIT 1),
(SELECT t.TruckLanceCode
FROM truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND REPLACE(IFNULL(t.Store_id, ''), '/', '') = pf.preferred_store_id
ORDER BY t.DepartureTime ASC
LIMIT 1),
(SELECT t.TruckLanceCode
@@ -405,7 +406,7 @@ class DoReleaseCoordinatorService(
(SELECT t.TruckLanceCode
FROM truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND REPLACE(IFNULL(t.Store_id, ''), '/', '') = pf.preferred_store_id
ORDER BY t.DepartureTime ASC
LIMIT 1),
(SELECT t.TruckLanceCode
@@ -429,14 +430,14 @@ class DoReleaseCoordinatorService(
(SELECT t.LoadingSequence
FROM truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND (pf.preferred_store_id != 4 OR do.estimatedArrivalDate IS NULL OR
AND REPLACE(IFNULL(t.Store_id, ''), '/', '') = pf.preferred_store_id
AND (pf.preferred_store_id != '${DemoFloors.FLOOR_B_KEY}' OR do.estimatedArrivalDate IS NULL OR
t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%'))
ORDER BY t.DepartureTime ASC LIMIT 1),
(SELECT t.LoadingSequence
FROM truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND REPLACE(IFNULL(t.Store_id, ''), '/', '') = pf.preferred_store_id
ORDER BY t.DepartureTime ASC LIMIT 1),
(SELECT t.LoadingSequence
FROM truck t
@@ -698,9 +699,9 @@ class DoReleaseCoordinatorService(
val storeId: String? = when {
first.usedDefaultTruck != false -> null
else -> when (first.preferredFloor) {
"2F" -> "2/F"
"4F" -> "4/F"
else -> "2/F"
DemoFloors.FLOOR_A_KEY -> DemoFloors.FLOOR_A
DemoFloors.FLOOR_B_KEY -> DemoFloors.FLOOR_B
else -> DemoFloors.FLOOR_A
}
}

@@ -776,15 +777,15 @@ class DoReleaseCoordinatorService(
preferredFloor: String?,
): Boolean {
val target = when (preferredFloor?.trim()?.uppercase(java.util.Locale.ROOT)?.replace("/", "")) {
"4F" -> "4F"
else -> "2F"
DemoFloors.FLOOR_B_KEY -> DemoFloors.FLOOR_B_KEY
else -> DemoFloors.FLOOR_A_KEY
}
val id = dpo.id ?: return true
val lines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(id)
if (lines.isEmpty()) return true
val (_, s4f) = doFloorSupplierSettingsService.loadDoFloorSupplierLists()
fun floorOf(supplierCode: String?): String =
if (!supplierCode.isNullOrBlank() && s4f.contains(supplierCode.trim())) "4F" else "2F"
if (!supplierCode.isNullOrBlank() && s4f.contains(supplierCode.trim())) DemoFloors.FLOOR_B_KEY else DemoFloors.FLOOR_A_KEY
for (line in lines) {
val doId = line.doOrderId ?: continue
val dorder = deliveryOrderRepository.findById(doId).orElse(null) ?: continue
@@ -884,9 +885,9 @@ class DoReleaseCoordinatorService(
val storeId: String? = when {
result.usedDefaultTruck!=false -> null
else -> when (result.preferredFloor) {
"2F" -> "2/F"
"4F" -> "4/F"
else -> "2/F"
DemoFloors.FLOOR_A_KEY -> DemoFloors.FLOOR_A
DemoFloors.FLOOR_B_KEY -> DemoFloors.FLOOR_B
else -> DemoFloors.FLOOR_A
}
}
@@ -1011,7 +1012,7 @@ class DoReleaseCoordinatorService(
// 2. 按日期和 storeId 分组
val grouped = tempTickets.groupBy { ticket ->
val date = ticket.requiredDeliveryDate
val storeId = ticket.storeId?.replace("/", "") ?: "2F"
val storeId = ticket.storeId?.replace("/", "") ?: DemoFloors.FLOOR_A_KEY
Pair(date, storeId)
}


+ 7
- 6
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchDopoAssignmentService.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.deliveryOrder.service

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.deliveryOrder.web.models.AssignByDeliveryOrderPickOrderIdRequest
import com.ffii.fpsms.modules.deliveryOrder.web.models.AssignByLaneRequest
@@ -108,8 +109,8 @@ open class DoWorkbenchDopoAssignmentService(
userBusyResponse(request.userId)?.let { return it }

val actualStoreId = when (request.storeId) {
"2/F" -> "2/F"
"4/F" -> "4/F"
DemoFloors.FLOOR_A -> DemoFloors.FLOOR_A
DemoFloors.FLOOR_B -> DemoFloors.FLOOR_B
else -> request.storeId
}
println(" DEBUG: assignByLaneForWorkbench storeId=$actualStoreId date=${request.requiredDate} lane=${request.truckLanceCode} dep=${request.truckDepartureTime} seq=${request.loadingSequence}")
@@ -156,7 +157,7 @@ open class DoWorkbenchDopoAssignmentService(
val candidateLimit = 50
val maxRounds = 3

val shouldOrderBySequence = actualStoreId == "2/F" && request.loadingSequence == null
val shouldOrderBySequence = actualStoreId == DemoFloors.FLOOR_A && request.loadingSequence == null
if (shouldOrderBySequence) {
sql.append(" ORDER BY dop.requiredDeliveryDate ASC, dop.truckDepartureTime ASC, dop.loadingSequence ASC, dop.id ASC LIMIT $candidateLimit ")
} else {
@@ -220,8 +221,8 @@ open class DoWorkbenchDopoAssignmentService(
userBusyResponse(request.userId)?.let { return it }

val actualStoreId = when (request.storeId) {
"2/F" -> "2/F"
"4/F" -> "4/F"
DemoFloors.FLOOR_A -> DemoFloors.FLOOR_A
DemoFloors.FLOOR_B -> DemoFloors.FLOOR_B
else -> request.storeId
}
println(" DEBUG: assignByLaneForWorkbenchV1 storeId=$actualStoreId date=${request.requiredDate} lane=${request.truckLanceCode} dep=${request.truckDepartureTime} seq=${request.loadingSequence}")
@@ -262,7 +263,7 @@ open class DoWorkbenchDopoAssignmentService(
} else {
sql.append(WorkbenchReleaseTypeSupport.assignFilterSql(request.releaseType))
}
val shouldOrderBySequenceV1 = actualStoreId == "2/F" && request.loadingSequence == null
val shouldOrderBySequenceV1 = actualStoreId == DemoFloors.FLOOR_A && request.loadingSequence == null
if (shouldOrderBySequenceV1) {
sql.append(" ORDER BY dop.requiredDeliveryDate ASC, dop.truckDepartureTime ASC, dop.loadingSequence ASC, dop.id ASC LIMIT 1 ")
} else {


+ 22
- 21
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt ファイルの表示

@@ -1,6 +1,7 @@

package com.ffii.fpsms.modules.deliveryOrder.service

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.bag.entity.BagRepository
import com.ffii.fpsms.modules.bag.service.BagService
@@ -699,8 +700,8 @@ return MessageResponse(
open fun getDeliveryOrderPickOrderSummaryByStore(storeId: String, requiredDate: LocalDate?, releaseType: String): StoreLaneSummary {
val targetDate = requiredDate ?: LocalDate.now()
val actualStoreId = when (storeId) {
"2/F" -> "2/F"
"4/F" -> "4/F"
DemoFloors.FLOOR_A -> DemoFloors.FLOOR_A
DemoFloors.FLOOR_B -> DemoFloors.FLOOR_B
else -> storeId
}
val rt = releaseType.lowercase()
@@ -805,7 +806,7 @@ return MessageResponse(

// 2/F:與 [DoPickOrderQueryService.getSummaryByStore] 一致,只按「出發時間 + 車線」合併,不按 loadingSequence 拆開。
// 否則同一車線會出現多顆 (1/1) 按鈕,與 normal FG 畫面不一致。
val laneAggsForUi = if (actualStoreId == "2/F") {
val laneAggsForUi = if (actualStoreId == DemoFloors.FLOOR_A) {
laneAggs
.groupBy { it.sortTime to it.lance }
.values
@@ -841,7 +842,7 @@ return MessageResponse(
.map {
LaneBtn(
truckLanceCode = it.lance,
loadingSequence = if (actualStoreId == "4/F") it.loadingSequence else null,
loadingSequence = if (actualStoreId == DemoFloors.FLOOR_B) it.loadingSequence else null,
unassigned = it.unassigned,
total = it.total,
handlerName = it.handlerName,
@@ -904,8 +905,8 @@ return MessageResponse(
val defaultTruckLaneCode = (defaultTruck?.truckLanceCode ?: "車線-X").trim()
val (_, s4f) = doFloorSupplierSettingsService.loadDoFloorSupplierLists()
val in4 = s4f.joinToString(", ") { "'" + it.replace("'", "''") + "'" }
.ifEmpty { "'__NO_4F_SUPPLIER__'" }
val floorExpr = "CASE WHEN s.code IN ($in4) THEN '4F' ELSE '2F' END"
.ifEmpty { "'__NO_${DemoFloors.FLOOR_B_KEY}_SUPPLIER__'" }
val floorExpr = "CASE WHEN s.code IN ($in4) THEN '${DemoFloors.FLOOR_B_KEY}' ELSE '${DemoFloors.FLOOR_A_KEY}' END"
val etraReleaseFilter = WorkbenchReleaseTypeSupport.etraFamilySql("dop.releaseType")

val sql = """
@@ -943,7 +944,7 @@ return MessageResponse(
INNER JOIN delivery_order d ON d.id = po.doId AND d.deleted = 0
LEFT JOIN shop s ON s.id = d.supplierId AND s.deleted = 0
WHERE po.deliveryOrderPickOrderId = dop.id AND po.deleted = 0
AND ($floorExpr) = '4F'
AND ($floorExpr) = '${DemoFloors.FLOOR_B_KEY}'
)
AND NOT EXISTS (
SELECT 1
@@ -951,16 +952,16 @@ return MessageResponse(
INNER JOIN delivery_order d2 ON d2.id = po2.doId AND d2.deleted = 0
LEFT JOIN shop s2 ON s2.id = d2.supplierId AND s2.deleted = 0
WHERE po2.deliveryOrderPickOrderId = dop.id AND po2.deleted = 0
AND (CASE WHEN s2.code IN ($in4) THEN '4F' ELSE '2F' END) <> '4F'
AND (CASE WHEN s2.code IN ($in4) THEN '${DemoFloors.FLOOR_B_KEY}' ELSE '${DemoFloors.FLOOR_A_KEY}' END) <> '${DemoFloors.FLOOR_B_KEY}'
)
THEN '4/F'
THEN '${DemoFloors.FLOOR_B}'
WHEN EXISTS (
SELECT 1
FROM pick_order po
INNER JOIN delivery_order d ON d.id = po.doId AND d.deleted = 0
LEFT JOIN shop s ON s.id = d.supplierId AND s.deleted = 0
WHERE po.deliveryOrderPickOrderId = dop.id AND po.deleted = 0
AND ($floorExpr) = '2F'
AND ($floorExpr) = '${DemoFloors.FLOOR_A_KEY}'
)
AND NOT EXISTS (
SELECT 1
@@ -968,9 +969,9 @@ return MessageResponse(
INNER JOIN delivery_order d2 ON d2.id = po2.doId AND d2.deleted = 0
LEFT JOIN shop s2 ON s2.id = d2.supplierId AND s2.deleted = 0
WHERE po2.deliveryOrderPickOrderId = dop.id AND po2.deleted = 0
AND (CASE WHEN s2.code IN ($in4) THEN '4F' ELSE '2F' END) <> '2F'
AND (CASE WHEN s2.code IN ($in4) THEN '${DemoFloors.FLOOR_B_KEY}' ELSE '${DemoFloors.FLOOR_A_KEY}' END) <> '${DemoFloors.FLOOR_A_KEY}'
)
THEN '2/F'
THEN '${DemoFloors.FLOOR_A}'
ELSE NULL
END
ELSE dop.storeId
@@ -1090,7 +1091,7 @@ return MessageResponse(
.thenBy { it.storeId ?: "" }
)
.map {
val is4F = it.storeId?.replace("/", "")?.trim()?.equals("4F", ignoreCase = true) == true
val is4F = it.storeId?.replace("/", "")?.trim()?.equals(DemoFloors.FLOOR_B_KEY, ignoreCase = true) == true
LaneBtn(
truckLanceCode = it.lance,
loadingSequence = if (is4F) it.loadingSequence else null,
@@ -1846,11 +1847,11 @@ return MessageResponse(
*/
private fun truckXPreferredFloorExistsSql(floorKey: String?): String {
val key = floorKey?.trim()?.uppercase(java.util.Locale.ROOT)?.replace("/", "").orEmpty()
if (key != "2F" && key != "4F") return ""
if (key != DemoFloors.FLOOR_A_KEY && key != DemoFloors.FLOOR_B_KEY) return ""
val (_, s4f) = doFloorSupplierSettingsService.loadDoFloorSupplierLists()
val in4 = s4f.joinToString(", ") { "'" + it.replace("'", "''") + "'" }
.ifEmpty { "'__NO_4F_SUPPLIER__'" }
val floorExpr = "CASE WHEN s.code IN ($in4) THEN '4F' ELSE '2F' END"
.ifEmpty { "'__NO_${DemoFloors.FLOOR_B_KEY}_SUPPLIER__'" }
val floorExpr = "CASE WHEN s.code IN ($in4) THEN '${DemoFloors.FLOOR_B_KEY}' ELSE '${DemoFloors.FLOOR_A_KEY}' END"
return """
AND EXISTS (
SELECT 1
@@ -1866,7 +1867,7 @@ return MessageResponse(
INNER JOIN delivery_order d2 ON d2.id = po2.doId AND d2.deleted = 0
LEFT JOIN shop s2 ON s2.id = d2.supplierId AND s2.deleted = 0
WHERE po2.deliveryOrderPickOrderId = dop.id AND po2.deleted = 0
AND (CASE WHEN s2.code IN ($in4) THEN '4F' ELSE '2F' END) <> '$key'
AND (CASE WHEN s2.code IN ($in4) THEN '${DemoFloors.FLOOR_B_KEY}' ELSE '${DemoFloors.FLOOR_A_KEY}' END) <> '$key'
)
""".trimIndent()
}
@@ -1915,8 +1916,8 @@ return MessageResponse(
)
if (!storeId.isNullOrBlank()) {
val sid = when (storeId) {
"2/F" -> "2/F"
"4/F" -> "4/F"
DemoFloors.FLOOR_A -> DemoFloors.FLOOR_A
DemoFloors.FLOOR_B -> DemoFloors.FLOOR_B
else -> storeId.trim()
}
sqlBuilder.append(" AND dop.storeId = :storeId ")
@@ -2389,7 +2390,7 @@ return MessageResponse(
try {
if (pickOrderId != null) {
// JO / Consumable prime with storeId=null; re-suggest must match or non-2F lots
// are filtered out after resolveWorkbenchSuggestionStoreId defaults to "2/F".
// are filtered out after resolveWorkbenchSuggestionStoreId defaults to DemoFloors.FLOOR_A.
val resolvedPoType = pickOrderRepository.findById(pickOrderId).orElse(null)?.type
val suggestionStoreId =
if (resolvedPoType == PickOrderType.JOB_ORDER ||
@@ -3066,7 +3067,7 @@ return MessageResponse(
emptyMap()
}
val sortedLines = when (doStoreFloorKey) {
"2F" -> exportLines.sortedWith(
DemoFloors.FLOOR_A_KEY -> exportLines.sortedWith(
compareBy(
{ row -> itemsById[row.line.itemId]?.item_Order ?: Int.MAX_VALUE },
{ row -> row.line.itemNo },


+ 10
- 9
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.deliveryOrder.service

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.web.models.ReleaseDoRequest
@@ -512,9 +513,9 @@ open class DoWorkbenchReleaseService(
first.truckLanceCode?.trim() == WORKBENCH_DEFAULT_TRUCK_LANCE_CODE
if (isDefaultTruckLane) return null
return when (first.preferredFloor) {
"2F" -> "2/F"
"4F" -> "4/F"
else -> "2/F"
DemoFloors.FLOOR_A_KEY -> DemoFloors.FLOOR_A
DemoFloors.FLOOR_B_KEY -> DemoFloors.FLOOR_B
else -> DemoFloors.FLOOR_A
}
}

@@ -553,7 +554,7 @@ open class DoWorkbenchReleaseService(
if (isDefaultTruckLane) {
WORKBENCH_TICKET_FLOOR_SEGMENT_DEFAULT_TRUCK
} else {
(storeId ?: "2/F").replace("/", "").trim()
(storeId ?: DemoFloors.FLOOR_A).replace("/", "").trim()
}

private data class MergeableWorkbenchTicket(
@@ -610,13 +611,13 @@ open class DoWorkbenchReleaseService(
preferredFloor: String?,
) {
val floor = when (preferredFloor?.trim()?.uppercase(java.util.Locale.ROOT)?.replace("/", "")) {
"4F" -> "4F"
else -> "2F"
DemoFloors.FLOOR_B_KEY -> DemoFloors.FLOOR_B_KEY
else -> DemoFloors.FLOOR_A_KEY
}
val (_, s4f) = doFloorSupplierSettingsService.loadDoFloorSupplierLists()
val in4 = s4f.joinToString(", ") { "'" + it.replace("'", "''") + "'" }
.ifEmpty { "'__NO_4F_SUPPLIER__'" }
val floorExpr = "CASE WHEN s.code IN ($in4) THEN '4F' ELSE '2F' END"
.ifEmpty { "'__NO_${DemoFloors.FLOOR_B_KEY}_SUPPLIER__'" }
val floorExpr = "CASE WHEN s.code IN ($in4) THEN '${DemoFloors.FLOOR_B_KEY}' ELSE '${DemoFloors.FLOOR_A_KEY}' END"
// Reject tickets that already have any DO on a different floor.
sql.append(
"""
@@ -1462,7 +1463,7 @@ open class DoWorkbenchReleaseService(
loadingSequence: Int?,
): String {
val storeNorm = normalizeStoreIdForLaneKey(storeId)
val is4F = storeNorm.replace("/", "").equals("4F", ignoreCase = true)
val is4F = storeNorm.replace("/", "").equals(DemoFloors.FLOOR_B_KEY, ignoreCase = true)
val seqPart = if (is4F) loadingSequence?.toString() ?: "" else ""
return listOf(
shopId?.toString() ?: "",


+ 5
- 4
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/TruckRoutingSummaryService.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.deliveryOrder.service

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus
import org.springframework.stereotype.Service
@@ -21,8 +22,8 @@ class TruckRoutingSummaryService(
return jdbcDao.queryForList(sql, emptyMap<String, Any>()).map { row ->
val rawValue = (row["value"] ?: "").toString().trim()
val label = when (rawValue.uppercase()) {
"2F", "2/F" -> "2/F"
"4F", "4/F" -> "4/F"
DemoFloors.FLOOR_A_KEY, DemoFloors.FLOOR_A -> DemoFloors.FLOOR_A
DemoFloors.FLOOR_B_KEY, DemoFloors.FLOOR_B -> DemoFloors.FLOOR_B
else -> rawValue
}
mapOf("label" to label, "value" to rawValue)
@@ -205,8 +206,8 @@ class TruckRoutingSummaryService(
if (raw.isNullOrBlank()) return ""
val normalized = raw.trim().replace("/", "")
return when (normalized.uppercase()) {
"2F" -> "2/F"
"4F" -> "4/F"
DemoFloors.FLOOR_A_KEY -> DemoFloors.FLOOR_A
DemoFloors.FLOOR_B_KEY -> DemoFloors.FLOOR_B
else -> raw.trim()
}
}


+ 5
- 4
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/WorkbenchTruckRoutingSummaryService.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.deliveryOrder.service

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus
import org.springframework.stereotype.Service
@@ -20,8 +21,8 @@ class WorkbenchTruckRoutingSummaryService(
return jdbcDao.queryForList(sql, emptyMap<String, Any>()).map { row ->
val rawValue = (row["value"] ?: "").toString().trim()
val label = when (rawValue.uppercase()) {
"2F", "2/F" -> "2/F"
"4F", "4/F" -> "4/F"
DemoFloors.FLOOR_A_KEY, DemoFloors.FLOOR_A -> DemoFloors.FLOOR_A
DemoFloors.FLOOR_B_KEY, DemoFloors.FLOOR_B -> DemoFloors.FLOOR_B
else -> rawValue
}
mapOf("label" to label, "value" to rawValue)
@@ -191,8 +192,8 @@ class WorkbenchTruckRoutingSummaryService(
if (raw.isNullOrBlank()) return ""
val normalized = raw.trim().replace("/", "")
return when (normalized.uppercase()) {
"2F" -> "2/F"
"4F" -> "4/F"
DemoFloors.FLOOR_A_KEY -> DemoFloors.FLOOR_A
DemoFloors.FLOOR_B_KEY -> DemoFloors.FLOOR_B
else -> raw.trim()
}
}


+ 2
- 1
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/AssignByStoreRequest.kt ファイルの表示

@@ -1,6 +1,7 @@
package com.ffii.fpsms.modules.deliveryOrder.web.models
import com.ffii.fpsms.modules.common.demo.DemoFloors

data class AssignByStoreRequest(
val storeId: String, // "2/F" or "4/F"
val storeId: String, // DemoFloors.FLOOR_A or DemoFloors.FLOOR_B
val assignTo: Long
)

+ 4
- 26
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchPickConstants.kt ファイルの表示

@@ -6,32 +6,10 @@ package com.ffii.fpsms.modules.jobOrder.service
* [DEFAULT_EXCLUDE_WAREHOUSE_CODES] applies on **assign / first prime** ([JoWorkbenchMainService])
* and on **scan-pick re-suggest** ([com.ffii.fpsms.modules.deliveryOrder.service.DoWorkbenchMainService]).
* JO re-suggest keeps [storeId] null (same as assign); it does not use DO floor store resolution.
*
* These bins are rewritten to LOC-X01..LOC-X24 by scripts/anonymize_demo_copy.sql.
*/
object JoWorkbenchPickConstants {
val DEFAULT_EXCLUDE_WAREHOUSE_CODES: Set<String> = setOf(
"4F-W402-01-00",
"4F-W402-02-00",
"4F-W402-03-00",
"4F-W402-04-00",
"4F-W402-05-00",
"4F-W402-#A-00",
"4F-W402-#B-00",
"4F-W402-#C-00",
"4F-W402-#D-00",
"4F-W402-#E-00",
"4F-W402-#F-00",
"4F-W402-#G-00",
"4F-W402-#H-00",
"4F-W402-#I-00",
"4F-W402-#J-00",
"4F-W402-#K-00",
"4F-W402-#L-00",
"4F-W402-#M-00",
"4F-W402-#N-00",
"4F-W402-#O-00",
"4F-W402-#P-00",
"4F-W402-#Q-00",
"4F-W402-#R-00",
"4F-W402-#S-00",
)
val DEFAULT_EXCLUDE_WAREHOUSE_CODES: Set<String> =
(1..24).map { "LOC-X%02d".format(it) }.toSet()
}

+ 12
- 11
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.jobOrder.service

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.core.exception.BadRequestException
import com.ffii.core.response.RecordsRes
import com.ffii.core.utils.CanonPrinterUtil
@@ -740,8 +741,8 @@ open class JobOrderService(
private fun validatePickRecordFloor(floor: String?): String {
val normalizedFloor = floor?.trim()?.uppercase()
?: throw BadRequestException("floor is required for pick record print")
if (normalizedFloor !in setOf("2F", "3F", "4F", "ALL")) {
throw BadRequestException("floor must be one of 2F, 3F, 4F, ALL")
if (normalizedFloor !in setOf(DemoFloors.FLOOR_A_KEY, DemoFloors.FLOOR_D_KEY, DemoFloors.FLOOR_B_KEY, "ALL")) {
throw BadRequestException("floor must be one of ${DemoFloors.FLOOR_A_KEY}, ${DemoFloors.FLOOR_D_KEY}, ${DemoFloors.FLOOR_B_KEY}, ALL")
}
return normalizedFloor
}
@@ -783,17 +784,17 @@ open class JobOrderService(
private fun updatePickOrderPlasticBoxCartonQty(pickOrderId: Long, floor: String, qty: Int) {
val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null) ?: return
when (floor) {
"2F" -> pickOrder.plasticBoxCartonQty2f = qty
"3F" -> pickOrder.plasticBoxCartonQty3f = qty
"4F" -> pickOrder.plasticBoxCartonQty4f = qty
DemoFloors.FLOOR_A_KEY -> pickOrder.plasticBoxCartonQty2f = qty
DemoFloors.FLOOR_D_KEY -> pickOrder.plasticBoxCartonQty3f = qty
DemoFloors.FLOOR_B_KEY -> pickOrder.plasticBoxCartonQty4f = qty
}
pickOrderRepository.save(pickOrder)
}

private fun persistAllFloorsPlasticBoxCartonQty(pickOrderId: Long, all: AllFloorsPlasticBoxCartonQty) {
updatePickOrderPlasticBoxCartonQty(pickOrderId, "2F", all.qty2f)
updatePickOrderPlasticBoxCartonQty(pickOrderId, "3F", all.qty3f)
updatePickOrderPlasticBoxCartonQty(pickOrderId, "4F", all.qty4f)
updatePickOrderPlasticBoxCartonQty(pickOrderId, DemoFloors.FLOOR_A_KEY, all.qty2f)
updatePickOrderPlasticBoxCartonQty(pickOrderId, DemoFloors.FLOOR_D_KEY, all.qty3f)
updatePickOrderPlasticBoxCartonQty(pickOrderId, DemoFloors.FLOOR_B_KEY, all.qty4f)
}

open fun getPickRecordPlasticBoxCartonQty(pickOrderId: Long): PickRecordPlasticBoxCartonQtyResponse {
@@ -839,10 +840,10 @@ open class JobOrderService(
val fields = mutableListOf<MutableMap<String, Any>>()
val params = mutableMapOf<String, Any>()
val normalizedFloor = request.floor?.trim()?.uppercase()
val selectedFloor = normalizedFloor?.takeIf { it in setOf("2F", "3F", "4F") }
val selectedFloor = normalizedFloor?.takeIf { it in setOf(DemoFloors.FLOOR_A_KEY, DemoFloors.FLOOR_D_KEY, DemoFloors.FLOOR_B_KEY) }
val floorLabel = when (normalizedFloor) {
"2F", "3F", "4F" -> normalizedFloor.replace("F", "/F")
"ALL" -> "2/F+3/F+4/F+N/A"
DemoFloors.FLOOR_A_KEY, DemoFloors.FLOOR_D_KEY, DemoFloors.FLOOR_B_KEY -> normalizedFloor.replace("F", "/F")
"ALL" -> "${DemoFloors.FLOOR_A}+${DemoFloors.FLOOR_D}+${DemoFloors.FLOOR_B}+N/A"
else -> ""
}



+ 2
- 1
src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.jobOrder.web

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.core.response.RecordsRes
import com.ffii.fpsms.modules.jobOrder.entity.projections.JobOrderDetail

@@ -318,7 +319,7 @@ fun getJobOrderPickOrderLotDetails(
@GetMapping("/AllJoPickOrder")
fun getAllJoPickOrder(
@RequestParam(name = "type", required = false) bomType: String?,
// Single floor, e.g. "2F"/"3F"/"4F". When provided, backend returns job pick orders
// Single floor, e.g. DemoFloors.FLOOR_A_KEY/DemoFloors.FLOOR_D_KEY/DemoFloors.FLOOR_B_KEY. When provided, backend returns job pick orders
// that still have unpicked lines on that floor OR any "no lot" remaining lines.
@RequestParam(required = false) floor: String?,
@RequestParam(name = "jobOrderCode", required = false) jobOrderCode: String?,


+ 2
- 1
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.jobOrder.web.model

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.fpsms.modules.jobOrder.enums.JobOrderBomMaterialStatus
import java.math.BigDecimal
import java.time.LocalDateTime
@@ -35,7 +36,7 @@ data class CreateJobOrderProcessRequest (
)

data class FloorPickCountDto(
val floor: String, // e.g. "2F", "4F"
val floor: String, // e.g. DemoFloors.FLOOR_A_KEY, DemoFloors.FLOOR_B_KEY
val finishedCount: Int,
val totalCount: Int
)


+ 2
- 4
src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt ファイルの表示

@@ -482,16 +482,14 @@ open class ProductionScheduleService(

return MessageResponse(
id = request.id.toLong(),
//message = "Successfully created Job Orders for all production schedule lines (Parent ID: ${request.id}).",
message = if (jobOrderCount == 0)
"沒有建立新工單(可能因為成品已有生產中的工單)."
else
"成功建立 $jobOrderCount 張工單!",
name = "",
code = "",
type = "",
code = jobOrderCount.toString(),
type = "psJobOrders",
errorPosition = null
// entity = mapOf("prodScheduleLines" to latestDetail?.prodScheduleLines)
)
}



+ 3
- 2
src/main/java/com/ffii/fpsms/modules/pickOrder/service/HierarchicalFgPayloadAssembler.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.pickOrder.service

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.core.support.JdbcDao
import java.util.Locale
import kotlin.system.measureTimeMillis
@@ -290,7 +291,7 @@ ORDER BY
?: ""
val sortMs = measureTimeMillis {
when (doStoreFloorKey) {
"2F" -> {
DemoFloors.FLOOR_A_KEY -> {
allPickOrderLines.sortWith(
compareBy(
{ line ->
@@ -304,7 +305,7 @@ ORDER BY
),
)
}
"4F" -> {
DemoFloors.FLOOR_B_KEY -> {
}
else -> {
allPickOrderLines.sortWith(


+ 13
- 12
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.pickOrder.service

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.core.response.RecordsRes
import com.ffii.core.support.AbstractBaseEntityService
import com.ffii.core.support.JdbcDao
@@ -3552,10 +3553,10 @@ AND (spl.pickOrderLineId IS NOT NULL OR sol.pickOrderLineId IS NOT NULL)
ORDER BY
CASE WHEN sol.status = 'rejected' THEN 0 ELSE 1 END,
COALESCE(w.`order`, 999999) ASC, -- 改用 warehouse.order
-- Add frontpart (floor) sorting: 1F > 2F/4F
-- Add frontpart (floor) sorting: ${DemoFloors.FLOOR_C_KEY} > ${DemoFloors.FLOOR_A_KEY}/${DemoFloors.FLOOR_B_KEY}
CASE
WHEN w.store_id = '1F' THEN 1
WHEN w.store_id IN ('2F', '4F') THEN 2
WHEN REPLACE(IFNULL(w.store_id, ''), '/', '') = '${DemoFloors.FLOOR_C_KEY}' THEN 1
WHEN REPLACE(IFNULL(w.store_id, ''), '/', '') IN ('${DemoFloors.FLOOR_A_KEY}', '${DemoFloors.FLOOR_B_KEY}') THEN 2
ELSE 3
END ASC,
po.code ASC,
@@ -3937,13 +3938,13 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto
allPickOrderLines.addAll(pickOrderLines)
}
// 合并到总列表
// DB 里 store_id 可能是 "2/F"、"4/F",与字面 "2F" 不一致会导致分支走错
// DB 里 store_id 可能是 DemoFloors.FLOOR_A、DemoFloors.FLOOR_B,与字面 DemoFloors.FLOOR_A_KEY 不一致会导致分支走错
val doStoreFloorKey = doPickOrderInfo["store_id"]?.toString()?.trim()?.uppercase(Locale.ROOT)
?.replace("/", "")
?.replace(" ", "")
?: ""
when (doStoreFloorKey) {
"2F" -> {
DemoFloors.FLOOR_A_KEY -> {
// 2F:按 items.item_Order 排序(INT,NULL 置后)
allPickOrderLines.sortWith(
compareBy(
@@ -3958,7 +3959,7 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto
)
)
}
"4F" -> {
DemoFloors.FLOOR_B_KEY -> {
// 4F:旧逻辑 — 保持 SQL 顺序(COALESCE(w.`order`, 999999), pol.id, lotNo),不二次排序
}
else -> {
@@ -4290,7 +4291,7 @@ open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, A
?.replace(" ", "")
?: ""
when (doStoreFloorKey) {
"2F" -> {
DemoFloors.FLOOR_A_KEY -> {
allPickOrderLines.sortWith(
compareBy(
{ line ->
@@ -4304,7 +4305,7 @@ open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, A
)
)
}
"4F" -> {
DemoFloors.FLOOR_B_KEY -> {
// 4F:保留 SQL 順序,不做二次排序
}
else -> {
@@ -5149,10 +5150,10 @@ open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, A
is String -> {
val parts = routerIndex.split("-")
if (parts.isNotEmpty()) {
val floorPart = parts[0].uppercase() // "1F", "2F", "4F"
val floorPart = parts[0].uppercase() // DemoFloors.FLOOR_C_KEY, DemoFloors.FLOOR_A_KEY, DemoFloors.FLOOR_B_KEY
when (floorPart) {
"1F" -> 1
"2F", "4F" -> 2
DemoFloors.FLOOR_C_KEY -> 1
DemoFloors.FLOOR_A_KEY, DemoFloors.FLOOR_B_KEY -> 2
else -> 3
}
} else {
@@ -5164,7 +5165,7 @@ open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, A
floorSortValue
},
{ line ->
// 提取数字部分:格式为 "store_id-number" (如 "2F-004")
// 提取数字部分:格式为 "store_id-number" (如 "${DemoFloors.FLOOR_A_KEY}-004")
val firstLot = line.lots.firstOrNull()
val routerIndex = firstLot?.router?.index
when (routerIndex) {


+ 2
- 1
src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckLaneScheduleExcelSupport.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.pickOrder.service

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.fpsms.modules.pickOrder.entity.Truck
import com.ffii.fpsms.modules.pickOrder.entity.TruckRepository
import com.ffii.fpsms.modules.pickOrder.web.models.TruckLaneScheduleExcelPreviewRow
@@ -80,7 +81,7 @@ class TruckLaneScheduleExcelSupport(

val storeRaw = colIndex["targetstoreid"]?.let { cellString(row, it) }.orEmpty()
val storeId = truckService.normalizeRouteStoreId(
if (storeRaw.isBlank()) "2F" else storeRaw,
if (storeRaw.isBlank()) DemoFloors.FLOOR_A_KEY else storeRaw,
)
val remark = colIndex["targetremark"]?.let { cellString(row, it) }
?.trim()?.takeIf { it.isNotEmpty() }


+ 17
- 16
src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckService.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.pickOrder.service

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.core.support.AbstractBaseEntityService
import com.ffii.core.support.JdbcDao
import com.ffii.core.utils.ExcelUtils
@@ -146,7 +147,7 @@ open class TruckService(
storeId: String,
remark: String?,
): List<Truck> {
val bucketRemark = if (storeId == "4F") remark?.trim()?.takeIf { it.isNotEmpty() } else null
val bucketRemark = if (storeId == DemoFloors.FLOOR_B_KEY) remark?.trim()?.takeIf { it.isNotEmpty() } else null
val trimmed = bucketRemark?.trim().orEmpty()
val blankRemark = trimmed.isEmpty()
return truckRepository.findAllByTruckLanceCodeAndRemarkAndDeletedFalse(
@@ -384,9 +385,9 @@ open class TruckService(
val remark = ExcelUtils.getStringValue(row.getCell(COLUMN_REMARK_INDEX)).trim()

val storeIdInt = when (store_id.uppercase()) {
"2F" -> 2
"4F" -> 4
"3F" -> 3
DemoFloors.FLOOR_A_KEY -> 2
DemoFloors.FLOOR_B_KEY -> 4
DemoFloors.FLOOR_D_KEY -> 3
else -> {
logger.warn("Invalid store_id '${store_id}', defaulting to 2")
2
@@ -482,11 +483,11 @@ open class TruckService(

open fun normalizeRouteStoreId(storeId: String?): String {
val s = storeId?.trim()?.uppercase().orEmpty()
return if (s == "4F") "4F" else "2F"
return if (s == DemoFloors.FLOOR_B_KEY) DemoFloors.FLOOR_B_KEY else DemoFloors.FLOOR_A_KEY
}

open fun bucketRemarkForStore(storeId: String, remark: String?): String? {
return if (normalizeRouteStoreId(storeId) == "4F") {
return if (normalizeRouteStoreId(storeId) == DemoFloors.FLOOR_B_KEY) {
remark?.trim()?.takeIf { it.isNotEmpty() }
} else {
null
@@ -630,7 +631,7 @@ open class TruckService(
?: targetBucket.first()
val storeNorm = normalizeRouteStoreId(to.storeId)
val toRemark =
if (storeNorm == "4F") to.remark?.trim()?.takeIf { it.isNotEmpty() } else null
if (storeNorm == DemoFloors.FLOOR_B_KEY) to.remark?.trim()?.takeIf { it.isNotEmpty() } else null
val resolvedDeparture =
departureTime ?: template.departureTime ?: truck.departureTime ?: LocalTime.of(0, 0)

@@ -710,8 +711,8 @@ open class TruckService(
updateTruckLance.districtReference = request.districtReference
updateTruckLance.departureTime = request.departureTime
updateTruckLance.storeId = request.storeId
// Only set remark if storeId is "4F", otherwise set to null
updateTruckLance.remark = if (request.storeId == "4F") request.remark else null
// Only set remark if storeId is DemoFloors.FLOOR_B_KEY, otherwise set to null
updateTruckLance.remark = if (request.storeId == DemoFloors.FLOOR_B_KEY) request.remark else null
if (request.updateLogistic) {
updateTruckLance.logistic = logisticRefOrNull(request.logisticId)
}
@@ -766,8 +767,8 @@ open class TruckService(
this.shopCode = request.shopCode
this.loadingSequence = request.loadingSequence
this.districtReference = request.districtReference
// Only set remark if store_id is "4F", otherwise set to null
this.remark = if (request.store_id == "4F") request.remark else null
// Only set remark if store_id is DemoFloors.FLOOR_B_KEY, otherwise set to null
this.remark = if (request.store_id == DemoFloors.FLOOR_B_KEY) request.remark else null
this.logistic = logisticRefOrNull(request.logisticId)
}

@@ -850,9 +851,9 @@ open class TruckService(
// Update loadingSequence
truck.loadingSequence = request.loadingSequence
// Update remark - only set if storeId is "4F", otherwise set to null
// Update remark - only set if storeId is DemoFloors.FLOOR_B_KEY, otherwise set to null
val storeIdStr = truck.storeId
if (storeIdStr == "4F") {
if (storeIdStr == DemoFloors.FLOOR_B_KEY) {
truck.remark = request.remark
} else {
truck.remark = null
@@ -909,8 +910,8 @@ open class TruckService(
this.shopCode = null
this.loadingSequence = request.loadingSequence
this.districtReference = request.districtReference
// Only set remark if store_id is "4F", otherwise set to null
this.remark = if (request.store_id == "4F") request.remark else null
// Only set remark if store_id is DemoFloors.FLOOR_B_KEY, otherwise set to null
this.remark = if (request.store_id == DemoFloors.FLOOR_B_KEY) request.remark else null
this.logistic = logisticRefOrNull(request.logisticId)
}

@@ -1860,7 +1861,7 @@ open class TruckService(
}

val effectiveRemark =
if (storeId == "4F") {
if (storeId == DemoFloors.FLOOR_B_KEY) {
when {
scheduleRemark.isNotEmpty() -> scheduleRemark
laneRemark != null && laneRemark.isNotEmpty() -> laneRemark


+ 2
- 1
src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.report.service

import com.ffii.fpsms.modules.common.demo.DemoFloors
import org.springframework.stereotype.Service
import net.sf.jasperreports.engine.*
import net.sf.jasperreports.engine.data.JRMapCollectionDataSource
@@ -166,7 +167,7 @@ FORMAT(ROUND(IFNULL(IFNULL(sol.qty, dol.qty), 0), 0), 0) AS qty,
(${supplierFloorSqlCases.floorStringCase}
AND (SELECT COUNT(*) FROM truck t3
WHERE t3.shopId = do.shopId AND t3.deleted = 0
AND t3.Store_id = '4F') > 1
AND t3.Store_id = '${DemoFloors.FLOOR_B_KEY}') > 1
AND IFNULL(dopo.requiredDeliveryDate, do.estimatedArrivalDate) IS NOT NULL
AND t2.TruckLanceCode LIKE CONCAT('%',
CASE DAYNAME(IFNULL(dopo.requiredDeliveryDate, do.estimatedArrivalDate))


+ 2
- 1
src/main/java/com/ffii/fpsms/modules/report/service/StockTakeVarianceReportService.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.report.service

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.core.support.JdbcDao
import org.springframework.stereotype.Service
import java.time.LocalDate
@@ -440,7 +441,7 @@ ORDER BY
""
} else {
args["storeId"] = normalized
// DB 里 store_id 可能是 "2/F" 或 "2F";用 REPLACE 去斜線做匹配
// DB 里 store_id 可能是 DemoFloors.FLOOR_A 或 DemoFloors.FLOOR_A_KEY;用 REPLACE 去斜線做匹配
"""
AND REPLACE(COALESCE(wh.store_id, ''), '/', '') = REPLACE(:storeId, '/', '')
""".trimIndent()


+ 11
- 10
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.stock.service

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.fpsms.modules.master.service.ItemUomService
import com.ffii.fpsms.modules.pickOrder.entity.PickOrder

@@ -107,8 +108,8 @@ open class SuggestedPickLotService(
val suggestedList: MutableList<SuggestedPickLot> = mutableListOf()
val holdQtyMap: MutableMap<Long?, BigDecimal?> = request.holdQtyMap
val excludedWarehouseCodes = setOf<String>(
"2F-W202-01-00",
"2F-W200-#A-00",
DemoFloors.LOC_HOLD,
DemoFloors.LOC_WIP,
)
val availableInventoryLotLines = inventoryLotLineService
.allInventoryLotLinesByItemIdIn(itemIds)
@@ -180,11 +181,11 @@ open class SuggestedPickLotService(
) {
return@forEachIndexed
}
if (warehouseStoreId == "3F" && isDeliveryOrderPick) {
if (warehouseStoreId == DemoFloors.FLOOR_D_KEY && isDeliveryOrderPick) {
return@forEachIndexed
}
if (doPreferredFloor != null &&
warehouseStoreId != "1F" &&
warehouseStoreId != DemoFloors.FLOOR_C_KEY &&
warehouseStoreId != doPreferredFloor
) {
// 例:首选 2F,但是 lot 在 4F -> 跳过
@@ -290,8 +291,8 @@ open class SuggestedPickLotService(
val suggestedList: MutableList<SuggestedPickLot> = mutableListOf()
val holdQtyMap: MutableMap<Long?, BigDecimal?> = request.holdQtyMap
val excludedWarehouseCodes = setOf<String>(
"2F-W202-01-00",
// "4F-W401-#C-00",
DemoFloors.LOC_HOLD,
// "${DemoFloors.FLOOR_B_KEY}-W401-#C-00",
)
// get current inventory lot line qty & grouped by item id
val availableInventoryLotLines = inventoryLotLineService
@@ -316,7 +317,7 @@ open class SuggestedPickLotService(
val lotEntityById = lotEntities.associateBy { it.id }
// 计算“默认优先仓位”(短期固定,后续可从 Items/配置取)
val preferredWarehouseCode = if (isWipItem) "2F-W200-#A-00" else null
val preferredWarehouseCode = if (isWipItem) DemoFloors.LOC_WIP else null
// 分组:先默认仓位,再其他;组内保持 FEFO 顺序(lotInfos 已按 expiry 排过序)
val (preferred, others) = if (!preferredWarehouseCode.isNullOrBlank()) {
@@ -343,7 +344,7 @@ open class SuggestedPickLotService(
// 既有规则
if (warehouseCode != null && excludedWarehouseCodes.contains(warehouseCode)) continue
val isJobOrder = line.pickOrder?.type?.value == "jo"
if (storeId == "3F" && !isJobOrder) continue
if (storeId == DemoFloors.FLOOR_D_KEY && !isJobOrder) continue
val availableQtyInBase = (lot.inQty ?: BigDecimal.ZERO)
.minus(lot.outQty ?: BigDecimal.ZERO)
@@ -1285,7 +1286,7 @@ private fun generateOptimalSuggestionsForAllPickOrders(
lotEntities.forEach { lot ->
if (remainingPickOrderLines.isEmpty()) return@forEach
val warehouseStoreId=lot.warehouse?.store_id
if (warehouseStoreId == "3F" && isDeliveryOrderContext) {
if (warehouseStoreId == DemoFloors.FLOOR_D_KEY && isDeliveryOrderContext) {
return@forEach
}
val totalQty = lot.inQty ?: zero
@@ -1574,7 +1575,7 @@ private fun generateCorrectSuggestionsWithExistingHolds(pickOrder: PickOrder): L
val warehouseStoreId=lot.warehouse?.store_id
// 3F filter only for delivery order
if (warehouseStoreId == "3F" && pickOrder.type?.value == "do") {
if (warehouseStoreId == DemoFloors.FLOOR_D_KEY && pickOrder.type?.value == "do") {
return@forEach
}
val totalQty = lot.inQty ?: zero


+ 3
- 2
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotWorkbenchService.kt ファイルの表示

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.stock.service

import com.ffii.fpsms.modules.common.demo.DemoFloors
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLine
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLineRepository
import com.ffii.fpsms.modules.stock.entity.InventoryLotLine
@@ -133,8 +134,8 @@ open class SuggestedPickLotWorkbenchService(
* Keep it empty if you don't need any default excludes.
*/
private val defaultExcludeWarehouseCodes: Set<String> = setOf(
"2F-W202-01-00",
"2F-W200-#A-00",
DemoFloors.LOC_HOLD,
DemoFloors.LOC_WIP,
)
private fun normalizeStoreId(raw: String?): String? {
val s = raw?.trim()?.uppercase() ?: return null


+ 2
- 2
src/main/resources/application-db-demo.yml ファイルの表示

@@ -5,10 +5,10 @@ spring:
username: fpsms_demo
password: FpsmsDemoOnly2026

# Demo sandbox only. Do not point this profile at fpsmsdb.
# Demo copy only. Do not point this profile at fpsmsdb.
fpsms:
demo-mode: true
# Slide DEMO- expiry and delivery dates forward so sample rows stay on today's screens.
# Dates stay as copied from production. This cron is unused.
demo:
date-refresh-cron: "0 10 0 * * *"
monitoring:


+ 2
- 483
src/main/resources/demo/demo-coverage.sql ファイルの表示

@@ -1,484 +1,2 @@

SET @user := (SELECT id FROM user WHERE username = 'demo' LIMIT 1);
SET @kg := (SELECT id FROM uom_conversion WHERE code = 'KG' LIMIT 1);
SET @pcs := (SELECT id FROM uom_conversion WHERE code = 'PCS' LIMIT 1);
SET @cur := (SELECT id FROM currency WHERE code = 'HKD' LIMIT 1);
SET @sup := (SELECT id FROM shop WHERE code = 'DEMO-SUP' LIMIT 1);
SET @shp := (SELECT id FROM shop WHERE code = 'DEMO-SHP' LIMIT 1);
SET @log := (SELECT id FROM m18_data_log WHERE refType = 'DEMO' LIMIT 1);
SET @whA := (SELECT id FROM warehouse WHERE code = 'DEMO-A' LIMIT 1);
SET @whB := (SELECT id FROM warehouse WHERE code = 'DEMO-B' LIMIT 1);
SET @flour := (SELECT id FROM items WHERE code = 'DEMO-RM-01' LIMIT 1);
SET @milk := (SELECT id FROM items WHERE code = 'DEMO-RM-02' LIMIT 1);
SET @loaf := (SELECT id FROM items WHERE code = 'DEMO-FG-01' LIMIT 1);
SET @bun := (SELECT id FROM items WHERE code = 'DEMO-FG-02' LIMIT 1);
SET @po3 := (SELECT id FROM purchase_order WHERE code = 'DEMO-PO-003' LIMIT 1);
SET @do3 := (SELECT id FROM delivery_order WHERE code = 'DEMO-DO-003' LIMIT 1);
SET @pkDo3 := (SELECT id FROM pick_order WHERE code = 'DEMO-PK-DO3' LIMIT 1);
SET @pkJo2 := (SELECT id FROM pick_order WHERE code = 'DEMO-PK-JO2' LIMIT 1);
SET @bom := (SELECT id FROM bom WHERE code = 'DEMO-BOM-01' LIMIT 1);

UPDATE warehouse
SET store_id = '2F', warehouse = 'W201', area = '#A', slot = '01', stockTakeSection = 'A', stockTakeSectionDescription = '甲倉區'
WHERE code = 'DEMO-A';

UPDATE warehouse
SET store_id = '4F', warehouse = 'W202', area = '#B', slot = '02', stockTakeSection = 'B', stockTakeSectionDescription = '乙倉區'
WHERE code = 'DEMO-B';

INSERT INTO settings (name, value, category, type)
SELECT 'DO.floor.suppliers.2F', 'DEMO-SUP', 'DO', 'csv'
WHERE NOT EXISTS (SELECT 1 FROM settings WHERE name = 'DO.floor.suppliers.2F');

INSERT INTO settings (name, value, category, type)
SELECT 'DO.floor.suppliers.4F', 'DEMO-SUP', 'DO', 'csv'
WHERE NOT EXISTS (SELECT 1 FROM settings WHERE name = 'DO.floor.suppliers.4F');

INSERT INTO item_category (code, parent, sub, temperature)
SELECT 'DEMO-CAT-BAKE', '食品', '烘焙', '常溫'
WHERE NOT EXISTS (SELECT 1 FROM item_category WHERE code = 'DEMO-CAT-BAKE');

INSERT INTO item_category (code, parent, sub, temperature)
SELECT 'DEMO-CAT-DAIRY', '食品', '乳品', '冷藏'
WHERE NOT EXISTS (SELECT 1 FROM item_category WHERE code = 'DEMO-CAT-DAIRY');

SET @catBake := (SELECT id FROM item_category WHERE code = 'DEMO-CAT-BAKE' LIMIT 1);
SET @catDairy := (SELECT id FROM item_category WHERE code = 'DEMO-CAT-DAIRY' LIMIT 1);

UPDATE items SET categoryId = @catBake WHERE code IN ('DEMO-RM-01', 'DEMO-SF-01', 'DEMO-FG-01', 'DEMO-FG-02') AND categoryId IS NULL;
UPDATE items SET categoryId = @catDairy WHERE code = 'DEMO-RM-02' AND categoryId IS NULL;

INSERT INTO shop (code, name, type, contactName, contactNo, addr1, district, m18Id, m18LastModifyDate)
SELECT 'DEMO-SUP2', '東岸糖廠', 'supplier', '陳小姐', '21110002', '示範大道 8 號', '九龍', 910031, NOW()
WHERE NOT EXISTS (SELECT 1 FROM shop WHERE code = 'DEMO-SUP2');

INSERT INTO shop (code, name, type, contactName, contactNo, addr1, district, m18Id, m18LastModifyDate)
SELECT 'DEMO-SHP2', '南風麵包店', 'shop', '林先生', '51110002', '海旁街 18 號', '港島', 910032, NOW()
WHERE NOT EXISTS (SELECT 1 FROM shop WHERE code = 'DEMO-SHP2');

INSERT INTO warehouse (code, name, description, capacity, `order`, warehouse, area, slot, store_id, stockTakeSection, stockTakeSectionDescription)
SELECT 'DEMO-C', '丙倉', '示範貨架丙', 800, '3', 'W203', '#C', '03', '2F', 'C', '丙倉區'
WHERE NOT EXISTS (SELECT 1 FROM warehouse WHERE code = 'DEMO-C');

INSERT INTO user (username, password, locked, name, title, staffNo, lotusNotesUser)
SELECT 'demo.stock', u.password, 0, '示範倉務', '倉務', 'demo.stock', 0
FROM user u
WHERE u.username = 'demo'
AND NOT EXISTS (SELECT 1 FROM user x WHERE x.username = 'demo.stock');

INSERT INTO user (username, password, locked, name, title, staffNo, lotusNotesUser)
SELECT 'demo.prod', u.password, 0, '示範出品', '出品', 'demo.prod', 0
FROM user u
WHERE u.username = 'demo'
AND NOT EXISTS (SELECT 1 FROM user x WHERE x.username = 'demo.prod');

INSERT INTO user_authority (userId, authId)
SELECT u.id, a.id
FROM user u
JOIN authority a ON a.authority = 'STOCK'
WHERE u.username = 'demo.stock'
AND NOT EXISTS (
SELECT 1 FROM user_authority ua WHERE ua.userId = u.id AND ua.authId = a.id
);

INSERT INTO user_authority (userId, authId)
SELECT u.id, a.id
FROM user u
JOIN authority a ON a.authority IN ('PROD', 'PACK')
WHERE u.username = 'demo.prod'
AND NOT EXISTS (
SELECT 1 FROM user_authority ua WHERE ua.userId = u.id AND ua.authId = a.id
);

INSERT INTO equipment (code, name, description)
SELECT 'DEMO-EQ-01', '示範烤箱', '示範用熱風烤箱'
WHERE NOT EXISTS (SELECT 1 FROM equipment WHERE code = 'DEMO-EQ-01');

INSERT INTO equipment_detail (code, name, description, equipmentCode)
SELECT 'DEMO-EQD-01', '烤箱一號', '二樓示範烤箱', 'DEMO-EQ-01'
WHERE NOT EXISTS (SELECT 1 FROM equipment_detail WHERE code = 'DEMO-EQD-01');

INSERT INTO truck (TruckLanceCode, DepartureTime, shopId, ShopName, ShopCode, LoadingSequence, Store_id)
SELECT 'DEMO-AM', '08:00:00', @shp, '海港咖啡店', 'DEMO-SHP', 1, '2F'
WHERE NOT EXISTS (SELECT 1 FROM truck WHERE TruckLanceCode = 'DEMO-AM');

INSERT INTO truck (TruckLanceCode, DepartureTime, shopId, ShopName, ShopCode, LoadingSequence, Store_id)
SELECT 'DEMO-PM', '14:30:00', @shp, '海港咖啡店', 'DEMO-SHP', 2, '4F'
WHERE NOT EXISTS (SELECT 1 FROM truck WHERE TruckLanceCode = 'DEMO-PM');

INSERT INTO qc_category (code, name, isDefault)
SELECT 'DEMO-QC', '示範品質檢查', 1
WHERE NOT EXISTS (SELECT 1 FROM qc_category WHERE code = 'DEMO-QC');

SET @qcCat := (SELECT id FROM qc_category WHERE code = 'DEMO-QC' LIMIT 1);

INSERT INTO qc_item (code, name, description)
SELECT 'DEMO-QC-TEMP', '溫度', '入倉溫度'
WHERE NOT EXISTS (SELECT 1 FROM qc_item WHERE code = 'DEMO-QC-TEMP');

INSERT INTO qc_item (code, name, description)
SELECT 'DEMO-QC-LOOK', '外觀', '包裝與外觀'
WHERE NOT EXISTS (SELECT 1 FROM qc_item WHERE code = 'DEMO-QC-LOOK');

SET @qcTemp := (SELECT id FROM qc_item WHERE code = 'DEMO-QC-TEMP' LIMIT 1);
SET @qcLook := (SELECT id FROM qc_item WHERE code = 'DEMO-QC-LOOK' LIMIT 1);

INSERT INTO qc_item_category (qcItemId, qcCategoryId, `order`, description)
SELECT @qcTemp, @qcCat, 1, '溫度須低於 8 度'
WHERE NOT EXISTS (
SELECT 1 FROM qc_item_category WHERE qcItemId = @qcTemp AND qcCategoryId = @qcCat
);

INSERT INTO qc_item_category (qcItemId, qcCategoryId, `order`, description)
SELECT @qcLook, @qcCat, 2, '包裝完整'
WHERE NOT EXISTS (
SELECT 1 FROM qc_item_category WHERE qcItemId = @qcLook AND qcCategoryId = @qcCat
);

INSERT INTO items_qc_category_mapping (itemId, qcCategoryId, type)
SELECT @flour, @qcCat, 'IQC'
WHERE NOT EXISTS (
SELECT 1 FROM items_qc_category_mapping WHERE itemId = @flour AND type = 'IQC'
);

INSERT INTO items_qc_category_mapping (itemId, qcCategoryId, type)
SELECT @loaf, @qcCat, 'EPQC'
WHERE NOT EXISTS (
SELECT 1 FROM items_qc_category_mapping WHERE itemId = @loaf AND type = 'EPQC'
);

INSERT INTO items (code, name, type, description, categoryId, m18Id, m18LastModifyDate)
SELECT 'DEMO-MAT-01', '高筋麵粉', 'MAT', '報告用材料', @catBake, 910301, NOW()
WHERE NOT EXISTS (SELECT 1 FROM items WHERE code = 'DEMO-MAT-01');

INSERT INTO items (code, name, type, description, categoryId, m18Id, m18LastModifyDate)
SELECT 'DEMO-WIP-01', '發酵麵團', 'WIP', '報告用半成品', @catBake, 910302, NOW()
WHERE NOT EXISTS (SELECT 1 FROM items WHERE code = 'DEMO-WIP-01');

INSERT INTO items (code, name, type, description, categoryId, m18Id, m18LastModifyDate)
SELECT 'DEMO-FG-03', '牛油包', 'FG', '報告用成品', @catBake, 910303, NOW()
WHERE NOT EXISTS (SELECT 1 FROM items WHERE code = 'DEMO-FG-03');

INSERT INTO items (code, name, type, description, categoryId, m18Id, m18LastModifyDate)
SELECT 'DEMO-NM-01', '包裝袋', 'NM', '報告用非耗材', @catBake, 910304, NOW()
WHERE NOT EXISTS (SELECT 1 FROM items WHERE code = 'DEMO-NM-01');

INSERT INTO items (code, name, type, description, categoryId, m18Id, m18LastModifyDate)
SELECT 'DEMO-CMB-01', '早餐套餐', 'CMB', '報告用組合', @catBake, 910305, NOW()
WHERE NOT EXISTS (SELECT 1 FROM items WHERE code = 'DEMO-CMB-01');

SET @mat := (SELECT id FROM items WHERE code = 'DEMO-MAT-01' LIMIT 1);
SET @wip := (SELECT id FROM items WHERE code = 'DEMO-WIP-01' LIMIT 1);
SET @fg3 := (SELECT id FROM items WHERE code = 'DEMO-FG-03' LIMIT 1);
SET @nm := (SELECT id FROM items WHERE code = 'DEMO-NM-01' LIMIT 1);
SET @cmb := (SELECT id FROM items WHERE code = 'DEMO-CMB-01' LIMIT 1);

INSERT INTO item_uom (uomId, itemId, baseUnit, stockUnit, pickingUnit, salesUnit, purchaseUnit, ratioN, ratioD, price, currencyId, m18Id, m18LastModifyDate)
SELECT @kg, i.id, 1, 1, 1, 1, 1, 1, 1, 6.50, @cur, 910400 + i.id, NOW()
FROM items i
WHERE i.code IN ('DEMO-MAT-01', 'DEMO-WIP-01', 'DEMO-NM-01')
AND NOT EXISTS (SELECT 1 FROM item_uom iu WHERE iu.itemId = i.id AND iu.stockUnit = 1 AND iu.deleted = 0);

INSERT INTO item_uom (uomId, itemId, baseUnit, stockUnit, pickingUnit, salesUnit, purchaseUnit, ratioN, ratioD, price, currencyId, m18Id, m18LastModifyDate)
SELECT @pcs, i.id, 1, 1, 1, 1, 1, 1, 1, 12.00, @cur, 910500 + i.id, NOW()
FROM items i
WHERE i.code IN ('DEMO-FG-03', 'DEMO-CMB-01')
AND NOT EXISTS (SELECT 1 FROM item_uom iu WHERE iu.itemId = i.id AND iu.stockUnit = 1 AND iu.deleted = 0);

INSERT INTO bom (code, name, description, itemId, outputQty, outputQtyUom, type, status, m18Id, m18LastModifyDate)
SELECT 'DEMO-FG-01', '鄉村麵包', 'FG', @loaf, 10, 'PCS', 'FG', 'active', 910211, NOW()
WHERE NOT EXISTS (SELECT 1 FROM bom WHERE code = 'DEMO-FG-01');

INSERT INTO bom (code, name, description, itemId, outputQty, outputQtyUom, type, status, m18Id, m18LastModifyDate)
SELECT 'DEMO-FG-03', '牛油包', 'FG', @fg3, 12, 'PCS', 'FG', 'active', 910212, NOW()
WHERE NOT EXISTS (SELECT 1 FROM bom WHERE code = 'DEMO-FG-03');

INSERT INTO bom (code, name, description, itemId, outputQty, outputQtyUom, type, status, m18Id, m18LastModifyDate)
SELECT 'DEMO-WIP-01', '發酵麵團', 'WIP', @wip, 8, 'KG', 'WIP', 'active', 910213, NOW()
WHERE NOT EXISTS (SELECT 1 FROM bom WHERE code = 'DEMO-WIP-01');

UPDATE stock_in_line
SET dnNo = 'DEMO-DN-FL1', productionDate = DATE_SUB(NOW(), INTERVAL 3 DAY), productLotNo = 'PLOT-FL1'
WHERE lotNo = 'DEMO-LOT-FL1';

UPDATE stock_in_line
SET dnNo = 'DEMO-DN-MK1', productionDate = DATE_SUB(NOW(), INTERVAL 3 DAY), productLotNo = 'PLOT-MK1'
WHERE lotNo = 'DEMO-LOT-MK1';

UPDATE stock_in_line
SET dnNo = 'DEMO-DN-LF1', productionDate = DATE_SUB(NOW(), INTERVAL 2 DAY), productLotNo = 'PLOT-LF1'
WHERE lotNo = 'DEMO-LOT-LF1';

UPDATE stock_in_line
SET dnNo = 'DEMO-DN-LF2', productionDate = DATE_SUB(NOW(), INTERVAL 1 DAY)
WHERE lotNo = 'DEMO-LOT-LF2';

UPDATE purchase_order_line pol
JOIN items i ON i.id = pol.itemId
SET pol.up = CASE i.code WHEN 'DEMO-RM-01' THEN 8.50 WHEN 'DEMO-RM-02' THEN 15.00 ELSE pol.up END
WHERE pol.purchaseOrderId = @po3;

INSERT INTO m18_goods_receipt_note_log (m18_record_id, grn_code, stock_in_line_id, purchase_order_line_id, purchase_order_id, po_code, status, message)
SELECT 910501, 'DEMO-GRN-001', sil.id, sil.purchaseOrderLineId, sil.purchaseOrderId, 'DEMO-PO-003', 1, '示範入倉'
FROM stock_in_line sil
WHERE sil.lotNo = 'DEMO-LOT-FL1'
AND NOT EXISTS (SELECT 1 FROM m18_goods_receipt_note_log WHERE grn_code = 'DEMO-GRN-001');

INSERT INTO m18_goods_receipt_note_log (m18_record_id, grn_code, stock_in_line_id, purchase_order_line_id, purchase_order_id, po_code, status, message)
SELECT 910502, 'DEMO-GRN-002', sil.id, sil.purchaseOrderLineId, sil.purchaseOrderId, 'DEMO-PO-003', 1, '示範入倉'
FROM stock_in_line sil
WHERE sil.lotNo = 'DEMO-LOT-MK1'
AND NOT EXISTS (SELECT 1 FROM m18_goods_receipt_note_log WHERE grn_code = 'DEMO-GRN-002');

INSERT INTO qc_result (qcItemId, itemId, stockInLineId, failQty, type, remarks, qcPassed)
SELECT @qcLook, @flour, sil.id, 1.00, 'IQC', '包裝略有壓損', 0
FROM stock_in_line sil
WHERE sil.lotNo = 'DEMO-LOT-FL1'
AND NOT EXISTS (
SELECT 1 FROM qc_result qr WHERE qr.stockInLineId = sil.id AND qr.qcItemId = @qcLook
);

INSERT INTO qc_result (qcItemId, itemId, stockInLineId, failQty, type, remarks, qcPassed)
SELECT @qcTemp, @loaf, sil.id, 0.00, 'EPQC', '溫度正常', 1
FROM stock_in_line sil
WHERE sil.lotNo = 'DEMO-LOT-LF1'
AND NOT EXISTS (
SELECT 1 FROM qc_result qr WHERE qr.stockInLineId = sil.id AND qr.qcItemId = @qcTemp
);

INSERT INTO qc_result_measurement (qcResultId, qcType, measureType, value, unit)
SELECT qr.id, 'EPQC', '溫度', 6.50, 'C'
FROM qc_result qr
JOIN stock_in_line sil ON sil.id = qr.stockInLineId
WHERE sil.lotNo = 'DEMO-LOT-LF1' AND qr.qcItemId = @qcTemp
AND NOT EXISTS (SELECT 1 FROM qc_result_measurement m WHERE m.qcResultId = qr.id);

INSERT INTO delivery_order_pick_order (
shopId, storeId, requiredDeliveryDate, truckDepartureTime, truckLanceCode,
shopCode, shopName, loadingSequence, ticketNo, ticketReleaseTime, ticketCompleteDateTime,
ticketStatus, handledBy, handlerName, deliveryNoteCode
)
SELECT @shp, '2F', DATE_SUB(CURDATE(), INTERVAL 1 DAY), '08:00:00', 'DEMO-AM',
'DEMO-SHP', '海港咖啡店', 1, 'DEMO-TK-003', DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 20 HOUR),
'completed', @user, '示範倉務', 'DEMO-DN-DO3'
WHERE NOT EXISTS (SELECT 1 FROM delivery_order_pick_order WHERE ticketNo = 'DEMO-TK-003');

SET @dopo := (SELECT id FROM delivery_order_pick_order WHERE ticketNo = 'DEMO-TK-003' LIMIT 1);

UPDATE pick_order
SET deliveryOrderPickOrderId = @dopo
WHERE code = 'DEMO-PK-DO3' AND deliveryOrderPickOrderId IS NULL;

INSERT INTO stock_out (type, deliveryOrderCode, completeDate, status, handlerId)
SELECT 'do', 'DEMO-DO-003', DATE_SUB(NOW(), INTERVAL 1 DAY), 'completed', @user
WHERE NOT EXISTS (SELECT 1 FROM stock_out WHERE deliveryOrderCode = 'DEMO-DO-003' AND type = 'do');

INSERT INTO stock_out (type, consoPickOrderCode, completeDate, status, handlerId)
SELECT 'job', 'DEMO-PK-JO2', DATE_SUB(NOW(), INTERVAL 2 HOUR), 'completed', @user
WHERE NOT EXISTS (SELECT 1 FROM stock_out WHERE consoPickOrderCode = 'DEMO-PK-JO2' AND type = 'job');

SET @soDo := (SELECT id FROM stock_out WHERE deliveryOrderCode = 'DEMO-DO-003' AND type = 'do' LIMIT 1);
SET @soJo := (SELECT id FROM stock_out WHERE consoPickOrderCode = 'DEMO-PK-JO2' AND type = 'job' LIMIT 1);
SET @polDo := (SELECT pol.id FROM pick_order_line pol WHERE pol.poId = @pkDo3 LIMIT 1);
SET @polJo := (SELECT pol.id FROM pick_order_line pol JOIN items i ON i.id = pol.itemId WHERE pol.poId = @pkJo2 AND i.code = 'DEMO-RM-01' LIMIT 1);
SET @illLoaf := (
SELECT ill.id FROM inventory_lot_line ill
JOIN inventory_lot il ON il.id = ill.inventoryLotId
WHERE il.lotNo = 'DEMO-LOT-LF1' LIMIT 1
);
SET @illFlour := (
SELECT ill.id FROM inventory_lot_line ill
JOIN inventory_lot il ON il.id = ill.inventoryLotId
WHERE il.lotNo = 'DEMO-LOT-FL1' LIMIT 1
);

INSERT INTO stock_out_line (itemId, qty, stockOutId, pickOrderLineId, inventoryLotLineId, status, pickTime, handled_by, endTime)
SELECT @loaf, 6, @soDo, @polDo, @illLoaf, 'completed', DATE_SUB(NOW(), INTERVAL 1 DAY), @user, DATE_SUB(NOW(), INTERVAL 1 DAY)
WHERE NOT EXISTS (SELECT 1 FROM stock_out_line WHERE stockOutId = @soDo AND pickOrderLineId = @polDo);

INSERT INTO stock_out_line (itemId, qty, stockOutId, pickOrderLineId, inventoryLotLineId, status, pickTime, handled_by, endTime)
SELECT @flour, 5, @soJo, @polJo, @illFlour, 'completed', DATE_SUB(NOW(), INTERVAL 2 HOUR), @user, DATE_SUB(NOW(), INTERVAL 2 HOUR)
WHERE NOT EXISTS (SELECT 1 FROM stock_out_line WHERE stockOutId = @soJo AND pickOrderLineId = @polJo);

INSERT INTO do_pick_order_record (
record_id, pick_order_id, do_order_id, shop_id, store_id, RequiredDeliveryDate,
TruckLanceCode, ShopCode, ShopName, delivery_order_code, deliveryNoteCode, pick_order_code,
ticket_no, ticket_release_time, ticketCompleteDateTime, ticket_status, handled_by, handler_name, deleted
)
SELECT 0, @pkDo3, @do3, @shp, '2F', DATE_SUB(CURDATE(), INTERVAL 1 DAY),
'DEMO-AM', 'DEMO-SHP', '海港咖啡店', 'DEMO-DO-003', 'DEMO-DN-DO3', 'DEMO-PK-DO3',
'DEMO-TK-003', DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 20 HOUR), 'completed', @user, '示範倉務', 0
WHERE NOT EXISTS (SELECT 1 FROM do_pick_order_record WHERE ticket_no = 'DEMO-TK-003');

UPDATE do_pick_order_record SET record_id = id WHERE ticket_no = 'DEMO-TK-003' AND record_id = 0;

SET @dpor := (SELECT id FROM do_pick_order_record WHERE ticket_no = 'DEMO-TK-003' LIMIT 1);

INSERT INTO do_pick_order_line_record (
record_id, do_pick_order_id, pick_order_id, do_order_id, pick_order_code, delivery_order_code, status, deleted
)
SELECT @dpor, @dopo, @pkDo3, @do3, 'DEMO-PK-DO3', 'DEMO-DO-003', 'correct', 0
WHERE @dpor IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM do_pick_order_line_record WHERE record_id = @dpor AND do_order_id = @do3);

INSERT INTO stock_ledger (date, inventoryId, itemId, itemCode, inQty, outQty, balance, type, modified)
SELECT DATE_SUB(CURDATE(), INTERVAL 20 DAY), inv.id, inv.itemId, i.code, inv.onHandQty, 0, inv.onHandQty, 'IN', DATE_SUB(NOW(), INTERVAL 20 DAY)
FROM inventory inv
JOIN items i ON i.id = inv.itemId
WHERE i.code IN ('DEMO-RM-01', 'DEMO-RM-02', 'DEMO-FG-01')
AND NOT EXISTS (
SELECT 1 FROM stock_ledger sl WHERE sl.itemCode = i.code AND sl.type = 'IN' AND sl.deleted = 0
);

INSERT INTO stock_ledger (date, inventoryId, itemId, itemCode, inQty, outQty, balance, type, stockOutLineId, modified)
SELECT DATE_SUB(CURDATE(), INTERVAL 1 DAY), inv.id, @loaf, 'DEMO-FG-01', 0, 6, inv.onHandQty, 'DO', sol.id, DATE_SUB(NOW(), INTERVAL 1 DAY)
FROM inventory inv
JOIN stock_out_line sol ON sol.stockOutId = @soDo
WHERE inv.itemId = @loaf
AND NOT EXISTS (SELECT 1 FROM stock_ledger sl WHERE sl.itemCode = 'DEMO-FG-01' AND sl.type = 'DO' AND sl.deleted = 0);

INSERT INTO stock_ledger (date, inventoryId, itemId, itemCode, inQty, outQty, balance, type, stockOutLineId, modified)
SELECT CURDATE(), inv.id, @flour, 'DEMO-RM-01', 0, 5, inv.onHandQty, 'JO', sol.id, DATE_SUB(NOW(), INTERVAL 2 HOUR)
FROM inventory inv
JOIN stock_out_line sol ON sol.stockOutId = @soJo
WHERE inv.itemId = @flour
AND NOT EXISTS (SELECT 1 FROM stock_ledger sl WHERE sl.itemCode = 'DEMO-RM-01' AND sl.type = 'JO' AND sl.deleted = 0);

INSERT INTO stock_in (code, supplierId, shopId, m18DataLogId, orderDate, completeDate, status)
SELECT 'DEMO-SI-RPT', @sup, @shp, @log, DATE_SUB(NOW(), INTERVAL 40 DAY), DATE_SUB(NOW(), INTERVAL 39 DAY), 'completed'
WHERE NOT EXISTS (SELECT 1 FROM stock_in WHERE code = 'DEMO-SI-RPT');

SET @siRpt := (SELECT id FROM stock_in WHERE code = 'DEMO-SI-RPT' LIMIT 1);

INSERT INTO stock_in_line (itemId, itemNo, stockInId, demandQty, acceptedQty, status, lotNo, expiryDate, receiptDate, productionDate, productLotNo, dnNo)
SELECT i.id, i.code, @siRpt, 20, 20, 'completed', CONCAT('DEMO-LOT-', i.code), DATE_ADD(CURDATE(), INTERVAL 90 DAY), DATE_SUB(NOW(), INTERVAL 39 DAY), DATE_SUB(NOW(), INTERVAL 40 DAY), CONCAT('PLOT-', i.code), CONCAT('DN-', i.code)
FROM items i
WHERE i.code IN ('DEMO-MAT-01', 'DEMO-WIP-01', 'DEMO-FG-03', 'DEMO-NM-01', 'DEMO-CMB-01')
AND NOT EXISTS (SELECT 1 FROM stock_in_line sil WHERE sil.itemId = i.id AND sil.stockInId = @siRpt);

INSERT INTO inventory_lot (itemId, stockInLineId, expiryDate, lotNo, stockInDate, productionDate)
SELECT sil.itemId, sil.id, sil.expiryDate, sil.lotNo, sil.receiptDate, sil.productionDate
FROM stock_in_line sil
WHERE sil.stockInId = @siRpt
AND NOT EXISTS (SELECT 1 FROM inventory_lot il WHERE il.stockInLineId = sil.id);

UPDATE stock_in_line sil
JOIN inventory_lot il ON il.stockInLineId = sil.id
SET sil.inventoryLotId = il.id
WHERE sil.stockInId = @siRpt AND sil.inventoryLotId IS NULL;

INSERT INTO inventory_lot_line (inventoryLotId, warehouseId, stockItemUomId, inQty, outQty, holdQty, status)
SELECT il.id, @whA, iu.id, 20, 0, 0, 'available'
FROM inventory_lot il
JOIN stock_in_line sil ON sil.id = il.stockInLineId
JOIN item_uom iu ON iu.itemId = il.itemId AND iu.stockUnit = 1 AND iu.deleted = 0
WHERE sil.stockInId = @siRpt
AND NOT EXISTS (SELECT 1 FROM inventory_lot_line ill WHERE ill.inventoryLotId = il.id);

INSERT INTO stock_ledger (date, inventoryId, itemId, itemCode, inQty, outQty, balance, type, modified)
SELECT DATE_SUB(CURDATE(), INTERVAL 30 DAY), inv.id, inv.itemId, i.code, 20, 0, 20, 'IN', DATE_SUB(NOW(), INTERVAL 30 DAY)
FROM inventory inv
JOIN items i ON i.id = inv.itemId
WHERE i.code IN ('DEMO-MAT-01', 'DEMO-WIP-01', 'DEMO-FG-03', 'DEMO-NM-01', 'DEMO-CMB-01')
AND NOT EXISTS (SELECT 1 FROM stock_ledger sl WHERE sl.itemCode = i.code AND sl.type = 'IN' AND sl.deleted = 0);

INSERT INTO stock_ledger (date, inventoryId, itemId, itemCode, inQty, outQty, balance, type, modified)
SELECT STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-01-15'), '%Y-%m-%d'), inv.id, i.id, i.code, 0, 4, 16, 'OUT', STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-01-15 10:00:00'), '%Y-%m-%d %H:%i:%s')
FROM items i
JOIN inventory inv ON inv.itemId = i.id
WHERE i.code IN ('DEMO-MAT-01', 'DEMO-WIP-01', 'DEMO-FG-03', 'DEMO-NM-01', 'DEMO-CMB-01')
AND NOT EXISTS (SELECT 1 FROM stock_ledger sl WHERE sl.itemCode = i.code AND sl.type = 'OUT' AND MONTH(sl.modified) = 1);

INSERT INTO stock_ledger (date, inventoryId, itemId, itemCode, inQty, outQty, balance, type, modified)
SELECT STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-04-15'), '%Y-%m-%d'), inv.id, i.id, i.code, 0, 6, 10, 'OUT', STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-04-15 10:00:00'), '%Y-%m-%d %H:%i:%s')
FROM items i
JOIN inventory inv ON inv.itemId = i.id
WHERE i.code IN ('DEMO-MAT-01', 'DEMO-WIP-01', 'DEMO-FG-03', 'DEMO-NM-01', 'DEMO-CMB-01')
AND NOT EXISTS (SELECT 1 FROM stock_ledger sl WHERE sl.itemCode = i.code AND sl.type = 'OUT' AND MONTH(sl.modified) = 4);

INSERT INTO stock_ledger (date, inventoryId, itemId, itemCode, inQty, outQty, balance, type, modified)
SELECT STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-07-15'), '%Y-%m-%d'), inv.id, i.id, i.code, 0, 3, 7, 'OUT', STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-07-15 10:00:00'), '%Y-%m-%d %H:%i:%s')
FROM items i
JOIN inventory inv ON inv.itemId = i.id
WHERE i.code IN ('DEMO-MAT-01', 'DEMO-WIP-01', 'DEMO-FG-03', 'DEMO-NM-01', 'DEMO-CMB-01')
AND NOT EXISTS (SELECT 1 FROM stock_ledger sl WHERE sl.itemCode = i.code AND sl.type = 'OUT' AND MONTH(sl.modified) = 7);

INSERT INTO stock_ledger (date, inventoryId, itemId, itemCode, inQty, outQty, balance, type, modified)
SELECT STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-09-10'), '%Y-%m-%d'), inv.id, i.id, i.code, 0, 5, 2, 'OUT', STR_TO_DATE(CONCAT(YEAR(CURDATE()), '-09-10 10:00:00'), '%Y-%m-%d %H:%i:%s')
FROM items i
JOIN inventory inv ON inv.itemId = i.id
WHERE i.code IN ('DEMO-MAT-01', 'DEMO-WIP-01', 'DEMO-FG-03', 'DEMO-NM-01', 'DEMO-CMB-01')
AND NOT EXISTS (SELECT 1 FROM stock_ledger sl WHERE sl.itemCode = i.code AND sl.type = 'OUT' AND MONTH(sl.modified) = 9);

INSERT INTO stock_take (code, planStart, planEnd, actualStart, actualEnd, status, stockTakeRoundId, stockTakeRoundName, stockTakeSection)
SELECT 'DEMO-ST-001', DATE_SUB(NOW(), INTERVAL 2 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 2 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY), 'completed', 9001, '示範盤點輪次', 'A'
WHERE NOT EXISTS (SELECT 1 FROM stock_take WHERE code = 'DEMO-ST-001');

SET @st := (SELECT id FROM stock_take WHERE code = 'DEMO-ST-001' LIMIT 1);
SET @lotFl := (SELECT id FROM inventory_lot WHERE lotNo = 'DEMO-LOT-FL1' LIMIT 1);
SET @lotMk := (SELECT id FROM inventory_lot WHERE lotNo = 'DEMO-LOT-MK1' LIMIT 1);
SET @lotLf := (SELECT id FROM inventory_lot WHERE lotNo = 'DEMO-LOT-LF1' LIMIT 1);

INSERT INTO stocktakerecord (
itemId, lotId, warehouseId, stockTakeId, stockTakeRoundId, stockTakerId, stockTakerName,
approverId, approverName, bookQty, approverStockTakeQty, varianceQty, uom, date, status,
approverTime, itemCode, itemName, lotNo, expiredDate
)
SELECT @flour, @lotFl, @whA, @st, 9001, @user, '示範倉務', @user, '示範管理員', 40, 38, -2, '公斤', DATE_SUB(CURDATE(), INTERVAL 1 DAY), 'completed', DATE_SUB(NOW(), INTERVAL 1 DAY), 'DEMO-RM-01', '普通麵粉', 'DEMO-LOT-FL1', DATE_ADD(CURDATE(), INTERVAL 180 DAY)
WHERE NOT EXISTS (SELECT 1 FROM stocktakerecord WHERE stockTakeId = @st AND lotId = @lotFl);

INSERT INTO stocktakerecord (
itemId, lotId, warehouseId, stockTakeId, stockTakeRoundId, stockTakerId, stockTakerName,
bookQty, approverStockTakeQty, varianceQty, uom, date, status, itemCode, itemName, lotNo
)
SELECT @milk, @lotMk, @whA, @st, 9001, @user, '示範倉務', 20, 20, 0, '公斤', DATE_SUB(CURDATE(), INTERVAL 1 DAY), 'completed', 'DEMO-RM-02', '鮮奶', 'DEMO-LOT-MK1'
WHERE NOT EXISTS (SELECT 1 FROM stocktakerecord WHERE stockTakeId = @st AND lotId = @lotMk);

INSERT INTO stocktakerecord (
itemId, lotId, warehouseId, stockTakeId, stockTakeRoundId, stockTakerId, stockTakerName,
bookQty, uom, date, status, itemCode, itemName, lotNo
)
SELECT @loaf, @lotLf, @whB, @st, 9001, @user, '示範倉務', 30, '件', CURDATE(), 'pending', 'DEMO-FG-01', '鄉村麵包', 'DEMO-LOT-LF1'
WHERE NOT EXISTS (SELECT 1 FROM stocktakerecord WHERE stockTakeId = @st AND lotId = @lotLf);

INSERT INTO delivery_order (code, shopId, supplierId, currencyId, orderDate, estimatedArrivalDate, status, m18DataLogId, m18BeId, isExtra)
SELECT 'DEMO-DO-004', @shp, @sup, currencyId, NOW(), CURDATE(), 'pending', m18DataLogId, m18BeId, 0
FROM delivery_order
WHERE code = 'DEMO-DO-001'
AND NOT EXISTS (SELECT 1 FROM delivery_order WHERE code = 'DEMO-DO-004');

SET @do4 := (SELECT id FROM delivery_order WHERE code = 'DEMO-DO-004' LIMIT 1);

INSERT INTO delivery_order_line (deliveryOrderId, itemId, itemNo, uomId, uomIdM18, qty, qtyM18, status, m18DataLogId)
SELECT @do4, @loaf, 'DEMO-FG-01', @kg, @kg, 3, 3, 'pending', @log
WHERE @do4 IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM delivery_order_line WHERE deliveryOrderId = @do4);

SET @doLine3 := (
SELECT dol.id FROM delivery_order_line dol WHERE dol.deliveryOrderId = @do3 LIMIT 1
);

INSERT INTO do_replenishment (
code, deliveryDate, sourceDoId, sourceDoCode, sourceDoLineId, sourceM18DataLogId, sourceM18Id,
itemId, itemNo, itemName, replenishQty, uomId, shopId, shopCode, shopName, truckLaneCode,
status, reason, deliveryOrderPickOrderId, pickOrderLineId
)
SELECT 'DEMO-REPL-001', DATE_SUB(CURDATE(), INTERVAL 1 DAY), @do3, 'DEMO-DO-003', @doLine3, @log, 910601,
@loaf, 'DEMO-FG-01', '鄉村麵包', 2, @pcs, @shp, 'DEMO-SHP', '海港咖啡店', 'DEMO-AM',
'completed', '示範補貨', @dopo, @polDo
WHERE NOT EXISTS (SELECT 1 FROM do_replenishment WHERE code = 'DEMO-REPL-001');

INSERT INTO m18_bom_shop_sync_log (bom_id, finished_item_code, m18_header_code, m18_api_status, synced, message)
SELECT @bom, 'DEMO-FG-01', 'DEMO-BOM-HDR', 1, 1, '示範同步成功'
WHERE NOT EXISTS (SELECT 1 FROM m18_bom_shop_sync_log WHERE finished_item_code = 'DEMO-FG-01' AND synced = 1);

INSERT INTO m18_bom_shop_sync_log (bom_id, finished_item_code, m18_header_code, m18_api_status, synced, message)
SELECT b.id, 'DEMO-FG-03', 'DEMO-BOM-HDR-2', 0, 0, '示範同步失敗'
FROM bom b
WHERE b.code = 'DEMO-FG-03'
AND NOT EXISTS (SELECT 1 FROM m18_bom_shop_sync_log WHERE finished_item_code = 'DEMO-FG-03' AND synced = 0);

INSERT INTO settings (name, value, category, type)
SELECT 'DEMO.coverage.v1', 'loaded', 'DEMO', 'marker'
WHERE NOT EXISTS (SELECT 1 FROM settings WHERE name = 'DEMO.coverage.v1');
-- Retired. Demo data is an anonymized copy of production.
-- Do not run this script. It inserts fictional DEMO- rows.

読み込み中…
キャンセル
保存